Been rebuilding our dashboard with App Router and keep hitting the same wall. We've got three route groups that need to coordinate state (auth context, user prefs, feature flags). Context providers are getting unwieldy, and we're doing more prop drilling than I'd like.
Currently we're wrapping everything in a context at the root layout, but that causes re-renders across unrelated routes. Tried using Zustand at the boundary but it doesn't play nice with server components without awkward client boundaries.
// current mess
<AuthProvider>
<PrefsProvider>
<FlagsProvider>
{children}
</FlagsProvider>
</PrefsProvider>
</AuthProvider>
Thinking maybe segment-level providers are better, or just embracing more granular client components. Haven't found good docs on this pattern. How are you all managing this.
I'd step back and ask if you actually need shared state across all three groups. In my experience, this problem usually means the boundaries are wrong.
Auth belongs in middleware/a single provider. User prefs and feature flags? Those are often better as server-side queries (database or cache hit on each route) rather than context. You pay a small perf cost upfront, avoid re-render cascades, and server components actually handle it cleanly.
If you must coordinate, Zustand works fine with the client boundary pattern. Wrap just the routes that need it, not the whole app. The "awkwardness" goes away once you stop fighting the server/client split.
Skip the monolithic context. Use Zustand with a server action boundary pattern instead. Keep your store client-side but initialize it server-side via a layout server component that passes hydration data down.
Root layout stays clean (just the provider), each route group initializes from that hydration data. Solves the re-render problem because unrelated routes don't subscribe to the whole store, only what they need.
The "doesn't play nice with server components" thing is just pattern friction. Server components call your actions, mutations hit Zustand, UI responds. We've done this for three dashboards now. Way simpler than fighting Context.
Skip the context wrapper at root. Use Zustand with a server action bridge instead. Keep your stores client-side, initialize them from a single server action that fetches auth/prefs/flags, then pass that data as props to your layout. Route groups stay independent until they actually need to talk.
We did exactly this and killed the unnecessary re-renders. Context for shared state across multiple route groups is the wrong abstraction here anyway.
Nina Okafor
ML engineer working on LLMs and RAG pipelines
Been there. Context at root layout is the trap everyone falls into. What actually worked for us: keep auth/user prefs in a lightweight context (just the bare essentials), but move feature flags to a separate server component that reads from a database or config service on each request.
The key insight: not all state needs to be reactive. Feature flags especially. We cache them with short TTLs and pass them down as props where needed. Cuts re-renders by like 60% in our test environment.
For the Zustand issue, yeah, mixing server/client is rough. We use Zustand only on the client boundary and keep it minimal. Route groups actually handle this well if you lean into the file structure instead of fighting it.
What's your current re-render pattern looking like.