redux createAsyncThunk dispatching twice with Formik - reactjs

This thunk is being dispatched on a user event (click log in) on a Formik element.
For some reason it dispatches twice. I have commented out strictMode to exclude that possibility.
In my redux slice:
// userSlice.js
const initialState = {
user: Cookies.get('user') ? JSON.parse(Cookies.get('user')) : null,
};
export const loginUser = createAsyncThunk('users/login', async userInputs => {
try {
const { data } = await axios.post('url', userInputs);
Cookies.set('user', JSON.stringify(data));
return data;
} catch (error) {
return error.response.data;
}
});
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(loginUser.fulfilled, (state, action) => {
state.user = action.payload;
});
},
});
In the component dispatching the thunk:
// loginForm.js
const loginSubmitHandler = async () => {
const data = await dispatch(
loginUser({
email,
password,
})
).unwrap();
if (data.message) {
setError(data.message);
}
else {
setError('');
navigate('/');
}
};
<Formik
enableReinitialize
initialValues={{
email,
password,
}}
validationSchema={loginvalidation}
onSubmit={loginSubmitHandler}></Formik>;
Any ideas?

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.

Redux toolkit how to call action from other slice on one action fullfilled

I have a async action name editLoginIdData() in loginsIdSlice.js,
which i am dispatching from certain component, which edits the data in mongoDB database, then when the action is fullfilled, i mutate the state in extraReducers
editLoginIdData.fulfilled.
But now what i want to do that whenever the editLoginIdData action is fullfilled
i want to also add the updated(which i will get from server responese -> updatedData at editLoginIdData()) data to activitiesData state, which i am handling in activitiesSlice.js
So basically is there a way that when editLoginIdData action is fullfilled we can
dispatch somehow mutate the state in other slice.
One approach i have taken is to import the editLoginIdData() action in activitiesSlice.js and then creating a extraReducer with editLoginIdData.fullfilled
and mutating activitiesData state.
I have done the above approach and seems its working correctly.
But there is a catch, like how show i get the response data at editLoginIdData()
to passed to activitiesSlice.js because i will required that updated data.
if the above appraoch is not correct, then how should i do it
loginsIdSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import * as api from "../../api"
const initialState = {
loginsIdData: [],
}
export const fecthLoginIdsData = createAsyncThunk("loginIds/fetch", async ({ user_id }, { getState }) => {
const res = await api.fetchUserLoginIds(user_id);
console.log(res);
const { data } = res;
data.reverse();
return data;
});
export const addNewLoginIdData = createAsyncThunk("loginIds/add", async ({ data, user_id }, { getState }) => {
const res = await api.addNewLoginIdA(data, user_id)
const { loginIdsArray } = res.data;
return loginIdsArray[loginIdsArray.length - 1];
});
export const editLoginIdData = createAsyncThunk("loginIds/edit", async ({ updatedData, login_id }, { getState }) => {
const res = await api.editLoginId(login_id, updatedData);
// console.log(updatedData);
return updatedData;
});
export const deleteLoginData = createAsyncThunk("loginIds/delete", async ({ login_id, user_id }, { getState }) => {
const res = await api.deleteLoginId(login_id, user_id);
// console.log(res);
const { data } = res;
// console.log(data);
return data.reverse();
});
//* Slice
const loginsIdSlice = createSlice({
name: 'loginsId',
initialState: initialState,
extraReducers: (builder) => {
builder.
addCase(fecthLoginIdsData.fulfilled, (state, action) => {
return {
...state,
loginsIdData: action.payload
};
}).
addCase(addNewLoginIdData.fulfilled, (state, action) => {
return {
...state,
loginsIdData: [action.payload, ...state.loginsIdData]
};
}).
addCase(editLoginIdData.fulfilled, (state, action) => {
const newArray = state.loginsIdData.map((loginId) => {
if (loginId._id === action.payload._id) {
return action.payload;
} else {
return loginId;
}
});
return {
...state,
loginsIdData: newArray,
};
}).
addCase(deleteLoginData.fulfilled, (state, action) => {
return {
...state,
loginsIdData: action.payload
};
})
}
})
export const { deleteLoginId, editLoginId } = loginsIdSlice.actions;
export default loginsIdSlice.reducer;
activitiesSlice
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import * as api from "../../api"
import { editLoginIdData } from "../loginsId/loginsIdSlice"
const initialState = {
activitiesData: [],
}
const activitiesSlice = createSlice({
name: 'activities',
initialState: initialState,
extraReducers: (builder) => {
builder.
addCase(editLoginIdData.fullfilled, (state, action) => {
console.log("ss")
return {
...state,
activitiesData: []
};
})
}
})
export default activitiesSlice.reducer;
Is there a way that when editLoginIdData action is fullfilled we can
dispatch somehow mutate the state in other slice.

How to use firebase authentication with Redux Toolkit using onAuthStateChanged?

I'm trying to implement Firebase Authentication via Redux Toolkit. But I think I'm missing something due to lack of knowledge.
My monitorAuthChange returns undefined.
I have two separate files - first list of firebase functions, second Redux Toolkit slice.
import {
createUserWithEmailAndPassword,
onAuthStateChanged,
} from "firebase/auth";
import { auth } from "./firebaseConfig";
export const createAccount = async (email, password) => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (error) {
console.log(error);
}
};
export const monitorAuthChange = () => {
onAuthStateChanged(auth, (user) => {
if (user) {
return true;
} else {
return false;
}
});
};
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { createAccount, monitorAuthChange } from "../../service/userServices";
export const createUser = createAsyncThunk(
"users/createUser",
async ({ username, password }) => {
await createAccount(username, password);
}
);
const initialState = { loginStatus: false };
const userSlice = createSlice({
name: "users",
initialState,
reducers: {},
extraReducers: {
[createUser.fulfilled]: (state, action) => {
const result = monitorAuthChange();
state.loginStatus = result;
},
[createUser.rejected]: (state, action) => {
state.loginStatus = false;
},
},
});
export const selectAllUsers = (state) => state.users;
export default userSlice.reducer;
Two things make me confused:
Thunk works - it creates account and I see it in Firebase. Do I need to track result of request in a different way?
If add console.log(user) inside monitorAuthChange it logs data depends if user was created or not. But still returns undefined.
Would appreciate any hint or advice or article to read to understand my mistake. Thanks in advance.
It seems you want to track user auth with onAuthStateChanged
You have plenty way to plug this callback to redux.
You cannot call monitorAuthChange inside the reducer as they must be pure.
Using global store
// users.slice.ts
const userSlice = createSlice({
name: "users",
initialState,
reducers: {
setLoginStatus: (state, action) {
state.loginStatus = action.payload;
}
},
extraReducers: {
[createUser.fulfilled]: (state, action) => {
state.loginStatus = true;
},
[createUser.rejected]: (state, action) => {
state.loginStatus = false;
},
},
});
// trackUserAuth.ts
onAuthStateChanged(auth, (user) => {
if (user) {
store.dispatch(setLoginStatus(true))
} else {
store.dispatch(setLoginStatus(true))
}
});
Using hooks
export const useAuth = () => {
const dispatch = useDispatch();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
dispatch(setLoginStatus(true))
} else {
dispatch(setLoginStatus(true))
}
});
return unsubscribe;
}, []);
}
Using thunks
export const checkAuthStatus = () => (dispatch) {
const unsubscribe = Firebase.auth().onAuthStateChanged(user => {
if (user) {
dispatch(setLoginStatus(true))
} else {
dispatch(setLoginStatus(true))
}
});
return unsubscribe;
}

react toastify with redux from axios API

i am trying to send the error messages that sent from my server ( express ) to axios and the error message displays in toastify component but the error message doesn't show up here is the login axios function with the toastify how can i display toastify message inside my page from redux ?
here is my code :
// redux controller
const login = async (username, password) => {
await axios.post("/login",{username,password,},
{ withCredentials: true });};
// reducer page
export function generateError(prop) {
return function (dispatch) {
dispatch({
type: "USER_FAIL"
});
toast.error(prop);
};
}
export function generateSuccess(prop) {
return function (dispatch) {
dispatch({
type: "USER_SUCCESS"
});
toast.success(prop);
};
}
export const login = createAsyncThunk(
"/login",
async ({ username, password }) => {
try {
const data = await authService.login(username, password);
if (data) {
if (data.errors) {
const { username, password } = data.errors;
if (username) generateError(username)
else if (password) generateError(password);
} else {
generateSuccess(data.success);
}
}
return { user: data };
} catch (error) {
console.log(error);
}
}
);
// login page
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ username, password }));
}
i am using react-tostify and #redux-toolkit but the message doesn't display inside my page
i fixed it and here is my code :
// auth.js ( redux page )
export const login = createAsyncThunk(
"/login",
async ({ username, password }) => {
try {
const {data} = await axios.post(
"/login",
{
username,
password,
},
{ withCredentials: true }
);
return { user: data };
} catch (error) {
console.log(error);
}
});
const initialState = user
? { isLoggedIn: true, user }
: { isLoggedIn: false, user: null };
const authSlice = createSlice({
name: "auth",
initialState,
extraReducers: {
[login.fulfilled]: (state, action) => {
state.isLoggedIn = true;
state.user = action.payload.user;
},
[login.rejected]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
[logout.fulfilled]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
}})
const { reducer } = authSlice; export default reducer;
Login Page :
const { isLoggedIn } = useSelector((state) => state.auth);
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ username, password })).then(data => {
console.log(data)
if (data.payload.user) {
if (data.payload.user.errors) {
const { username, password } = data.payload.user.errors;
if (username) generateError(username)
else if (password) generateError(password);
} else {
generateSuccess(data.success);
navigate("/dashboard");
}
}
})
}
i realized when i back the data it has an object name payload i used it to get the error messages from express and then i put the message in toastify function gettingError and here it is
const generateError = error => {
toast.error(error, {
position: "bottom-right",
})
}
Hai I'm also looking for the same problem while searching I found a solution at with this : react-toastify-with-redux
my Code : authAction.js
import 'react-toastify/dist/ReactToastify.min.css';
import { toast} from 'react-toastify';
export const registerUser = (userData) => dispatch =>{
axios.post('user/register',userData)
.then(res=>toast.success('Your Account Created Successfully 👍'))
.then(res=> window.location = '/authentication/sign-in')
.catch(err=>dispatch(
{
type: GET_ERRORS,
payload: err.response.data
}
),toast.error("Error 😣"))
// .catch((err)=> {return })
};
On your signUp page just add
<ToastContainer />
That's all ...
This answer is probably late. But I came across this problem and decided to do it my way. I know there is toast. promise to handle promises and I don't want to call dispatch.then every time. So I can up with passing dispatch to my action wrapper. Here is my code.
// utils.ts
type ArgumentTypes<F extends CallableFunction> = F extends (
...args: infer A
) => any
? A[0]
: never;
export const withToast = <T = AnyAction | typeof createAsyncThunk>(
action: T,
{ pending, error, success }: ToastPromiseParams<T>
) => {
return (
dispatch: ReturnType<typeof useAppDispatch>,
actionParams?: ArgumentTypes<T & CallableFunction> | void
) => {
const promise = dispatch(
(action as CallableFunction)(actionParams as any)
).unwrap();
toast.promise(promise, {
pending,
error,
success,
});
};
};
// actions.ts
export const login = createAsyncThunk(
"user/login",
async (payload: {
email: string;
password: string;
}): Promise<Partial<LoginAPIResponse>> => {
const { data } = await axios.post(`${API}/${LOGIN_EP}/`, payload);
return data;
}
);
export const loginWithToast = withToast(login, {
pending: "Logging in...",
error: {
render: (error: any) => {
return error?.password || error?.email
? "Invalid email or password"
: "Something went wrong";
},
},
success: "Logged in successfully",
});
// usage in component
const dispatch = useAppDispatch();
loginWithToast(dispatch, {
email: values.email.value,
password: values.password.value,
});
First createAsyncThunk:
import { coreAxios } from "utilities/axios"; // Own customized axios
import { createAsyncThunk } from "#reduxjs/toolkit";
const BASE_URL = process.env.REACT_APP_MAIN_URL
export const GetProducts = createAsyncThunk(
"inventory/GetProducts",
async () => {
const {data} = await coreAxios.get(`${BASE_URL}/api/product/list/`);
return data
}
);
Second createSlice:
import { createSlice } from "#reduxjs/toolkit";
import { GetProducts } from "services/inventory/product.service";
import { toast } from 'react-toastify';
export const productSlice = createSlice({
name: "products",
initialState: {
productsList: [],
productsLoading: false,
productsError: null,
},
extraReducers:
(builder) => {
builder.addCase(GetProducts.pending, (state) => {
toast.loading('Promise is pending...')
state.productsLoading = true
});
builder.addCase(GetProducts.fulfilled, (state, action) => {
toast.dismiss();
toast.success('Promise resolved 👌');
state.productsList = action.payload
state.productsLoading = false
state.productsError = null
});
builder.addCase(GetProducts.rejected, (state, action) => {
toast.dismiss();
toast.error('Promise rejected 🤯 😣')
state.productsLoading = false
state.productsError = action.error?.message
});
},
});
export default productSlice.reducer;
Third page:
import { ToastContainer } from 'react-toastify';
import { useSelector, useDispatch } from "react-redux";
import { GetProducts } from 'services/inventory/product.service';
const Product = () => {
const { productsList, productsLoading, productsError } = useSelector((state) => state.products);
const dispatch = useDispatch();
useEffect(() => {
dispatch(GetProducts());
}, []);
return (
<div className="grid crud-demo">
<h1>Hello Alim</h1>
<ToastContainer />
</div>
);
}

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.

Resources