Redux-Toolkit dispatch in useEffect hook results in an infinite loop - reactjs

Problem
I use useMuation from react-query to do post requests and get the user info from JSON and then try to store it to my redux store using useEffect according to the status given by react-query useMutation hook which is success. The problem rises in this status. all info is successfully stored in the redux store as you can see in the picture, but it causes infinite loop.
I tried to put an empty dependency array and even put userData?.data?.data?.user?.name and userData?.data?.token instead of userData but still the same.
Error
Store
userSlice.ts
import { createSlice, configureStore, PayloadAction } from "#reduxjs/toolkit";
type initialState = {
user: string;
dashboardIndex: number;
theme: string;
token: string;
isLoggedIn: boolean;
};
const initialState: initialState = {
user: "",
dashboardIndex: 0,
theme: "themeLight",
token: "",
isLoggedIn: false,
};
const userSlice = createSlice({
name: "user",
initialState: initialState,
reducers: {
updateUser(state, action: PayloadAction<string>) {
state.user = action.payload;
},
updateDashboardIndex(state, action: PayloadAction<number>) {
state.dashboardIndex = action.payload;
},
updateTheme(state, action: PayloadAction<string>) {
state.theme = action.payload;
},
updateToken(state, action: PayloadAction<string>) {
state.token = action.payload;
},
updateIsLoggedIn(state, action: PayloadAction<boolean>) {
state.isLoggedIn = action.payload;
},
reset: () => initialState,
},
});
// ...
Login.tsx
const LoginComponents = () => {
let navigate = useNavigate();
const [loginObject, setLoginOject] = useState<loginObjectType>({
email: "",
password: "",
});
const {
mutate,
error,
isError,
isSuccess,
data: userData,
} = useQueryMutationInvalidateHooksPost("api/v1/users/login");
const dispatch = useAppDispatch();
...
// Signin process handler
useEffect(() => {
console.log("hi");
if (isSuccess) {
if (userData) {
dispatch(usersActions.updateUser(userData?.data?.data?.user?.name));
dispatch(usersActions.updateToken(userData?.data?.token));
dispatch(usersActions.updateIsLoggedIn(!!userData?.data?.token));
alert(
`Succeeded in login. Welcome ${userData?.data?.data?.user?.name}!`
);
navigate("/home");
}
}
if (isError) {
if (error instanceof AxiosError) {
alert(error?.response?.data?.message);
}
}
}, [navigate, error, isSuccess, isError, userData, dispatch]);
// Button functions
const submitHandler = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.preventDefault();
if (!loginObject?.email || !loginObject?.password) {
alert("Please input all required fields.");
} else if (loginObject?.email && loginObject.password) {
// fetching data
const temp = {
...loginObject,
};
mutate(temp);
}
};

I would suggest to use useEffect hook only for initializations and not as a handler.
To react on mutation success or error use the mutation as follows.
mutate(temp, {
onSuccess: (data, variables, context) => {
// add your success handling logic here
},
onError: (error, variables, context) => {
// Add your error handling logic here
},
onSettled: (data, error, variables, context) => {
// Code that must run, irrelevant of success or error, should be added here.
},
})
This way you will get rid of your Signin process handler useeffect which will remove your looping logic.

Related

extraReducers builder.addCase rejected state not showing Using Redux Thunk With Redux Toolkit With Typescript

I am trying to make a POST request to an endpoint with React. On fulfilled state of my builder.addCase reducer, my data is generated or return back to me when fulfilled, but on rejected state of my builder.addCase reducer, checking my Redux Dev Tools, no rejected state is found even though I have an error. Rather the error message that ought to be in rejected state is found or populated to fulfilled state. Checking Redux Dev Tools I can only find pending and fulfilled state, rejected state is nowhere to be found.
Here is my code:
export const userRegisterAction = createAsyncThunk(
"users/register",
async (user: User, { rejectWithValue }) => {
try {
const response = await axios.post(
"http://localhost:5000/api/users/register",
user,
);
return response.data;
} catch (error) {
return rejectWithValue(error);
}
}
);
Here is my slice:
const usersSlices = createSlice({
name: "users",
initialState: {
userAuth: "login",
registered: {},
loading: false,
Error: ""
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(userRegisterAction.pending, (state) => {
state.loading = true
})
builder.addCase(userRegisterAction.fulfilled, (state, { payload }) => {
state.registered = payload
})
builder.addCase(userRegisterAction.rejected, (state) => {
state.loading = false;
})
}
});
export default usersSlices.reducer;
Here is where I dispatch my action.
const dispatch = useAppDispatch();
export interface User {
firstName : string;
password : string;
lastName: string;
email : string;
}
export const Signup = (): JSX.Element => {
const [passwordFieldType, setPasswordFieldType] = useState<boolean>(false);
const [user, setUser] = useState<User>({
firstName: "",
lastName: "",
email: "",
password: ""
});
const dispatch = useAppDispatch();
const handleInputChange = (e: any) => {
const name = e.target.name;
const value = e.target.value;
setUser({
...user,
[name]: value.trim()
});
}
const handleInputSubmit = async (event: any) => {
event.preventDefault();
const { firstName, lastName, email, password } = user;
if (!firstName || !lastName || !email || !password) {
return toast.error(
"Please, fill up all inputs !!!",
{
toastId: "fill_inputs",
position: toast.POSITION.TOP_CENTER,
autoClose: 1000,
}
);
}
const response = await dispatch(userRegisterAction({
firstName,
email,
password,
lastName
}))
}
}
I have tried everything I could, but to no avail. I've checked online too, no related help or answer to the issue.
I encountered the same challenge a few minutes ago. For me, it was because I did not add "return" before my "rejectWithValue"; which prevented my "rejected" from working because the function is a promise which has three states. And whenever you are working with a promise, always return your response.
For you, you need to parse "error.response.data" instead of only "error" in your reject function and update the state "Error" with the rejected action. See the implementation below.
The previous implementation of the userRegisterAction function
export const userRegisterAction = createAsyncThunk(
"users/register",
async (user: User, { rejectWithValue }) => {
try {
const response = await axios.post(
"http://localhost:5000/api/users/register",
user,
);
return response.data;
} catch (error) {
return rejectWithValue(error);
}
}
);
The new implementation of the userRegisterAction function
export const userRegisterAction = createAsyncThunk(
"users/register",
async (user: User, { rejectWithValue }) => {
try {
const response = await axios.post(
"http://localhost:5000/api/users/register",
user,
);
return response.data;
} catch (error) {
return rejectWithValue(error.response.data); //This carries the response you are receiving from the server
}
}
);
Previous implementation of the usersSlice
const usersSlices = createSlice({
name: "users",
initialState: {
userAuth: "login",
registered: {},
loading: false,
Error: ""
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(userRegisterAction.pending, (state) => {
state.loading = true
})
builder.addCase(userRegisterAction.fulfilled, (state, { payload }) => {
state.registered = payload
})
builder.addCase(userRegisterAction.rejected, (state) => {
state.loading = false;
})
}
});
export default usersSlices.reducer;
New implementation of usersSlice
const usersSlices = createSlice({
name: "users",
initialState: {
userAuth: "login",
registered: {},
loading: false,
Error: ""
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(userRegisterAction.pending, (state) => {
state.loading = true
})
builder.addCase(userRegisterAction.fulfilled, (state, { payload }) => {
state.registered = payload
})
builder.addCase(userRegisterAction.rejected, (state) => {
state.loading = false;
state.Error = payload; // Note: I updated the state here
})
}
});
export default usersSlices.reducer;
I hope this helps you resolve the bug.

State value not change when dispatch to another reduces in redux

I am building login system. I am having problem with store state value when I use Selector function. in my system, I want to get isSuccess, error, userInfo from store state values
/login/index.js
const userLogin = useSelector((state) => state.userLogin);
console.log(userLogin);
const { isSuccess, error, userInfo } = userLogin;
if error it will show error message on UI, otherwise if isSuccess it will show success message. isSuccess appears when post to api success (it mean user login success) and user data is dispatched to loginSuccess reduces else error value dispatched to loginFail reduces
/action/userAction.js
import {
loginSuccess,
loginFail,
} from "\~/redux/user/userLoginSlice";
export const login = (inputs) =\> async (dispatch) =\> {
try {
const {data} = await request.post("/auth/login", inputs);
dispatch(loginSuccess(data));
localStorage.setItem("userInfo", JSON.stringify(data));
} catch (error) {
console.log(error.response.data)
dispatch(loginFail(error.response.data))
}
. however instead of just getting isSuccess in loginSucces reduce, it also get error when previous login was loginFail
console.log(userLogin). it should only show userInfo and isSuccess. anyone help me to solve this problem with . this is reduces
userLoginSlice.js
import { createSlice } from '#reduxjs/toolkit';
const userLoginSlice = createSlice({
name: 'userInfo',
initialState: {},
reducers: {
loginSuccess: (state, action) => {
state.isSuccess = true;
state.userInfo = action.payload;
},
loginFail: (state, action) => {
state.error = action.payload
},
},
});
export const { loginSuccess, loginFail } = userLoginSlice.actions;
export default userLoginSlice.reducer;
and my reduce store
store.js
import { configureStore } from '#reduxjs/toolkit';
import productModalReducer from './product-modal/productModalSlice';
import cartItemsSlice from './cart/cartSlide';
import userLoginSlice from './user/userLoginSlice.js';
import userRegisterSlice from './user/userRegisterSlice.js';
import userUpdateSlice from './user/userUpdateSlice.js';
const userInfoFromStorage = localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')) : null;
const initialState = {
userLogin: { userInfo: userInfoFromStorage },
};
export const store = configureStore({
reducer: {
productModal: productModalReducer,
cartItems: cartItemsSlice,
userLogin: userLoginSlice,
userRegister: userRegisterSlice,
userUpdate: userUpdateSlice,
},
preloadedState: initialState,
});
State value not change when dispatch to another reduces in redux
It seems that this could be because loginSuccess is only adding userInfo and isSuccess to the state but not omitting previous error.
Perhaps try reset state value to exclude error in the reducer:
const userLoginSlice = createSlice({
name: "userInfo",
initialState: {},
reducers: {
loginSuccess: (state, action) => {
// 👇 Reset state to omit error message in loginSuccess
state = { isSuccess: true, userInfo: action.payload };
},
loginFail: (state, action) => {
// 👇 Could add "isSuccess: false, userInfo: null" if needed
state = { error: action.payload };
},
},
});
Or return a state value instead of using Immer proxy state:
const userLoginSlice = createSlice({
name: "userInfo",
initialState: {},
reducers: {
loginSuccess: (state, action) => {
// 👇 Reset state to omit error message in loginSuccess
return { isSuccess: true, userInfo: action.payload };
},
loginFail: (state, action) => {
// 👇 Could add "isSuccess: false, userInfo: null" if needed
return { error: action.payload };
},
},
});

Redux-Toolkit dispatch in useEffect hook goes to an infinite loop

problem
Hello. sir/madam. I'm having this issue and driving me crazy now. It might be appreciated if you can help me to find a solution for this.
I use useMuation from react-query to do post requests and get the user info from JSON and then try to store it to my redux store using useEffect according to the status given by react-query useMutation hook which is success. The problem rises in this status. all info is successfully stored in the redux store as you can see in the picture, but it causes infinite loop.
I tried to put an empty dependency array and even put userData?.data?.data?.user?.name and userData?.data?.token instead of userData but still the same.
Could anybody can help me out..?
Error
Store
userSlice.ts
import { createSlice, configureStore, PayloadAction } from "#reduxjs/toolkit";
type initialState = {
user: string;
dashboardIndex: number;
theme: string;
token: string;
isLoggedIn: boolean;
};
const initialState: initialState = {
user: "",
dashboardIndex: 0,
theme: "themeLight",
token: "",
isLoggedIn: false,
};
const userSlice = createSlice({
name: "user",
initialState: initialState,
reducers: {
updateUser(state, action: PayloadAction<string>) {
state.user = action.payload;
},
updateDashboardIndex(state, action: PayloadAction<number>) {
state.dashboardIndex = action.payload;
},
updateTheme(state, action: PayloadAction<string>) {
state.theme = action.payload;
},
updateToken(state, action: PayloadAction<string>) {
state.token = action.payload;
},
updateIsLoggedIn(state, action: PayloadAction<boolean>) {
state.isLoggedIn = action.payload;
},
reset: () => initialState,
},
});
...
Login.tsx
const LoginComponents = () => {
let navigate = useNavigate();
const [loginObject, setLoginOject] = useState<loginObjectType>({
email: "",
password: "",
});
const {
mutate,
error,
isError,
isSuccess,
data: userData,
} = useQueryMutationInvalidateHooksPost("api/v1/users/login");
const dispatch = useAppDispatch();
...
// Signin process handler
useEffect(() => {
console.log("hi");
if (isSuccess) {
if (userData) {
dispatch(usersActions.updateUser(userData?.data?.data?.user?.name));
dispatch(usersActions.updateToken(userData?.data?.token));
dispatch(usersActions.updateIsLoggedIn(!!userData?.data?.token));
alert(
`Succeeded in login. Welcome ${userData?.data?.data?.user?.name}!`
);
navigate("/home");
}
}
if (isError) {
if (error instanceof AxiosError) {
alert(error?.response?.data?.message);
}
}
}, [navigate, error, isSuccess, isError, userData, dispatch]);
// Button functions
const submitHandler = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.preventDefault();
if (!loginObject?.email || !loginObject?.password) {
alert("Please input all required fields.");
} else if (loginObject?.email && loginObject.password) {
// fetching data
const temp = {
...loginObject,
};
mutate(temp);
}
};
I tried to put an empty dependency array and even put userData?.data?.data?.user?.name and userData?.data?.token instead of userData but still the same.
Could anybody can help me out..?

Redux toolkit not checking the rejected case

I am new to redux toolkit, My objective is to not let the dashboard to render if the user who is trying to login is not a registered member. I am sending a 500 error from the backend if the username or password is invalid which works fine. The extraReducers is not going to rejected case even if the api response is 500 instead it goes to the fulfilled case.My expectation was that if the api gives an error, it will go to the rejected case. Below is my slice,
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import axios from "axios";
export const currentUser = localStorage.getItem("userInfo");
const initialState = {
currentUser: currentUser
? currentUser
: {
email: "",
password: "",
name: "",
pic: "",
confirmPassword: "",
},
status: null,
isLoggedIn: currentUser ? true : false,
userInfoFromStorage: "",
};
//login action
export const authUser = createAsyncThunk("users/authUser", async (data) => {
const res = await axios.post("http://localhost:3010/api/users/login", data);
return res.data;
});
//register action
export const registerUser = createAsyncThunk(
"users/registerUser",
async (data) => {
try {
const res = await axios.post("http://localhost:3010/api/users", data);
console.log(res);
return res.data;
} catch (error) {
console.log(error);
}
}
);
const userReducer = createSlice({
name: "user", //to identify the slice
initialState,
reducers: {
logoutUser: (state, action) => {
state.isLoggedIn = false;
localStorage.removeItem("userInfo");
},
},
extraReducers: (builder) => {
builder.addCase(authUser.pending, (state) => {
console.log("pending");
state.status = "pending";
});
builder.addCase(authUser.fulfilled, (state, action) => {
state.status = "success";
state.isLoggedIn = true;
state.currentUser = action.payload;
state.userInfoFromStorage = localStorage.setItem(
"userInfo",
JSON.stringify(action.payload)
);
});
builder.addCase(authUser.rejected, (state) => {
console.log("failed")
state.status = "failed";
state.isLoggedIn = false;
});
},
});
// Action creators are generated for each case reducer function
export const { logoutUser } = userReducer.actions;
export default userReducer.reducer;
dispatching action below from loginPage
const submitHandler = async (e) => {
e.preventDefault();
dispatch(authUser({ email: email, password: password }));
// history.push("/myNotes");
};
In your thunk, you are manually handling the error. There is no way that any other code after that could know that something went wrong.
You have three options here:
do not try..catch in your createAsyncThunk call. The error will be cleaned up from non-standard fields and end up in action.error of the rejected action
manually throw something. The error will be cleaned up from non-standard fields and end up in action.error of the rejected action
manually return thunkApi.rejectWithValue(yourError). The error will not be cleaned up and land in action.payload of the rejected action.
What you are doing now (handling the error and returning nothing) is essentially equal to doing return undefined in your function - which will end up as a fulfilled action with undefined as payload.

Common loading state reducer with Redux toolkit

I'm working on an app where I have multiple slices. I'm using createAsyncThunk for API calls and I like it cause it provides action creators for different state of API request, so that I can track loading state and errors within the reducer. But my question is, what if I want to have a separate reducer to track loading, error and success of my API calls how do I accomplish that with redux-toolkit
I know I can dispatch an action from within my createAsyncThunk function but it doesn't feel right and kinda defeats the purpose of the function itself. Also, side effects inside the reducer are considered to be a bad practice. So, I'm kinda confused at this point, I want to have just one Loader component in the root of the app that gets triggered when the loading state is true and it doesn't matter what exactly is loading
Here is an example of my current code:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
import { AxiosError } from 'axios'
import { masterInstance } from 'api'
import { GetAccessCodeParams, RegistrationStateType } from 'store/slices/registration/types'
export const getAccessCodeRequest = createAsyncThunk<void, GetAccessCodeParams, { rejectValue: { message: string } }>(
'registration/getAccessCodeRequest',
async ({ email }, { rejectWithValue }) => {
try {
await masterInstance.post(`/authorization/getAccessCodeWc`, { email })
} catch (err) {
let error: AxiosError = err
if (error) {
return rejectWithValue({
message: `Error. Error code ${error.response?.status}`,
})
}
throw err
}
}
)
const initialState: RegistrationStateType = {
isLoading: false,
error: null,
}
const registrationSlice = createSlice({
name: 'registration',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(getAccessCodeRequest.fulfilled, (state) => {
state.isLoading = false
state.error = null
})
builder.addCase(getAccessCodeRequest.pending, (state) => {
state.isLoading = true
state.error = null
})
builder.addCase(getAccessCodeRequest.rejected, (state, action) => {
if (action.payload) {
state.error = {
message: action.payload.message,
}
} else {
state.error = action.error
}
state.isLoading = false
})
},
})
export const registrationReducer = registrationSlice.reducer
I want isLoading and error to be in a separate reducer
You could have a shared reducer matcher function.
// mySharedStuff.js
export const handleLoading = (action, (state) => {
state.loading = action.type.endsWith('/pending'); // or smth similar
});
export const handleError = (action, (state) => {
state.error = action.type.endsWith('/rejected'); // or smth similar
});
// mySlice.js
const mySlice = createSlice({
name: 'FOO',
initialState: {},
reducers: {},
extraReducers: builder => {
builder.addMatcher(handleLoading),
builder.addMatcher(handleError),
...

Resources