Reputation: 1015
I am learning Redux Thunk now. I tried to use createAsyncThunk
from Redux Toolkit
to deal with user log in and I encountered some problems. I created a demo here ('[email protected]' + whatever password => success, other combination => rejection).
I created a modal for users to input their emails and passwords using reactstrap
. Click the Login
button then you will see the form.
Here is my UserSlice.js
file:
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { loginFeedback } from "./feedBack";
export const login = createAsyncThunk(
"user/login",
async (userInfo, { rejectWithValue }) => {
try {
const response = await loginFeedback(userInfo);
return response.data;
} catch (err) {
return rejectWithValue(err);
}
}
);
const initialState = {
isAuthenticated: false,
isLoading: false,
user: null,
error: null,
};
const userSlice = createSlice({
name: "users",
initialState,
reducers: {},
extraReducers: {
[login.pending]: (state, action) => {
state.isLoading = true;
state.isAuthenticated = false;
},
[login.fulfilled]: (state, action) => {
state.isLoading = false;
state.isAuthenticated = true;
state.user = action.payload;
},
[login.rejected]: (state, action) => {
state.isLoading = false;
state.isAuthenticated = false;
state.user = [];
state.error = action.payload.message;
},
},
});
export default userSlice.reducer;
So in my LoginModal.js
file, when the Login button is clicked, it will fire up the form submit function handleSubmit()
:
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ email, password }))
// something else....
}
So in UserSlice.js
, login
function will take care of the async call to fetch data since I used createAsyncThunk
. The createAsyncThunk
will create three actions: pending
, fulfilled
, and rejected
. and I defined the actions accordingly in the extraReducers
in my userSlice
.
// userSlice.js
[login.pending]: (state, action) => {
state.isLoading = true;
state.isAuthenticated = false;
},
[login.fulfilled]: (state, action) => {
state.isLoading = false;
state.isAuthenticated = true;
state.user = action.payload;
},
[login.rejected]: (state, action) => {
state.isLoading = false;
state.isAuthenticated = false;
state.user = [];
state.error = action.payload.message;
},
So if the loginFeedback
succeeded, the isAuthenticated
state should be set as true
, and if the call is rejected it will be set as false
and we will have an error message show up in the form.
In my LoginModal.js
, I want to close the modal if the user authentication is succeeded, and if fails, then shows the error message. To treat the action like normal promise contents, I found this in Redux Toolkits (here):
The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an unwrapResult function that can be used to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action.
So I wrote my handleSubmit
function as:
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ email, password }))
.then(unwrapResult)
.then(() => {
if (isAuthenticated && modal) {
setEmail("");
setPassword("");
toggle();
}
})
.catch((error) => {
setHasError(true);
});
};
We first unwrapResult
, then if the promise returns succeeded plus the state isAuthenticated
and the modal
are true then we toggle
(close) the modal, otherwise we log the error.
However, the modal does not close even if I see the user/login/fulfilled
action executes by the Redux DevTool, and the isAuthenticated
state is set as true
.
In my understanding, the isAuthenticated
should already set to be true
when the login
thunk finishes, so we have everything ready when we call the .then()
method. Is it because I get the isAutheticated
state by useSelector
, so it requires some time to update itself? So we cannot guarantee React Redux already updated it when the promise return? Is there a better way to close the modal when succeeded?
I appreciate your help! You can find the demo here. ('[email protected]' + whatever password => success, other combination => rejection)
Upvotes: 2
Views: 7170
Reputation: 8308
I think the return value from the Promise
inside the then
callback does signify that the login operation is successful. But that doesn't mean you will get isAuthenticated
as true
because your handleSubmit
would have been closing over the previous value of isAuthenticated
which was false
.
You would require to have a custom useEffect
which triggers on isAuthenticated
and other values that your logic requires.
The following changes should satisfy what you need :-
const toggle = useCallback(() => {
setHasError(false);
setModal((modal) => !modal);
}, []);
useEffect(() => {
if (isAuthenticated && modal) {
setEmail("");
setPassword("");
toggle();
}
}, [isAuthenticated, modal, toggle]);
const handleSubmit = (e) => {
e.preventDefault();
dispatch(login({ email, password }))
.then(unwrapResult)
.catch(() => setHasError(true));
};
Here is the codesandbox :-
Upvotes: 4