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
?
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.
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>
);
}