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

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;
}

问题根源

  1. 路由实例重复创建导致状态丢失:
    根路由组件Router每次重渲染时都会调用createBrowserRouter创建全新的路由实例,当AuthContext中的userId更新触发组件重渲染时,新的路由实例会重置路由状态,导致首次跳转的状态被覆盖,UI停留在登录页。

  2. 闭包捕获的上下文过时:
    loginAction(authContextApi)在路由初始化时捕获了初始的authContextApi引用,后续AuthContextProvider重渲染生成的新api实例无法被action获取,导致首次登录后userId的更新无法触发路由组件的状态同步。

  3. 组件渲染时直接返回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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 10:16:01