React Router doesn't load the new page component after redirecting via navigate

Viewed 5979

I am new to React Hooks and tried to play around with them + the experimental React Router version 6 library, so I don't have to upgrade my code from version 5 to 6 at some point in the future

Note: version 6 is not comptaible with version 5, so no Switch is not the answer ;)

Idea: Create a simple admin application where you can login/logout via button click. The admin page is protected via access lock, so no one can access it via direct URL.

Problem: After clicking the login button, the URL is changed to /admin, but I don't see the admin page (Only after a page refresh or an enforced page refresh via windows.location.reload()).

Is this a problem in React Router v6 or am I using the API the wrong way? Source code (https://codesandbox.io/s/quirky-black-rsdy0):

import React, {useEffect, useState} from "react";
import {BrowserRouter, Route, Routes, useNavigate} from "react-router-dom";

async function isLoggedIn() {
    // In real life this function is calling a REST endpoint and awaiting the result
    return localStorage.getItem("loggedin") !== null;
}

async function setLoginStatus(status) {
    // In real life this function is calling a REST endpoint and awaiting the result
    if (status === true) {
        localStorage.setItem("loggedin", true);
    } else {
        localStorage.removeItem("loggedin");
    }
}

function LoginPage() {
    const navigate = useNavigate();

    async function handleLogin(event) {
        event.preventDefault();
        await setLoginStatus(true);
        navigate("/admin"); // --> The user is logged in and the URL changes to /admin, but he doesn't see the admin page (Only after a page refresh)
        //window.location.reload(); // Hack to reload the page to see the admin page
    }

    return (
        <form onSubmit={handleLogin}>
            <button type="submit">Login</button>
        </form>
    );
}

function LogoutPage() {
    const navigate = useNavigate();

    useEffect(() => {
        const logoutAccount = async () => {
            await setLoginStatus(false);
            navigate("/login");
        };
        logoutAccount();
    });

    return <p>Logging out</p>;
}

function AdminPage() {
    return (
        <div>
            <p>The secured admin page</p>
            <a href="/logout">Logout</a>
        </div>
    );
}

function AuthenticationLock(props) {
    if (props.isLoggedIn === true) {
        return <div>{props.children}</div>;
    } else {
        return <LoginPage/>;
    }
}

export default function App() {

    const [loginStatus, setAppLoginStatus] = useState(false);

    useEffect(() => {
        const login = async () => {
            const value = await isLoggedIn();
            setAppLoginStatus(value);
        };
        login();
    });

    return (
        <BrowserRouter>
            <AuthenticationLock isLoggedIn={loginStatus}>
                <Routes>
                    <Route path="/" element={<AdminPage/>}/>
                    <Route path="/admin" element={<AdminPage/>}/>
                    <Route path="/login" element={<LoginPage/>}/>
                    <Route path="/logout" element={<LogoutPage/>}/>
                    <Route path="/*" element={<AdminPage/>}/>
                </Routes>
            </AuthenticationLock>
        </BrowserRouter>
    );
}
3 Answers

Somehow my AuthenticationLock was repsonsible for the problem. I changed to a different design and now it works:

import React, { useEffect } from "react";
import {
  BrowserRouter,
  Route,
  Routes,
  Link,
  useNavigate
} from "react-router-dom";

function isLoggedIn() {
  // In real life this function is calling a REST endpoint and awaiting the result
  return localStorage.getItem("loggedin") !== null;
}

async function setLoginStatus(status) {
  // In real life this function is calling a REST endpoint and awaiting the result
  if (status) {
    localStorage.setItem("loggedin", "1");
  } else {
    localStorage.removeItem("loggedin");
  }
}

function DashboardPage() {
  return (
    <div>
      <h1>Dashboard page</h1>
      <p>This is the dashboard page</p>
      <Link to={"/accounts"}>Accounts</Link>
      <br />
      <Link to={"/logout"}>Logout</Link>
    </div>
  );
}

function AccountsPage() {
  return (
    <div>
      <h1>Accounts page</h1>
      <p>This is the accounts page</p>
      <Link to={"/dashboard"}>Dashboard</Link>
      <br />
      <Link to={"/logout"}>Logout</Link>
    </div>
  );
}

function LoginPage() {
  const navigate = useNavigate();

  async function handleLogin(event) {
    event.preventDefault();
    await setLoginStatus(true);
    navigate("/dashboard");
  }

  return (
    <div>
      <h1>Login</h1>
      <form onSubmit={handleLogin}>
        <button type="submit">Login</button>
      </form>
    </div>
  );
}

function LogoutPage() {
  const navigate = useNavigate();

  useEffect(() => {
    const logoutAccount = async () => {
      await setLoginStatus(false);
      navigate("/login");
    };
    logoutAccount();
  });

  return <p>Logging out</p>;
}

const PrivateRoute = ({ component, ...rest }) => {
  if (isLoggedIn()) {
    const routeComponent = props => React.createElement(component, props);
    return <Route {...rest} render={routeComponent} />;
  } else {
    return <LoginPage />;
  }
};

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <PrivateRoute path="/" element={<DashboardPage />} />
        <PrivateRoute path="/dashboard" element={<DashboardPage />} />
        <PrivateRoute path="/accounts" element={<AccountsPage />} />
        <Route path="/login" element={<LoginPage />} />
        <PrivateRoute path="/logout" element={<LogoutPage />} />
        <PrivateRoute path="/*" element={<DashboardPage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Wrapp all your routes around <Switch></Switch> and set the prop exact={true} on all routes except that last one.

import {BrowserRouter, Route, Routes, Switch, useNavigate} from "react-router-dom";

[...]

    return (
        <BrowserRouter>
            <AuthenticationLock isLoggedIn={loginStatus}>
                <Routes>
                    <Switch>
                        <Route path="/" exact={true} element={<AdminPage/>}/>
                        <Route path="/admin" exact={true} element={<AdminPage/>}/>
                        <Route path="/login" exact={true} element={<LoginPage/>}/>
                        <Route path="/logout" exact={true} element={<LogoutPage/>}/>
                        <Route path="/*" element={<AdminPage/>}/>
                    </Switch>
                </Routes>
            </AuthenticationLock>
        </BrowserRouter>
    );

So in this case we would need to use the <Switch> </Switch> component, where it needs to wrap all <Route /> components so it will render one route at a time.

Always for all components we need to set the exact bool so it won't mistaken route='/' with route='/about' which both have the / character or if we have a route like this: /about and /about/blabla. So the switch will know what <Route /> to render.

And if you want to have a <NotFoundPage /> component for a link that doesn't exist. You only assign the component prop to the <Route /> componenent e.g:

<Switch>
  <Route exact path='/' />
  <Route exact path='/about' />
  <Route component={NotFoundPage} />
</Switch>
Related