Managing State Beyond Local Components
The core building blocks for state in a Next.js application are the same primitives available in any React project. Data always flows from parent to child through props. When that chain gets deep, the Context API steps in to deliver data directly to whichever component needs it, skipping the intermediate layers — this is all the Context API does, plumbing data between Provider and Consumer. It has no opinion about how that data is structured or updated.
For updating state, two React hooks cover the ground that most apps need:
useState— best suited for simple data structures.useReducer— a better fit for complex state shapes and conditional updates.
Next.js brings its own file-based routing convention into the mix, and two files inherit special responsibilities there. In _app, you define logic meant to be shared across every view — put your Context Providers here if you keep things basic. A <Provider> wrapper on this level hands state down to whichever page component currently renders:
If the state you need to share is composed of several distinct concerns, a single context started as one provider will quickly become a bottleneck. React re-renders everything subscribed to a context when its value changes, so state shared too broadly leads to wasted render cycles. The practical fix is splitting your contexts by concern — authentication users in one, theme in another, internationalization in yet another. This is a moderate amount of bookkeeping, but considerably easier than tracking down renders caused by an unrelated slice of state.
Abstracting Layers and Hierarchy
When the number of providers at the root starts to grow past a comfortable minimum, the Layout pattern gives you a place to consolidate that logic without mixing it with the page. Next.js lets you set a getLayout function and override the default on any route, giving every page its own definition of what is provided and how. Keep these layouts in separate modules and reuse them across routes to avoid duplication.
For properly nested routes — a wider layout component wrapping a set of pages that share common state between them — Next.js has no low-config solution yet, though there is an open proposal for a Wrapper feature. For now the per-page Layout approach gets you close even to this case, because the getLayout function behaves much like a children prop: you can wrap layers as deeply as your hierarchy requires and share that logic across multiple routes.
Provider Hell Down the Road
Once your app hits a certain size, you'll find the nest of providers in the root quite hard to inspect. If one provider is intended for only one view, things get worse: wiring it in at runtime requires that all consumers and the provider link up while the app is mounted — which undercuts the readability you gained from avoiding prop drilling in the first place.
Jōtai is a state library that sidesteps this trap while keeping a familiar API. Its atoms look much like useState on the surface, and because they attach at the component level, you avoid hand-rolling the provider tree. Jōtai does wrap things in a Provider internally, but isolates each atom's values so a single provider can serve many isolated definitions. It also supports a provider-less mode for apps with a single store, accepts initialValues with default state in place, and supports composing derived definitions from one another. It won't suit every project, but for incremental scalability when the provider pyramid is out of control, it can be a better alternative.
Client-Side Data Fetching
Almost every real application needs to pull data from external APIs at some point, and the client side brings its own set of concerns. When you fetch data in the browser, you have to think about more than just making the request:
- Don't refetch what you already have — respect the user's network connection.
- Decide what to show while the server responds.
- Handle the cases where data isn't available: server errors, empty responses, broken endpoints.
The first point is purely a fetching concern. The others straddle the line between fetching and state management. Handling errors and recovery is definitely state management territory, but it's all tied to the fetch lifecycle and the server integration. These are universal problems, and the patterns for solving them don't vary much between apps — which is exactly why libraries exist for them.
Tools like React Query and SWR apply the same local state patterns we've discussed to external data. They handle caching on the client, so when state is already available you can configure whether to use the cache or refresh it. They can even serve stale data while revalidating in the background, then prompt an interface update when fresh content arrives.
The React team has also signaled early on that new APIs are coming to improve this experience — the proposed Suspense documentation is a good reference. Library authors have prepared for those APIs, and developers can already use similar syntax today.
Here's how the MainUserManagement layout handles external state with SWR:
import { useSWR } from 'swr'
import { UserInfoProvider } from '../context/user-info'
import { ExtDataProvider } from '../context/external-data-provider'
import { UserNavigationLayout } from '../layouts/user-navigation'
import { ErrorReporter } from '../components/error-reporter'
import { Loading } from '../components/loading'
export const MainUserManagement = (page) => {
const { data, error } = useSWR('/api/endpoint')
if (error) => <ErrorReporter {...error} />
if (!data) => <Loading />
return (
<UserInfoProvider>
<ExtDataProvider>
<UserNavigationLayout>
{page}
</UserNavigationlayout>
</ExtDataProvider>
</UserInfoProvider>
)
}
The useSWR hook bundles a lot of abstractions:
- a default fetcher
- a zero-config caching layer
- error handling
- loading handling
With just two conditions you can provide early returns in your component — one for when the request fails, one for while the round-trip is pending. These libraries sit close to state management tools: they aren't strictly state managers, but they integrate cleanly and give you what you need to manage complex asynchronous states.
One advantage of an isomorphic app is reducing back-end requests. Every additional client-side request hurts perceived performance. There's a detailed article on the topic worth reading.
This pattern is not meant to replace getStaticProps or getServerSideProps in Next.js apps. It's another tool for specific situations where client-side fetching is the right call.
Tradeoffs To Keep In Mind
These patterns come with caveats. Two stand out:
- Server-side static generation is often a better choice than client-side fetching. Prefer it when possible.
- The Context API can cause multiple re-renders if you aren't careful about where state changes occur.
All the standard best practices for client-side React state still apply in Next.js. The server layer can give you a performance boost, which may offset some computation issues, but it doesn't excuse ignoring good rendering habits.
Try The Patterns Yourself
You can see these patterns live at nextjs-layout-state.netlify.app or dig into the code on GitHub. Deploy the repo instantly to Netlify:
If you'd prefer something less opinionated or are just starting with Next.js, there's an awesome starter project ready to deploy:
References
- Context and Redux: differences
- Next.js Wrapper Proposal
- Next.js Layouts
- Jōtai
- Using React Context for State Management in Next.js
Further Reading
- A Guide To Image Optimization On Jamstack Sites
- A New Pattern For The Jamstack: Segmented Rendering
- Full Stack GraphQL With Next.js, Neo4j AuraDB And Vercel
- How To Build A Multilingual Website With Nuxt.js




