Search code examples
javascriptreactjsreduxreact-reduxredux-toolkit

Redux Toolkit createAsyncThunk question: state updates after async dispatch call?


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.

enter image description here

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)


Solution

  • 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 :-

    Edit react-modal (forked) - stackoverflow