React Router v6登录表单需两次提交才跳转的问题排查
React Router v6 登录认证流程首次跳转UI不更新问题分析
问题现象
- 首次提交正确登录信息后,URL更新为目标路由(如
/app/profile),但页面停留在登录页,认证路由和目标组件未渲染 - 再次提交表单后,认证路由和目标路由正常渲染
相关代码
根路由
function Router() { const authContextApi = useAuthContextApi(); console.log("Root router"); const routesConfig = [ { path: "/", element: <LandingRoute />, errorElement: <ErrorRoute />, }, { path: "/auth/login", element: <LoginRoute />, action: loginAction(authContextApi), }, { path: "/auth/register", element: <RegisterRoute />, action: registerAction, }, { path: "/app", element: ( <AuthRoute> <AppRoot /> // AppRoot是包含布局组件和<Outlet />的包装器 </AuthRoute> ), children: [ { path: "profile", element: <ProfileRoute />, loader: profileLoader, }, ], }, ]; const router = createBrowserRouter(routesConfig); return <RouterProvider router={router} />; }
AuthContext
function AuthContextProvider({ children }: React.PropsWithChildren) { const [userId, setUserId] = useState<string>(""); console.log("Value of userId in AuthContext", userId); const contextValue = { userId }; const api = { setUserId }; return ( <AuthContextApi.Provider value={api}> <AuthContext.Provider value={contextValue}> {children} </AuthContext.Provider> </AuthContextApi.Provider> ); }
loginAction函数
const loginAction = (authContextApi: AuthContextApi): ActionFunction => async ({ request }) => { const url = new URL(request.url); const redirectTo = url.searchParams.get("redirectTo"); const formData = Object.fromEntries(await request.formData()); console.log("After input validation"); const response = "1"; authContextApi?.setUserId(response); console.log("Before redirect"); return redirect(redirectTo || "/app/profile"); };
登录组件
function LoginRoute() { const [searchParams] = useSearchParams(); const redirectTo = searchParams.get("redirectTo"); const url = `/auth/login${ redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : "" }`; return ( <Form method="post" action={url}> <input type="email" name="email" placeholder="Email address" /> <input type="password" name="password" placeholder="Password" /> <button type="submit">Login</button> </Form> ); }
认证路由
function AuthRoute({ children }: React.PropsWithChildren<{}>) { const { userId } = useAuthContext(); const [searchParams] = useSearchParams(); const redirectTo = searchParams.get("redirectTo"); if (!userId) { console.log("UserId is empty string"); const redirectUrl = `/auth/login${ redirectTo ? `?redirectTo=${encodeURIComponent(redirectTo)}` : "" }`; return <Navigate to={redirectUrl} replace />; } console.log("UserId in AuthRoute", userId); return children; }
问题根源
路由实例重复创建导致状态丢失:
根路由组件Router每次重渲染时都会调用createBrowserRouter创建全新的路由实例,当AuthContext中的userId更新触发组件重渲染时,新的路由实例会重置路由状态,导致首次跳转的状态被覆盖,UI停留在登录页。闭包捕获的上下文过时:
loginAction(authContextApi)在路由初始化时捕获了初始的authContextApi引用,后续AuthContextProvider重渲染生成的新api实例无法被action获取,导致首次登录后userId的更新无法触发路由组件的状态同步。组件渲染时直接返回
Navigate的副作用:AuthRoute在渲染阶段直接返回Navigate组件,可能导致路由状态还未完成更新就触发重定向,引发状态不同步。
解决方案
1. 缓存路由实例,避免重复创建
将路由配置和createBrowserRouter调用移出组件,或用useMemo缓存路由实例,确保路由实例的稳定性:
// 路由配置移到组件外部 const routesConfig = [ { path: "/", element: <LandingRoute />, errorElement: <ErrorRoute />, }, { path: "/auth/login", element: <LoginRoute />, action: loginAction, // 不再直接传入authContextApi }, { path: "/auth/register", element: <RegisterRoute />, action: registerAction, }, { path: "/app", element: ( <AuthRoute> <AppRoot /> </AuthRoute> ), children: [ { path: "profile", element: <ProfileRoute />, loader: profileLoader, }, ], }, ]; function Router() { // 用useMemo缓存路由实例 const router = useMemo(() => createBrowserRouter(routesConfig), []); console.log("Root router"); return <RouterProvider router={router} />; }
2. 在action内部获取AuthContext
React Router v6的action函数支持使用React Hooks,直接在action中获取上下文,避免闭包问题:
const loginAction: ActionFunction = async ({ request }) => { const url = new URL(request.url); const redirectTo = url.searchParams.get("redirectTo"); const formData = Object.fromEntries(await request.formData()); console.log("After input validation"); const response = "1"; // 在action内部获取最新的上下文实例 const authContextApi = useAuthContextApi(); authContextApi?.setUserId(response); console.log("Before redirect"); return redirect(redirectTo || "/app/profile"); };
3. 优化AuthRoute的重定向逻辑
使用useNavigate和useEffect处理重定向,避免渲染阶段直接返回Navigate:
function AuthRoute({ children }: React.PropsWithChildren<{}>) { const { userId } = useAuthContext(); const navigate = useNavigate(); const location = useLocation(); useEffect(() => { if (!userId) { console.log("UserId is empty string"); // 携带当前页面的完整路径作为重定向目标 const redirectUrl = `/auth/login?redirectTo=${encodeURIComponent(location.pathname + location.search)}`; navigate(redirectUrl, { replace: true }); } }, [userId, navigate, location]); // 未登录时返回null或加载组件,避免重复渲染 if (!userId) { return null; } console.log("UserId in AuthRoute", userId); return children; }
4. 确保AuthContextProvider包裹RouterProvider
保证路由组件和action函数都能正确访问上下文:
function App() { return ( <AuthContextProvider> <Router /> </AuthContextProvider> ); }
内容的提问来源于stack exchange,提问作者John Doe
相关产品推荐
相关产品推荐

