React-redux state change not reflected immediatelly, but after modifying the script only

Hi fellow programmers,

I’m working on the second to last project I believe in the full-stack developer path, which is the e-commerse webshop using the PERN stack. I encountered an issue that prevents me from finishing off this project. It relates to how react-redux store’s state gets updated by the createAsyncThunk utility. In short after a successful login once the browser is refreshed a call is being made to the db that should authenticate the user to the protected paths of the app by, when the user’s cookie was stored in the browsers localstorage. I’m always able to retrieve the user object from the database on subsequent request after a login, but the change in the isAuthenticated variable of the redux store becomes updated to true only, when I adjust the script, let’s by adding a coma to the end of a statement, and have the App component re-render. Then it updates successfully(while app is running). Can’t seem to figure out why the change isnt reflected after regular re-render without modifying something in the script. Adding the relevant parts of the app here:
Many thanks to everybody willing to assist in advance.

/////////////User slice configuration
export const checkIfLoggedIn = createAsyncThunk(
    "auth/verifyLogin",
    async(params, thunkAPI) => {
        try {
            // This call will hit the correct endpoint, that deserializes user and makes request to the database accordingly.
            // This works properly
            const result = await verifyLogin();
            
            if(!result) {
                throw new Error();
            }

            // If result returned something then return this promise
            return {
                user: result,
                cart: {},
                isAuthenticated: true
            };

        } catch(e) {
            console.log(e.message);
            throw e
        }
    }
)

// This runs on Login
export const loginUser = createAsyncThunk(
    "auth/loginUser",
    async(params, thunkAPI) => {
        try {
            // This runs on user login
            const result = await userAuth(params);

            if(!result) {
                throw new Error();
            }

            return {
                user: result,
                cart: {},
                isAuthenticated: true
            }

        } catch(e) {
            console.log(e.message);
            throw e
        }
    }
)

const usersSlice = createSlice({
    name: "users",
    initialState: {
        isAuthenticated: false,
        isFetching: false,
        loadError: false
    },
    reducers: {
        // loadUser: (state, action) => {
        //     Object.assign(state, action.payload)
        //     console.log(state.user)
        // }, 

        // toggleIsAuthenticated: (state, action) => {
        //     state.isAuthenticated = action.payload;
        //     state.isFetching = false;
        //     state.loadError = false;
        // },

        // toggleIsFetching: (state, action) => {
        //     state.isFetching = true
        // },

        // toggleLoadError: (state, action) => {
        //     state.loadError = action.payload
        // }
    },
    extraReducers: (builder) => {
        builder
        .addCase(checkIfLoggedIn.fulfilled, (state, action) => {
            
            const { isAuthenticated, user } = action.payload;
            
            state.isAuthenticated = isAuthenticated;
            state.loadError = false;
            
            
        })
        .addCase(checkIfLoggedIn.rejected, (state, action)  => {
            state.loadError = true;
            state.isAuthenticated = false;
        })
        .addCase(loginUser.fulfilled, (state, action) => {
            const { isAuthenticated, user } = action.payload;
            state.isAuthenticated = isAuthenticated;
            state.loadError = false;
            
            
        })
        .addCase(loginUser.rejected, (state, action) => {
            state.loadError = true;
            state.isAuthenticated = false;
        })
    }

});

//////////////// Store configuration
export default configureStore({
    reducer: combineReducers({
        users: usersReducer
    })
});

/////////////////////App component

function App() {

  const dispatch = useDispatch();
  const { isAuthenticated, user } = useSelector(state => state.users);
  const [forceRender, setForceRender] = useState(false);
  console.log("App mounts");

  // Calling useEffect should update the redux store's isAuthenticated value to true 
  // on every App component re-render after user is logged in.
  useEffect(() => {
    
    async function isLoggedin() {
      try { 
        
        
        await dispatch(checkIfLoggedIn())

        // Update not reflected in the redux store's state isAuthenticated value, unless I edit the script with let's say a
        // a minor adjustment that dont changes the logic. In this case the useEffect runs the api call once again and the state
        // is updated successfully, but when I only hit let's say ctrl + F5 then the query is made, data is retrieved but the update on the
        // isAuthenticated variable is not reflected.

        setTimeout(() => {
          // Change is not reflected isAuthenticated is false, unless I modify the App.js component by a minor adjustment, then the change
          // in the store's state becomes reflected immediatelly.
          console.log("App component "+ isAuthenticated)
        
        }, 2000)
      } catch(e) {
        console.log("Error");
        console.log(e.message);
      } 
    }

    isLoggedin()
    
  }, []);

  return (
    <BrowserRouter>
      <Routes>
        {/*Add routes here */}
        <Route path='/' element={ <ProtectedRoute /> }>
          <Route path='/' element={ <Home /> }></Route>
        </Route>
        {/*Here's the login path: */}
        <Route path='login' element={ <Login /> }></Route>
      </Routes>
    </BrowserRouter>

  )
};

///////////////Relevant part of Login component(child component of App)
function Login() {

    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const [isLoading, setIsLoading] = useState(false);
    const { isAuthenticated, loadError, user } = useSelector(state => state.users);
    const dispatch = useDispatch();
    const navigate = useNavigate();

    // handleSubmit is triggered when form is submitted
    const handleSubmit = async(e) => {
        try {
            e.preventDefault();
            // This should update the redux store's state to isAuthenticated = true. This works correctly.
            setIsLoading(true);

            await dispatch(loginUser({ username: email, password: password }));

            setIsLoading(false);

            if(isAuthenticated) {
                // isAuthenticated is true here, so redirection works
                alert(`user is authenticated`);
                navigate("/")
            
            }

        } catch(e) {
            console.log(e.message);
            setIsLoading(false);
        }
        
    }

The effect hook has an empty dependency array – so it runs only once, immediately after launch. It does the check, that will return false, and that’s it.
Why do you need the effect hook in the first place? What is it supposed to do that does not already happen in the event handler?
If it should perform any extra action, it should suffice to add the isAuthenticated state to the dependency array.
And if you need to repeat the check b/c the isAuthenticated isn’t updated accordingly on login, you probably need to run an interval inside the effect hook, that repeats dispatching the check function. But that would be a little hacky.

Thanks for your answer. So the purpose of the useEffect hook is to authenticate the user based on the cookie value that was granted after the first successful login. It makes the session persist, so when you refresh the browser or visit the site again you dont have authenticate to it every time. Your suggestion makes it better, because after the first attempt an additional api call is made to the database, after which the update is reflected. But this does not explain why the first one fails after a successful login. For example if you have the following route protection logic, then you will never be able to get to the protected routes, since when the useEffect runs for the first time, the change is not immediatelly reflected and isAuthenticated is false. Therefore you always going to be redirected. But it makes things better a bit:

return isAuthenticated ? <Outlet /> : <Navigate to="/login"/> 

But doesn’t that already happen in the event handler? Both actions do the same thing: update isAuthenticated.

Do you really set a cookie? isAuthenticated is a state which will always have the initial value ‘false’ after reload.

Your effect hook is located in the App component. The app component is the parent of your routes, so the App component does not rerender on location change. Therefore the effect hook with an empty dependency array runs only once after the App is mounted.

Sorry for the late response, but I was searching for a workaround that I ultimately found, the whole week. If I pass in the isAuthenticated variable as the dependency array, as you mentioned earlier, it is a viable solution. However, I wanted to limit the API calls to my database, therefore I came up with the following solution, since redux doesn’t seem to have the component re-render, the change in the store values are not reflected immediatelly. It takes another re-render of the component, that you can achieve by local state values and the useState hooks. This implementation works as intended. Thanks for your support once agains!

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import logo from './logo.svg';
import './App.css';
// import custom components
  import Login from './login/Login';
  import Header from './header/Header';
  import Cart from './cart/Cart';
  import Home from './home/Home';
  import ProductDetails from './productDetails/productDetails';
  import Register from './register/Register';
  import Profile from './profile/Profile';
//
import { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { checkIfLoggedIn } from './store/user/slice';
import { useSelector } from 'react-redux';

function App() {

  const dispatch = useDispatch();
  const { isAuthenticated, user, loadError } = useSelector(state => state.users);
  const [localUser, setLocalUser] = useState({});
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [key, setKey] = useState(0);
  
  const forceRerender = () => {
    setKey(prevKey => prevKey + 1);
  };

  useEffect(() => {
    

    setIsLoggedIn(isAuthenticated);
    setLocalUser(user);

    // console.log("isLoggedIn in App is " + isLoggedIn);
    // console.log("localUser email in App is :" + localUser.email);
    

  }, [dispatch, isAuthenticated, user, localUser, isLoggedIn]);

  

  useEffect(() => {
    
    async function isLoggedin() {
      try { 
        
        
        await dispatch(checkIfLoggedIn())
        
        forceRerender();
        
      } catch(e) {
        console.log("Error");
        console.log(e.message);
      } 
    }

    isLoggedin()
    
  }, []);

  return (
    <BrowserRouter>
      <Routes>
          <Route path='/' element={<Header isLoggedIn={isLoggedIn}/>}> 
            <Route path='' element={ <Home /> }></Route>
            <Route path='register' element={ <Register /> }></Route>
            <Route path='login' element={ <Login isLoggedIn={isLoggedIn} /> }></Route>
            <Route path='cart' element={<Cart isLoggedIn={isLoggedIn} />} ></Route>
            <Route path='profile' element={<Profile isLoggedIn={isLoggedIn} localUser={localUser} />}></Route>
            <Route path='products/:productId' element={ <ProductDetails /> }></Route>
          </Route>
      </Routes>
    </BrowserRouter>

  )
};

export default App;


1 Like