Search code examples
javascriptreactjsreduxredux-thunk

React cannot use the updated Redux state after having dispatched an action


I'm relatively new to React and Redux and learning them through my personal project.

The issue here is that isAuthed cannot use the updated Redux state after rest.dispatch(actions.isValidUser(json)) is executed. As far as I know, the Redux state is updated by the action. (But I don't see connect() is called after the update...I don't know if this is associated with this problem.)

Also I tried using Redux-thunk in my action file to fetch data from an API endpoint and using useEffect(), but it didn't solve the issue. Could you please help me out?

Thank you in advance.

**ProtedtedRoute.jsx**

import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import * as actions from '../actions/actions';


function ProtectedRoute({ component: Component, isAuthed, ...rest }) {
  
  async function verifyUser() {
     // checking if a user is valid by checking JWT
      const res = await fetch(ENDPOINT, reqOptions);
    if (res.status === 200) {
      const json = await res.json();
      rest.dispatch(actions.isValidUser(json));
    } else {
      // failure handling
    };
  };

  verifyUser();

  return (
    <Route
      {...rest}
      render={(props) => isAuthed == true ? <Component {...props} /> : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}
    />
  );
};


export default connect(state => {
  return {
    isAuthed: state.isAuthenticated
  }
})(ProtectedRoute);

**reducer.js**
const initState = {
    data: {},
    // when a user is valid, it will be ```true```
    isAuthenticated: false
}


**App.js**

function App() {

  return (
    <Provider store={store}>
        <BrowserRouter>
          <div>
            <div className="content">
              <Switch>
                <Route exact path="/" component={Home} />
                <PublicRoute path="/login" component={LogIn} />
                <PublicRoute path="/signup" component={SignUp} />
                <ProtectedRoute path="/dashboard" component={Dashboard} />
              </Switch>
              ...


**Login.jsx**

 const res = await fetch(ENDPOINT, { reqOptions});
        if (res.status === 200) {
            props.history.push('/dashboard');
        else{
            // error handling
        }

Solution

  • You don't want a function call like verifyUser(); just floating in the component. It needs to be inside a useEffect hook.

    Your Login component fetches the endpoint before you redirect to Dashboard, so you should not need to fetch the endpoint again in order to access the Dashboard through PrivateRoute.

    You can change your initialState to include isAuthenticated: undefined as in "we don't know if they are authenticate or not because we haven't checked yet."

    Then in PrivateRoute, we only need to call verifyUser if the value of isAuthed is undefined meaning that we haven't checked yet. If it's true or false we just use that existing value.

    We still have a bit of a problem with the aysnc flow because we don't want to to Redirect off of the PrivateRoute before verifyUser has finished. For that, we can conditionally render a loading state that shows while awaiting credentials.

    I don't know that this is the most elegant solution but it should work

    function ProtectedRoute({ component: Component, isAuthed, ...rest }) {
    
      async function verifyUser() {
        // checking if a user is valid by checking JWT
        const res = await fetch(ENDPOINT, reqOptions);
        if (res.status === 200) {
          const json = await res.json();
          rest.dispatch(actions.isValidUser(json));
        } else {
          // failure handling
        }
      }
    
      useEffect(() => {
        if (isAuthed === undefined) {
          verifyUser();
        }
      }, [isAuthed]); //re-run when isAuthed changes
    
      return (
        <Route
          {...rest}
          render={(props) =>
            isAuthed === undefined ? (
              <Loading />
            ) : isAuthed === true ? (
              <Component {...props} />
            ) : (
              <Redirect
                to={{ pathname: "/login", state: { from: props.location } }}
              />
            )
          }
        />
      );
    }