Sharing State in Next.js
Sharing state between pages or layouts in a Next.js may be complex when pages fetch data from the server or you need to track state on a layout for tasks like user interactions within nested routes.
Client layouts and server pages
Although client components can't include server components, we can have a client layout rendering a page from the server. This allows us to create a state or context to share data across our application.
Getting Started
We'll create a root layout for our app that will show the counter state, a providers file with our counter context, and a page and nested page that both render a counter client component to mutate the counter state.
|/app |__/layout.js (client) - this is the root layout which displays the counter state |__/providers.js (client) - exports the counter context |__/counter.js (client) - a component that updates the counter |__/page.js (server) - renders the counter component |__/nested |____/page.js (server) - renders the counter component on a nested page
We start by creating a context that stores a counter
value and exports a useCounter
hook to interact with that value:
// app/providers.js import { createContext, useContext, useState } from 'react' const Counter = createContext([0, () => {}]) function CounterProvider({ children }) { const state = useState(0) return <Counter.Provider value={state}>{children}</Counter.Provider> } const useCounter = () => useContext(Counter) export { CounterProvider, useCounter }
Now, lets wrap our root layout with this context provider to make the useCounter
hook available to its pages:
// app/layout.js 'use client' import { Link } from 'next/link' import { CounterProvider, useCounter } from './providers' function RootLayout({ children }) { const [counter] = useCounter() return ( <> <nav> <Link href="/">Index</Link> <Link href="/nested">Nested</Link> </nav> {children} <hr /> <h2>Counter</h2> <p>{counter}</p> </> ) } export default function RootLayoutContainer(props) { return ( <CounterProvider> <RootLayout {...props} /> </CounterProvider> ) }
Next, let's create a counter component that will allow us to increment and decrement the counter value.
// app/counter.js 'use client' import { useCounter } from './providers' export default function Counter() { const [counter, setCounter] = useCounter() return ( <div> <button onClick={() => setCounter((counter) => counter - 1)}> Decrement </button> <p>{counter}</p> <button onClick={() => setCounter((counter) => counter + 1)}> Increment </button> </div> ) }
And finally we will create a page and a nested page where both render the counter component:
// app/page.js import Counter from './counter' export default function IndexPage() { return ( <div> <h1>Index page</h1> <Counter /> </div> ) }
// app/nested/page.js import Counter from './counter' export default function NestedPage() { return ( <div> <h1>Nested page</h1> <Counter /> </div> ) }
Fetching data from the layout
If you need your layout to fetch information from the server, you can create a group and make the layout inside the group a client component instead of the root layout
|/app |__/layout.js (server) |__/(app) |____/layout.js (client) |____/page.js (server) |____/providers.js (client) |____/counter.js (client) |____/nested |______/page.js (server)
Demo
Now you will be able to fetch information from the server while sharing state between layouts and routes, click here to see a working demo.