如何用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:
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.
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;
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;
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;
- No Premature Renders: The
loadingcheck 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
getRedirectPathfunction 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-networkbalances 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

