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.