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

如何用Apollo Client + React Router 4实现私有路由与基于用户状态的重定向?

Got it, let's tackle this problem step by step. You're using React Router 4 and Apollo Client, need to build a robust PrivateRoute system that handles redirects based on user state (like isLoggedIn, isOnboarded, isWaitlisted) and ensures no page renders before confirming the user's status. Here's how to do it properly:

1. First: Fetch & Cache User State with Apollo Client

First, you need a reliable way to get the user's status from Apollo's cache or the server. Let's create a custom hook to handle this—it'll make your code reusable across components.

import { useQuery, gql } from '@apollo/client';

const GET_USER_STATUS = gql`
  query GetUserStatus {
    user {
      isLoggedIn
      isOnboarded
      isWaitlisted
      # Add other status flags you need for your funnel
    }
  }
`;

export const useUserStatus = () => {
  const { loading, error, data } = useQuery(GET_USER_STATUS, {
    fetchPolicy: 'cache-and-network', // Use cached data first, then refresh from server
    ssr: false, // Adjust this if you're doing server-side rendering
  });

  return {
    loading,
    error: error?.message,
    userStatus: data?.user || {},
  };
};

This hook gives you the loading state, any errors, and the user's status flags. The cache-and-network policy ensures a fast experience (using cached data immediately) while keeping the status fresh with the server.

2. Build the PrivateRoute Component

This component wraps React Router's Route and handles authorization logic. It checks if the user meets required status flags, redirects if not, and shows a loading state until the user status is confirmed.

We'll make it flexible for dynamic redirects (like sending logged-in but non-onboarded users to onboarding instead of login):

import { Route, Redirect } from 'react-router-dom';
import { useUserStatus } from './useUserStatus';
import LoadingSpinner from './LoadingSpinner'; // Replace with your loading component

const PrivateRoute = ({
  component: Component,
  requiredStatus, // Object like { isLoggedIn: true, isOnboarded: true }
  getRedirectPath, // Function to return dynamic redirect path based on user state
  ...rest
}) => {
  const { loading, userStatus } = useUserStatus();

  // Show loading until we confirm user status—no premature renders!
  if (loading) {
    return <LoadingSpinner />;
  }

  // Check if all required status flags are satisfied
  const meetsRequirements = Object.entries(requiredStatus).every(
    ([key, value]) => userStatus[key] === value
  );

  return (
    <Route
      {...rest}
      render={(props) =>
        meetsRequirements ? (
          <Component {...props} />
        ) : (
          <Redirect to={getRedirectPath(userStatus)} />
        )
      }
    />
  );
};

export default PrivateRoute;
3. Handle Public Routes (Like Login/Onboarding)

Even public routes should check user status to avoid unnecessary renders. For example, a logged-in user shouldn't see the login page. Let's create a PublicRoute component for this:

import { Route, Redirect } from 'react-router-dom';
import { useUserStatus } from './useUserStatus';
import LoadingSpinner from './LoadingSpinner';

const PublicRoute = ({
  component: Component,
  allowedStatus, // Object like { isLoggedIn: false }
  redirectPath, // Path to redirect if user doesn't meet allowed status
  ...rest
}) => {
  const { loading, userStatus } = useUserStatus();

  if (loading) {
    return <LoadingSpinner />;
  }

  const isAllowed = Object.entries(allowedStatus).every(
    ([key, value]) => userStatus[key] === value
  );

  return (
    <Route
      {...rest}
      render={(props) =>
        isAllowed ? (
          <Component {...props} />
        ) : (
          <Redirect to={redirectPath} />
        )
      }
    />
  );
};

export default PublicRoute;
4. Put It All Together in Your Router

Now configure your routes to map user states to the correct pages. Here's an example of a typical conversion funnel flow:

import { BrowserRouter as Router, Switch } from 'react-router-dom';
import PrivateRoute from './PrivateRoute';
import PublicRoute from './PublicRoute';

// Your page components
import Dashboard from './pages/Dashboard';
import Login from './pages/Login';
import Onboarding from './pages/Onboarding';
import Waitlist from './pages/Waitlist';
import NotFound from './pages/NotFound';

const AppRouter = () => {
  return (
    <Router>
      <Switch>
        {/* Dashboard: Requires logged in + completed onboarding */}
        <PrivateRoute
          path="/dashboard"
          component={Dashboard}
          requiredStatus={{ isLoggedIn: true, isOnboarded: true }}
          getRedirectPath={(userStatus) => {
            if (!userStatus.isLoggedIn) return '/login';
            if (!userStatus.isOnboarded) return '/onboarding';
            return '/login'; // Fallback
          }}
        />

        {/* Login: Only allowed if user isn't logged in */}
        <PublicRoute
          path="/login"
          component={Login}
          allowedStatus={{ isLoggedIn: false }}
          redirectPath="/dashboard"
        />

        {/* Onboarding: Requires logged in but not yet onboarded */}
        <PrivateRoute
          path="/onboarding"
          component={Onboarding}
          requiredStatus={{ isLoggedIn: true, isOnboarded: false }}
          getRedirectPath={(userStatus) => {
            if (!userStatus.isLoggedIn) return '/login';
            if (userStatus.isOnboarded) return '/dashboard';
            return '/login';
          }}
        />

        {/* Waitlist: Only for users in the waitlist */}
        <PrivateRoute
          path="/waitlist"
          component={Waitlist}
          requiredStatus={{ isWaitlisted: true }}
          getRedirectPath={(userStatus) => {
            if (!userStatus.isLoggedIn) return '/login';
            if (!userStatus.isWaitlisted) return '/dashboard';
            return '/login';
          }}
        />

        {/* Catch-all for 404 */}
        <Route component={NotFound} />
      </Switch>
    </Router>
  );
};

export default AppRouter;
5. Key Notes to Remember
  • No Premature Renders: The loading check in both route components ensures no page (or redirect) happens until we have the user's status from Apollo—eliminating flash-of-unauthorized-content issues.
  • Dynamic Redirects: The getRedirectPath function lets you handle complex funnel flows, like sending users straight to the next step in their journey instead of a generic login page.
  • Cache + Freshness: Using cache-and-network balances speed (cached data) and accuracy (server refresh).
  • Error Handling: You can extend the hooks/components to handle network errors by adding an error state and rendering an error page instead of loading.

That should cover all your requirements! Let me know if you need to tweak any part for your specific funnel logic.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 04:35:44