skip to content
Gary Holland's blog

Sharing in-memory state between pages in Next.js

/ 5 min read

The problem

I recently needed to share some state between two pages in a Next.js app with the following constraints:

  1. it couldn’t be passed in URL params
  2. it couldn’t be stored anywhere on the client (so local storage, session storage, and cookies were out)
  3. it couldn’t be stored anywhere on the server
  4. it must only be available in the relevant pages (so any kind of global provider was out)

What I wanted was a way to keep hold of the state in memory as the user navigates between the pages in the client-side. I knew that I could solve this problem in a “regular” React app (i.e. one where I’m handling my own routing etc rather than using a framework like Next.js) with React’s Context API, but I didn’t know how to wrap multiple Next.js routes inside a shared context provider, nor whether the same instance would get reused. If I couldn’t get the same instance to be reused, then the state wouldn’t be maintained anyway.

It turns out that this is totally possible and very simple to implement! The next section describes how it works. You can click here to skip ahead for a code example .

Per-page layouts

Next.js supports layouts, which are components that wrap whole pages to provide common functionality. For example, if you have the same header on every page in your application, you can just wrap the whole application in a layout and render the header there.

This is a promising start because if we render a context provider in a layout, then any page that gets wrapped in the layout will have access to its data. But this would break constraint number 4. Fortunately, Next.js supports per-page layouts, which are exactly what they sound like: layouts defined for individual pages rather than for the whole application. So we can define layouts for our two relevant pages and only wrap them in our context provider.

And the real magic is that React is able to maintain the same instance of the context provider between each page that is wrapped in it via one of these layouts. From the docs:

This layout pattern enables state persistence because the React component tree is maintained between page transitions. With the component tree, React can understand which elements have changed to preserve state.

That’s exactly what we want!

Code example

Here’s how it works in practice. I’m using TypeScript, but you can just ignore all the types and you’ll have a working JavaScript version if that’s your thing.

First, we can copy an example from the official docs to define some types for pages and apps with layouts:

// pages/_app.tsx
import type { ReactElement, ReactNode } from "react";
import type { NextPage } from "next";
import type { AppProps } from "next/app";

export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
	getLayout?: (page: ReactElement) => ReactNode;
};

type AppPropsWithLayout = AppProps & {
	Component: NextPageWithLayout;
};

Next, let’s create our context provider. To keep it simple, our provider will just expose a number and a function that we can call to increment that number.

// pages/number-context.tsx
import { createContext, useContext, useEffect, useState } from "react";
import * as React from "react";

const numberContext = createContext<{ incremenet: () => void; num: number } | null>(null);

export const useNumber = () => useContext(numberContext);

export const NumberProvider = ({ children }: { children: React.ReactNode }) => {
	const [num, setNum] = useState(0);

	function increment() {
		setNum((currentValue) => currentValue + 1);
	}

	return <numberContext.Provider value={{ increment, num }}>{children}</numberContext.Provider>;
};

Then, we can wrap our first page with our provider, allowing us to increment the number in the page:

// pages/increment.tsx

import type { ReactElement } from "react";
import { NumberProvider, useNumber } from "./number-context";
import type { NextPageWithLayout } from "./_app";
import Link from "next/link";

const Page: NextPageWithLayout = () => {
	const { increment, num } = useNumber();
	return (
		<>
			<p>The number is: {num}</p>
			<button onClick={increment}>Increment</button>
			<Link href="/view-number">View final number</Link>
		</>
	);
};

Page.getLayout = function getLayout(page: ReactElement) {
	return <NumberProvider>{page}</NumberProvider>;
};

export default Page;

And wrap our second page with the provider, allowing us to demonstrate that the state was maintained as we moved between pages.

// pages/view-number.tsx

import type { ReactElement } from "react";
import { NumberProvider, useNumber } from "./number-context";
import type { NextPageWithLayout } from "./_app";
import Link from "next/link";

const Page: NextPageWithLayout = () => {
	const { num } = useNumber();
	return <p>The final number is: {num}</p>;
};

Page.getLayout = function getLayout(page: ReactElement) {
	return <NumberProvider>{page}</NumberProvider>;
};

export default Page;

All that remains is to update the root of the application to check for a getLayout method on each page and use it if it’s there. This example also comes from the Next.js docs:

// pages/_app.tsx
// after the type definitions we wrote earlier
export default function App({ Component, pageProps }: AppPropsWithLayout) {
	// Use the layout defined at the page level, if available
	const getLayout = Component.getLayout ?? ((page) => page);

	return getLayout(<Component {...pageProps} />);
}

Conclusion

That’s it! You can now navigate between two pages of your application while maintaining some state in memory.

Next steps you’re likely to want to take from here include abstracting the getLayout method for your pages into a reusable function and redirecting the user if they arrive at the view-number page without having first visited increment. These are left as exercises for the reader 🙂.