javascriptreactjsreact-routerreact-router-dommobx-react

React Router navigates to Login instead of Home after signup


I am building a React application using react-router (v7), mobx, and mobx-react. I have a Signup page where, upon clicking the Signup button, the app should update the user state and navigate to the Home page (/). However, after clicking the Signup button, the app navigates to the Login page (/login) instead of the Home page.

Here is my setup:

App.tsx:

import {
  BrowserRouter,
  createBrowserRouter,
  Navigate,
  Route,
  RouterProvider,
  Routes
} from 'react-router'
import './App.css'
import userStore from './userStore';
import Home from './Home';
import Signup from './Signup';
import Login from './Login';
import { observer } from 'mobx-react';

const router = createBrowserRouter(
  [
    {
      path: "/",
      element: userStore.user ? <Home /> : <Navigate to='/login' />,
    },
    {
      path: "/signup",
      element: userStore.user ? <Navigate to='/' /> : <Signup />,
    },
    {
      path: "/login",
      element: userStore.user ? <Navigate to='/' /> : <Login />,
    }
  ]
);
function App() {

  return (
    <>
      <RouterProvider router={router} />
    </>
  )
}
export default observer(App);

Home.tsx:

import React from 'react'
import userStore from './userStore';
import { useNavigate } from 'react-router';
import { observer } from 'mobx-react';

const Home = () => {
    const navigate = useNavigate();
    const handleLogout = ()=>{
        userStore.setUser(null);
        navigate('/login');
    };
    return (
        <>
            <div>Home</div>
            <button onClick={()=>handleLogout()}>logout</button>
        </>
    )
}
export default observer(Home)

Signup.tsx:

import React from 'react'
import userStore from './userStore';
import { useNavigate } from 'react-router';
import { observer } from 'mobx-react';

const Signup = () => {
    const navigate = useNavigate();
    const handleSignup = ()=>{
        userStore.setUser('user');
        navigate('/');
    };
    return (
        <>
            <div>Signup page</div>
            <button onClick={()=>handleSignup()}>Signup</button>
        </>
    )
}
export default observer(Signup)

Login.tsx:

import React from 'react'

const Login = () => {
    return (
        <div>Login</div>
    )
}

export default Login

userStore.ts:

import { makeAutoObservable } from "mobx";


class UserStore{
    user:string|null = null;

    constructor(){
        makeAutoObservable(this);
        this.getUserFromLocalStorage();
    }
    
    setUser(user:string|null){
        this.user = user;
        if(user){
            localStorage.setItem('user', user);
        }else{
            localStorage.removeItem('user');
        }
    }

    private getUserFromLocalStorage(){
        const user = localStorage.getItem('user')
        if(user){
            this.user = user;
        }
    };
};

const userStore = new UserStore();
export default userStore;

When I switch to using <BrowserRouter> and <Routes> instead of createBrowserRouter, the issue is resolved, and the app correctly navigates to the Home page. Here's the working configuration:

App.tsx:

function App() {
  return (
    <>
      {/* <RouterProvider router={router} /> */}
      <BrowserRouter>
        <Routes>
          <Route
            path='/'
            element={userStore.user ? <Home /> : <Navigate to='/login' />}
          />
          <Route
            path='/signup'
            element={userStore.user ? <Navigate to='/' /> : <Signup />}
          />
          <Route
            path='/login'
            element={userStore.user ? <Navigate to='/' /> : <Login />}
          />
        </Routes>
      </BrowserRouter>
    </>
  )
}

Why does createBrowserRouter fail to redirect to the Home page even though userStore.user updates correctly, while the <BrowserRouter> and <Routes> setup works perfectly? How can I resolve this issue while continuing to use createBrowserRouter?


Solution

  • Issue

    Using the regular BrowserRouter works because when it renders it's able to access the current userStore.user value and conditionally render the correct route element component.

    The Data router you are using is created outside the ReactTree so I suspect it closes over the current userStore.user value in scope when it is created.

    Solution Suggestion

    You can re-create the router within the ReactTree so that it can re-enclose the current userStore.user value and render the appropriate route element component.

    Example:

    function App() {
      const router = useMemo(() => createBrowserRouter(
        [
          {
            path: "/",
            element: userStore.user ? <Home /> : <Navigate to='/login' />,
          },
          {
            path: "/signup",
            element: userStore.user ? <Navigate to='/' /> : <Signup />,
          },
          {
            path: "/login",
            element: userStore.user ? <Navigate to='/' /> : <Login />,
          }
        ]
      ), [userStore.user]);
    
      return <RouterProvider router={router} />;
    }
    
    export default observer(App);
    

    Ideally though your router will be a stable reference that never changes. It is more idiomatic to implement protected routes.

    Example:

    import { Navigate, Outlet } from 'react-router-dom';
    import { observer } from 'mobx-react';
    import userStore from './userStore';
    
    export const ProtectedRoute = observer(() => {
      return userStore.user ? <Outlet /> : <Navigate to='/login' />;
    });
    
    export const AnonymousRoute = observer(() => {
      return userStore.user ? <Navigate to='/' /> : <Outlet />;
    });
    

    App.js

    const router = createBrowserRouter(
      [
        {
          element: <ProtectedRoute />,
          children: [
            { path: "/", element: <Home /> },
          ],
        },
        {
          element: <AnonymousRoute />,
          children: [
            { path: "/signup", element: <Signup /> },
            { path: "/login", element: <Login /> }
          ],
        },
      ]
    );
    
    function App() {
      return <RouterProvider router={router} />;
    }
    
    export default App;
    

    or

    function App() {
      return (
        <BrowserRouter>
          <Routes>
            <Route element={<ProtectedRoute />}>
              <Route path='/' element={<Home />} />
            </Route>
            <Route element={<AnonymousRoute />}>
              <Route path='/signup' element={<Signup />} />
              <Route path='/login' element={<Login />} />
            </Route>
          </Routes>
        </BrowserRouter>
      );
    }