You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Router v6.10中createBrowserRouter登录跳转首次失效问题

React Router v6 使用createBrowserRouter后登录跳转失效问题

我基于React Router官方Auth示例开发带受保护路由的应用,按照文档推荐用createBrowserRouter替代原示例的BrowserRouter后,出现登录异常:调用auth.signin()已成功存储用户信息,但LoginPage组件中的navigate()首次执行无法跳转到/protected页面,需第二次点击登录才生效。

改造后代码

main.tsx

import './index.css';

import React from 'react';

import ReactDOM from 'react-dom/client';

import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

App.tsx

import * as React from 'react';

import {
    createBrowserRouter,
    Link,
    Navigate,
    Outlet,
    RouterProvider,
    useLocation,
    useNavigate,
} from 'react-router-dom';

import { fakeAuthProvider } from './auth';

const router = createBrowserRouter([
    {
        element: <Layout />,
        children: [
            {
                path: '/',
                element: <PublicPage />,
            },
            {
                path: '/login',
                element: <LoginPage />,
            },
            {
                path: '/protected',
                element: (
                    <RequireAuth>
                        <ProtectedPage />
                    </RequireAuth>
                ),
            },
        ],
    },
]);

export default function App() {
    return (
        <AuthProvider>
            <h1>Auth Example</h1>

            <p>
                This example demonstrates a simple login flow with three pages: a public page, a protected page, and a
                login page. In order to see the protected page, you must first login. Pretty standard stuff.
            </p>

            <p>
                First, visit the public page. Then, visit the protected page. You're not yet logged in, so you are
                redirected to the login page. After you login, you are redirected back to the protected page.
            </p>

            <p>
                Notice the URL change each time. If you click the back button at this point, would you expect to go back
                to the login page? No! You're already logged in. Try it out, and you'll see you go back to the page you
                visited just *before* logging in, the public page.
            </p>

            <RouterProvider router={router} />
        </AuthProvider>
    );
}

function Layout() {
    return (
        <div>
            <AuthStatus />

            <ul>
                <li>
                    <Link to="/">Public Page</Link>
                </li>
                <li>
                    <Link to="/protected">Protected Page</Link>
                </li>
            </ul>

            <Outlet />
        </div>
    );
}

interface AuthContextType {
    user: any;
    signin: (user: string, callback: VoidFunction) => void;
    signout: (callback: VoidFunction) => void;
}

let AuthContext = React.createContext<AuthContextType>(null!);

function AuthProvider({ children }: { children: React.ReactNode }) {
    let [user, setUser] = React.useState<any>(null);

    let signin = (newUser: string, callback: VoidFunction) => {
        return fakeAuthProvider.signin(() => {
            setUser(newUser);
            callback();
        });
    };

    let signout = (callback: VoidFunction) => {
        return fakeAuthProvider.signout(() => {
            setUser(null);
            callback();
        });
    };

    let value = { user, signin, signout };

    return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

function useAuth() {
    return React.useContext(AuthContext);
}

function AuthStatus() {
    let auth = useAuth();
    let navigate = useNavigate();

    if (!auth.user) {
        return <p>You are not logged in.</p>;
    }

    return (
        <p>
            Welcome {auth.user}!{' '}
            <button
                onClick={() => {
                    auth.signout(() => navigate('/'));
                }}
            >
                Sign out
            </button>
        </p>
    );
}

function RequireAuth({ children }: { children: JSX.Element }) {
    let auth = useAuth();
    let location = useLocation();

    if (!auth.user) {
        return <Navigate to="/login" state={{ from: location }} replace />;
    }

    return children;
}

function LoginPage() {
    let navigate = useNavigate();
    let location = useLocation();
    let auth = useAuth();

    let from = location.state?.from?.pathname || '/';

    function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
        event.preventDefault();

        let formData = new FormData(event.currentTarget);
        let username = formData.get('username') as string;

        auth.signin(username, () => {
            navigate(from, { replace: true });
        });
    }

    return (
        <div>
            <p>You must log in to view the page at {from}</p>

            <form onSubmit={handleSubmit}>
                <label>
                    Username: <input name="username" type="text" />
                </label>{' '}
                <button type="submit">Login</button>
            </form>
        </div>
    );
}

function PublicPage() {
    return <h3>Public</h3>;
}

function ProtectedPage() {
    return <h3>Protected</h3>;
}

问题原因

核心问题是路由实例的创建时机早于AuthContext的初始化:你在App组件外部提前创建了router对象,此时AuthProvider还未向组件树注入用户状态上下文,导致RequireAuth组件在路由初始化时获取的auth.user始终为null。

第一次登录后执行navigate(from),路由跳转到/protected时,RequireAuth会读取缓存的旧状态(用户未登录),于是再次重定向回/login页面;第二次点击登录时,AuthContext的用户状态已经完成更新,RequireAuth能正确识别用户已登录状态,才会允许跳转到受保护页面。

解决方案

方法一:将路由实例移到AuthProvider内部创建(推荐)

把router的创建逻辑放到被AuthProvider包裹的组件中,确保路由组件能正确访问到最新的AuthContext状态:

修改App.tsx,新增RouterWrapper组件并将路由创建逻辑移入:

export default function App() {
    return (
        <AuthProvider>
            <h1>Auth Example</h1>

            <p>
                这个示例展示了简单的登录流程:公共页面、受保护页面和登录页。要查看受保护页面必须先登录,逻辑很常规。
            </p>

            <p>
                先访问公共页面,再尝试访问受保护页面,因为未登录会被重定向到登录页。登录后会自动跳回受保护页面。
            </p>

            <p>
                注意每次的URL变化。如果此时点击返回按钮,你不会回到登录页——因为已经登录了,会回到登录前访问的公共页面。
            </p>

            {/* 用Wrapper包裹RouterProvider */}
            <RouterWrapper />
        </AuthProvider>
    );
}

// 新增RouterWrapper组件
function RouterWrapper() {
    // 用useMemo缓存路由实例,避免重复创建
    const router = React.useMemo(() => 
        createBrowserRouter([
            {
                element: <Layout />,
                children: [
                    { path: '/', element: <PublicPage /> },
                    { path: '/login', element: <LoginPage /> },
                    {
                        path: '/protected',
                        element: (
                            <RequireAuth>
                                <ProtectedPage />
                            </RequireAuth>
                        ),
                    },
                ],
            },
        ]), []);

    return <RouterProvider router={router} />;
}

方法二:在RequireAuth中监听用户状态变化(临时修复)

修改RequireAuth组件,添加useEffect监听auth.user的变化,当用户登录后自动触发跳转:

function RequireAuth({ children }: { children: JSX.Element }) {
    let auth = useAuth();
    let location = useLocation();
    let navigate = useNavigate();

    // 监听用户状态变化,登录后自动跳转到目标页
    React.useEffect(() => {
        if (auth.user && location.state?.from) {
            navigate(location.state.from.pathname, { replace: true });
        }
    }, [auth.user, navigate, location.state]);

    if (!auth.user) {
        return <Navigate to="/login" state={{ from: location }} replace />;
    }

    return children;
}

补充说明

方法一是React Router v6推荐的最佳实践,从根源上解决了上下文依赖问题;方法二更适合快速调试或无法重构路由结构的场景,属于临时修复方案。

内容的提问来源于stack exchange,提问作者mezerap

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 02:27:07