Redirect page upon login using react-router-v5 and redux-toolkit

Viewed 7301

I'm using react-router-dom v5.2.

Upon login, I want my page to redirect to /home from /. The login form is at /.

When I try to perform authentication without any async function (ie. comparing username and password to hardcoded values in react), everything works perfectly.

But when I do perform authentication using express and mongo, the redirection upon login stops working. If I login again, then the redirection happens. Protected routes still work (redirect to login page if the user is not logged in).

Here's a small demo of the problem where I use do auth with express + mongo ie. async redux. This is not working as intended. https://youtu.be/Zxm5GOYymZQ

Here's the link of the app where I use hardcoded username and password (both "test") to do auth. No async here. This works as intended. Username and password are both "test". https://poke-zoo.herokuapp.com/


Here's App.js:

const ProtectedRoute = ({ component: Component, ...rest }) => {
  const authState = useSelector(selectorAuth)
  // const location = useLocation()
  return (
    <Route
      {...rest}
      render={props => {
        if (authState.isUserLoggedIn) {
          return <Component {...props} />
        } else {
          return (
            <Redirect
              to={{
                pathname: "/",
                state: {
                  from: props.location,
                },
              }}
            />
          )
        }
      }}
    />
  )
}

const App = () => {
  return (
    <Router>
      <div tw="flex flex-col bg-green-100 min-h-screen">
        <Navbar />
        <Switch>
          <Route exact path="/" component={Landing} />
          <ProtectedRoute path="/home" component={Home} />
          <ProtectedRoute path="/explore" component={Explore} />
          <Route path="*" component={() => "404 Not found."} />
        </Switch>
      </div>
    </Router>
  )
}

Here's ModalLogin.js.

const ModalLogin = props => {
  const { loginModalBool, setLoginModalBool } = props
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")

  const dispatch = useDispatch()
  const history = useHistory()

  const attemptLogin = e => {
    e.preventDefault()
    dispatch(tryLogin(username, password))
    history.push("/home")
  }

  return (
    <div tw="flex flex-col text-center h-full w-64 bg-gray-200 text-gray-900 rounded-lg shadow-lg p-2 md:p-4 lg:p-6">
      <div tw="flex flex-row justify-between">
        <p tw="text-lg">Login</p>
        <button tw="text-sm" onClick={() => setLoginModalBool(!loginModalBool)}>
          close
        </button>
      </div>
      <div tw="flex flex-col justify-around my-1">
        <form onSubmit={attemptLogin} tw="">
          <input
            tw="my-1"
            value={username}
            onChange={e => setUsername(e.target.value)}
            placeholder="username"
          />
          <input
            tw="my-1"
            value={password}
            onChange={e => setPassword(e.target.value)}
            type="password"
            placeholder="password"
          />
          <button
            type="submit"
            tw="my-1 p-1 rounded bg-gray-800 text-gray-100 hover:bg-gray-900"
          >
            log in
          </button>
        </form>
      </div>
    </div>
  )
}

Here's the authSlice.js.

import { createSlice } from "@reduxjs/toolkit"
import axios from "axios"

const initialState = {
  isUserLoggedIn: false,
  username: "",
}

export const authSlice = createSlice({
  name: "auth",
  initialState: initialState,
  reducers: {
    login: (state, action) => {
      const user = action.payload

      if (!user) return alert("Login failed. Incorrect username or password.")

      state.username = user.username
      state.isUserLoggedIn = true
    },
    logout: (state, action) => {
      // window.localStorage.removeItem("loggedInUser")
      state.username = ""
      state.isUserLoggedIn = false
    },
    signup: (state, action) => {
      const user = action.payload
      state.username = user.data.username
      state.isUserLoggedIn = true
    },
  },
})

export const tryLogin = (username, password) => {
  return async dispatch => {
    try {
      const response = await axios.post("/api/auth/login", {
        username: username,
        password: password,
      })

      const user = {
        token: response.headers["auth-token"],
        username: response.data.username,
      }

      // window.localStorage.setItem("token", response.headers["auth-token"])

      dispatch(login(user))
    } catch (e) {
      alert("Incorrect Username/Password.")
    }
  }
}

export const selectorAuth = state => state.auth
export const { login, logout } = authSlice.actions
export default authSlice.reducer


Am I using react-router with redux-toolkit incorrectly?

Here's the Github repo

3 Answers

Your code does not define redirection logic after login. You can do it in two way.

1st : You can define another redirection wrapper for authentication if you want your routes to redirect in case of authentication.

const AuthRoute = ({ component: Component, ...rest }) => {
  const authState = useSelector(selectorAuth)
  const location = useLocation()
  return (
    <Route
      {...rest}
      render={props => {
        if (!authState.isUserLoggedIn) {
          return <Component {...props} />
        } else {
          return (
            <Redirect
              to={{
                pathname: "/home",
                state: {
                  from: location,
                },
              }}
            />
          )
        }
      }}
    />
  )
}

const App = () => {
  return (
    <Router>
      <div tw="flex flex-col bg-green-100 min-h-screen">
        <Navbar />
        <Switch>
          // It is for login users to redirect to home page
          <AuthRoute exact path="/" component={Landing} />
          <ProtectedRoute path="/home" component={Home} />
          <ProtectedRoute path="/explore" component={Explore} />
          <Route path="*" component={() => "404 Not found."} />
        </Switch>
      </div>
    </Router>
  )
}

2nd : Another approach can be imperatively handling with history.push() or history.replace() :

const Layout = () => {
  const authState = useSelector(selectorAuth);
  const history = useHistory();

  useEffect(() => {
    // if isUserLoggedIn turned to true redirect to /home
    if (authState.isUserLoggedIn) { 
      history.push("/home");
    }
  }, [authState.isUserLoggedIn]); // triggers when isUserLoggedIn changes

  return (
    <Switch>
      <Route exact path="/" component={Landing} />
      <ProtectedRoute path="/home" component={Home} />
      <ProtectedRoute path="/explore" component={Explore} />
      <Route path="*" component={() => "404 Not found."} />
    </Switch>
  );
};

const App = () => {
  return (
    <Router>
      <div tw="flex flex-col bg-green-100 min-h-screen">
        <Navbar />
        <Layout />
      </div>
    </Router>
  );
};

Why didn't your code work? Take a look at the code below :

      <Route exact path="/" component={Landing} />
      <ProtectedRoute path="/home" component={Home} />
      <ProtectedRoute path="/explore" component={Explore} />
      <Route path="*" component={() => "404 Not found."} />

What it does? It checks your browser path and check if it matches with given Route rule from top to down. If Route path matches then it renders the component if not it continues through down visiting each one of Route until it matches your 404.

So back to your case; when you login, you were not leaving "/" path. Because there is no logic implemented to leave the "/" path. So it again match with landing page even though it is authenticated. It matches with route path (Landing page) and stays there. It does not continue and try your logic on ProtectedRoute.

The current accepted answer fixes your issues, but does not properly identify the reason that you were having trouble. So I want to explain it for you (and anyone else reading this).

Problem: Redirecting Too Soon

const attemptLogin = e => {
  e.preventDefault()
  dispatch(tryLogin(username, password))
  history.push("/home")
}

This code dispatches the tryLogin and the immediately calls history.push("/home"). It does not wait for the dispatched action to complete and update the store.

When you call history.push("/home") your app loads the ProtectedRoute for component Home. Inside the ProtectedRoute you use a selector to examine the value of authState.isUserLoggedIn. If it is false, then you redirect to "/".

So if you redirect to "/home" before the value of authState.isUserLoggedIn has been updated then you will end up getting redirected back to "/" instead of being able to view the Home component.

That's why you are having these issues with asynchronous authentication:

When I try to perform authentication without any async function (ie. comparing username and password to hardcoded values in react), everything works perfectly.

But when I do perform authentication using express and mongo, the redirection upon login stops working.

Solution: Wait Before Redirecting

You need to wait for the authState to update before redirecting to a protected route.

There's a lot of ways to do this, but in general we want to use selectors to listen for changes being posted to the redux state. Here's one approach which uses the ModalLogin to conditionally render a Redirect component. I would also recommend including some sort of selector for login errors in case the dispatch finishes resolving but the login was unsuccessful. You would want to show an error message to the user in the modal.

const ModalLogin = props => {
  const { loginModalBool, setLoginModalBool } = props
  const [username, setUsername] = useState("")
  const [password, setPassword] = useState("")

  const dispatch = useDispatch()
  const history = useHistory()
  
  // Look at the authState from redux.  This value will automatically update.
  const authState = useSelector(selectorAuth)

  const attemptLogin = e => {
    e.preventDefault()
    dispatch(tryLogin(username, password))
    // don't redirect here anymore
  }

  // Once logged in, render the redirection
  if ( authState.isUserLoggedIn ) {
     return (
       <Redirect to="/home" />
     );
  }
  
  // Otherwise, render the modal
  return (
    /* your current code */
  )
}

to be honest, what i do is use plain old javascript to change the location when the user is logged in

window.location = "/redirect"
Related