How would you manage this for 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.
"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:
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 route —
React.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.