Back to React
React
medium
mid

How would you manage shared authentication state across multiple components or routes?

Extract the shared logic into a reusable custom hook or a query library so each component/route doesn't re-implement it. Centralize cross-cutting state (auth, theme) in context/store, code-split per route, and use a layout/loader pattern so routes share fetching, loading, and error handling consistently.

4 min read·~8 min to think through

"Manage this across multiple components or routes" — whatever "this" is (data fetching, loading state, auth, a subscription) the answer is the same principle: don't duplicate it — extract it once and share it.

1. Extract logic into a custom hook

If several components do the same data-fetching / state logic, pull it into a custom hook:

js
function useUser(userId) {
  const [state, setState] = useState({ status: "loading", data: null });
  useEffect(() => {
    const controller = new AbortController();
    fetchUser(userId, controller.signal).then(/* ... */);
    return () => controller.abort();
  }, [userId]);
  return state;
}

Now every component/route calls useUser(id) — one implementation, consistent loading/error/cleanup behavior everywhere.

2. Use a data layer for server state

For fetching specifically, React Query / SWR is the multi-component answer: query keys mean two components requesting the same data share one cached request (dedup), refetch consistently, and get uniform loading/error states. No per-route boilerplate.

3. Centralize genuinely shared state

Cross-cutting state — auth user, theme, feature flags — goes in Context or a store (Zustand/Redux) at the app root, so any route reads it without prop-drilling or refetching.

4. Route-level patterns

  • Code-split per routeReact.lazy + Suspense, so each route only loads its own code.
  • Shared layout components — a <DashboardLayout> wrapping multiple routes handles common chrome, auth guards, and shared data once.
  • Route loaders — React Router's loader (or Next.js server components / loaders) fetch a route's data before render, centralizing the fetch + error handling per route declaratively.
  • Error boundaries per route — so one route crashing doesn't take down the app.

5. The principle

DRY the logic, not just the markup. The failure mode this question targets is copy-pasting the same useEffect-fetch-loading-error block into 10 components — they drift, bugs get fixed in some but not others. One hook / one query / one provider = one place to fix.

The framing

"Whatever 'this' is, the move is the same: extract it once and share it. Shared logic becomes a custom hook so every component calls one implementation. For server data specifically, a query library shares cached requests across components by key. Genuinely global state — auth, theme — lives in context or a store at the root. At the route level, I code-split, use shared layouts and route loaders to centralize fetching and error handling, and put error boundaries per route. The anti-pattern I'm avoiding is duplicating the same effect-and-state block everywhere so it drifts."

Follow-up questions

  • When do you extract logic into a custom hook vs a context?
  • How does React Query dedupe requests across components?
  • What's the route loader pattern and what does it centralize?
  • How do shared layouts reduce per-route duplication?

Common mistakes

  • Copy-pasting the same fetch/loading/error logic into every component.
  • Putting everything in global state instead of a reusable hook.
  • Re-fetching the same data independently in sibling components.
  • No per-route error boundaries or code splitting.

Performance considerations

  • A query library deduping shared requests cuts redundant network calls. Code splitting per route shrinks each route's bundle. Centralized state must use selectors/memoization to avoid re-rendering every route on change.

Edge cases

  • Two routes needing the same data with slightly different shapes.
  • A custom hook that's used in so many places its API needs to stay stable.
  • Shared state that updates frequently causing wide re-renders.

Real-world examples

  • A useAuth() hook consumed by guards, headers, and pages across all routes.
  • React Query sharing a cached /me request across the whole app.

Senior engineer discussion

Seniors answer with the DRY-the-logic principle: custom hooks for shared logic, a query library for shared server state, context/store for cross-cutting state, and route-level patterns (code splitting, layouts, loaders, error boundaries) — avoiding duplicated logic that drifts.

Related questions