Jussi Nevavuori Logo JN
Open menu
Gradient

How To Access Page Props in All Components with Next.js and TypeScript

Published 01.07.2022

#nextjs#typescript#webdev#javascript

The problem: Lifting page props from getStaticProps to your <Layout/> component, rendered in _app.tsx is a hassle. In fact, Next.js provides no existing solution to that problem and we have to come up with one ourselves. In this article I present the idea of global props as a solution.

When would I ever need to do this?

A simple scenario where I could see this happening is on a site with a selection of products. Each product is fetched in getStaticProps, in /products and the developer wants to display the number of products in the header, next to the link to the /products page.

This means the developer has to send the products prop received by the products page component from getStaticProps all the way to the <Header/> component, rendered by <Layout/> in _app.tsx.

A worse developer might come up with a “quick hack”, a one-off solution to get just this feature working. For example, they might set up a pubsub system, where the header component subscribes to a subscribable, and the /products page component publishes the props it receives. Will work, but runs the risk of becoming unmaintainable and confusing as the data is moving sideways through the component tree via an external subscribable.

Instead, this article demonstrates another way of passing data into the entire application.

Introducing global props

The solution I ended up using on my personal portfolio page, as well as other projects I have worked on is GlobalProps.

We declare certain props as global, which are accessible on all pages. In the above example, that might be a list of products. These could also be projects and blogs on your personal page or any other server-side fetched data that is accessible to the entire application. The essence of the solution goes as follows.

  1. On all pages, fetch the global props (and other page-specific props)
  2. Intercept the global props in _app.tsx using pageProps
  3. Pass the global props to a GlobalPropsContext in _app.tsx
  4. Use the global props in ANY component with a custom hook.

In addition to the solution, I will also provide some utility functions which make these steps easier. Especially step 1, fetching the global props on each page, which could result in a lot of code duplication with a worse implementation.

GitHub

Follow the code on GitHub

Step 1 - Define the global props

First we define what props we want to declare as global. We are going to run with the above example and define products as a global prop we want to be accessible app-wide. Let’s declare it as a type, assuming we have some Product type.

// types/index.d.ts

type Product = { /* ... */ }

type GlobalProps = {
	product: Product[];
};

Step 2 - Fetching the global props

Step 1 was easy. Now we can get on with even more code. Let’s assume we have a productApi, which defines the productApi.getAll(): Promise<Product[]> method for fetching all products.

Let’s first define a utility function which fetches global props.

// features/GlobalProps/lib/fetchGlobalProps.ts

import { productsApi } from "../../../lib/api/productsApi";

export async function fetchGlobalProps(): Promise<GlobalProps> {
  return {
    products: await productsApi.getAll(),
  };
}

We could now use this function on all pages as follows

// pages/index.tsx

import type { GetStaticProps, InferGetStaticPropsType } from 'next'
import { fetchGlobalProps } from '../features/GlobalProps/lib/fetchGlobalProps';

type PageProps = InferGetStaticPropsType<typeof getStaticProps>;

export default function Page(props: PageProps) {
	return (
		<div style={{ padding: "8rem" }}>

			<h1>Home</h1>

		</div>
	)
}

export const getStaticProps: GetStaticProps<GlobalProps> = async () => {
	return {
		props: {
			...await fetchGlobalProps(),
		}
	}
}

But we can make this even nicer by providing a getStaticProps wrapper that automatically adds global props to any getStaticProps function.

Let’s define getStaticPropsWithGlobalProps for this purpose. It’ll act as a wrapper for the getStaticProps function and can fetch the rest of the data required by the page using the regular getStaticProps API, however it automatically adds the global props to the returned props, with automatic typing.

// features/GlobalProps/lib/getStaticPropsWithGlobalProps.ts

import {
  GetStaticProps,
  GetStaticPropsContext,
  GetStaticPropsResult,
} from "next";
import { fetchGlobalProps } from "./fetchGlobalProps";

export function getStaticPropsWithGlobalProps<T extends {}>(
  getStaticProps: (
    ctx: GetStaticPropsContext,
    globalProps: GlobalProps
  ) => Promise<GetStaticPropsResult<T>>
): GetStaticProps<T & GlobalProps> {

  // Construct getStaticProps function
  return async (ctx: GetStaticPropsContext) => {

    // Fetch global props
    const globalProps = await fetchGlobalProps();

    // Run getStaticProps with user defined getStaticProps, provide context
    // and global props
    const result = await getStaticProps(ctx, globalProps);

    // If redirect or notFound in result, return result as is, in this case
    // no page props will be provided
    if ("redirect" in result || "notFound" in result) {
      return result;
    }

    // Return combined page props and global props as page props
    return {
      props: {
        ...result.props,
        ...globalProps,
      },
      revalidate: result.revalidate,
    };
  };
}

Let’s also quickly define a GlobalProps object which exports all functionality meant for the public API of this feature and export this function as getStaticProps.

// features/GlobalProps/GlobalProps.ts

import { getStaticPropsWithGlobalProps } from "./lib/getStaticPropsWithGlobalProps";

// Defines the public API
export const GlobalProps = {
  getStaticProps: getStaticPropsWithGlobalProps,
};

Now we can use it on our pages as follows.

// pages/products.tsx

// ...

export const getStaticProps = GlobalProps.getStaticProps<{exampleValue: number}>(
	async (ctx) => {
		return { props: { exampleValue: 1 } }
	}
)

The page will now have access to both products, along with all the other components in the app. The page will also have access to someOtherProp, however it will not be accessible to the rest of the app, only the page via page props.

Accessing Global Props in getStaticProps

Furthermore, we can access the global props in the getStaticProps implementation as the second argument after ctx, the default getStaticProps context object. This is useful for example on the product page, where the props depend on the global props.

// pages/products/[slug].ts

import { GetStaticPaths, InferGetStaticPropsType } from "next";
import { GlobalProps } from "../../features/GlobalProps/GlobalProps";
import { productsApi } from "../../lib/api/productsApi";

type PageProps = InferGetStaticPropsType<typeof getStaticProps>;

export default function Page(props: PageProps) {
	return <div style={{ padding: "8rem" }}>
		<h1>{props.product.name}</h1>
		<p>{props.product.price} $</p>
	</div>
}

export const getStaticPaths: GetStaticPaths = async () => {
	const products = await productsApi.getAll()
	return {
		paths: products.map(p => ({ params: { slug: p.slug } })),
		fallback: false,
	}
}

export const getStaticProps = GlobalProps.getStaticProps<{ product: Product }>(
	async (ctx, globalProps) => {

		// GetStaticProps has access to the global props as the parameter
		const slug = (ctx.params?.["slug"] ?? "").toString()
		const product = globalProps.products.find(_ => _.slug === slug)

		if (!product) return { notFound: true }

		return { props: { product } }
	}
)

Utility for pages with no static props other than the global props

Instead of having to type out

export const getStaticProps = GlobalProps.getStaticProps(async () => ({ props: { } }))

for every route without props, let’s define GlobalProps.getEmptyStaticProps to do it for us. Then we can simply write

export const getStaticProps = GlobalPrsop.getEmptyStaticProps

// features/GlobalProps/GlobalProps.ts

import {
  GlobalPropsContextProvider,
  useGlobalProps,
} from "./contexts/GlobalPropsContext";
import { extractGlobalProps } from "./lib/extractGlobalProps";
import { getStaticPropsWithGlobalProps } from "./lib/getStaticPropsWithGlobalProps";

// Defines the public API
export const GlobalProps = {
  getStaticProps: getStaticPropsWithGlobalProps,

	// ADD THIS:
  getEmptyStaticProps: getStaticPropsWithGlobalProps(async () => ({
    props: {},
  })),
};

Step 3 - Providing global props via a context

Let’s start by defining a React context for passing the global props to the entire component tree.

// features/GlobalProps/contexts/GlobalPropsContext.tsx

import { createContext, useContext, ReactNode } from "react"

// Default value for global props
export const defaultGlobalPropsContextValue: GlobalProps = {
	products: [],
}

// Global props context
export const GlobalPropsContext = createContext<GlobalProps>(defaultGlobalPropsContextValue)

// Global props context provider props
export interface GlobalPropsContextProviderProps {
	children?: ReactNode,
	globalProps: GlobalProps;
}

// Global props context provider
export function GlobalPropsContextProvider(props: GlobalPropsContextProviderProps) {

	return <GlobalPropsContext.Provider value={props.globalProps}>
		{props.children}
	</GlobalPropsContext.Provider>

}

// Utility hook to access global props
export function useGlobalProps() {
	return useContext(GlobalPropsContext)
}

The context is a regular context which has to be provided the global props which are then passed on. Note the useGlobalProps() hook which is later used to access these global props.

Let’s now also export these to our public API. For shorter syntax, I’m also going to call useGlobalProps() simply use(). This means we can just call GlobalProps.use() from our components to access the props.

// features/GlobalProps/GlobalProps.ts

import {
  GlobalPropsContextProvider,
  useGlobalProps,
} from "./contexts/GlobalPropsContext";
import { extractGlobalProps } from "./lib/extractGlobalProps";
import { getStaticPropsWithGlobalProps } from "./lib/getStaticPropsWithGlobalProps";

// Defines the public API
export const GlobalProps = {
  getStaticProps: getStaticPropsWithGlobalProps,
  getEmptyStaticProps: getStaticPropsWithGlobalProps(async () => ({
    props: {},
  })),
  Provider: GlobalPropsContextProvider,
  use: useGlobalProps,
	extract: extractGlobalProps,
};

Wait a minute - what is extractGlobalProps? In short, _app.tsx will receive a pageProps object of unknown type. We have to take the pageProps object and extract the globalProps from it into an object of type GlobalProps. At the same time we have to handle errors and invalid or missing data and provide default values. The implementation is the following for this use case (assuming a productsApi.isProducts(arg): arg is Product[] function exists.

// features/GlobalProps/lib/extractGlobalProps.ts

import { productsApi } from "../../../lib/api/productsApi";
import { defaultGlobalPropsContextValue } from "../contexts/GlobalPropsContext";

export function extractGlobalProps(data: any): GlobalProps {
  if (!data) return defaultGlobalPropsContextValue;

  // Do it the correct way with type validation and default values
  return {
    products: productsApi.isProducts(data.products) ? data.products : [],
  };

  // Or do it the lazy, error prone way if you trust your pageProps to have
	// the correct shape
	// return data as GlobalProps;
}

Now the last thing to get the props to our entire application is to provide them in _app.tsx. We set the GlobalProps.Provider as the root component in our application in order for all components to have access to GlobalProps and pass the extracted global props as an argument to it.

// pages/_app.tsx

import type { AppProps } from 'next/app'
import { GlobalPropsContextProvider } from '../features/GlobalProps/contexts/GlobalPropsContext'
import { GlobalProps } from '../features/GlobalProps/GlobalProps'
import { Layout } from '../components/Layout/Layout'

function MyApp({ Component, pageProps }: AppProps) {
	return <GlobalPropsContextProvider globalProps={GlobalProps.extract(pageProps)}>
		<Layout>
			<Component {...pageProps} />
		</Layout>
	</GlobalPropsContextProvider>
}

export default MyApp

There! Now our entire application can access the global props. Just as long as we remember to implement fetching on all pages, as shown in the next step.

Step 4 - Fetching on all routes

The above solution will only work as long as every route (including 404.tsx and 500.tsx) fetches the global props. It will not error out else, but will provide the default values for the global props which we do not want.

Luckily, refactoring components is easy.

Refactoring a component with no static props

Refactoring components with no static props is easy. It requires three changes (shown in the code below)

  1. Import GlobalProps
  2. Create an empty getStaticProps function with the GlobalProps.getEmptyStaticProps
// ORIGINAL

export default function Page() {
	return (
		<div style={{ padding: "8rem" }}>
			<h1>Home</h1>
		</div>
	)
}

// REFACTORED

// Step 1 - Import Global Props
import { GlobalProps } from '../features/GlobalProps/GlobalProps';

export default function Page(props: PageProps) {
	return (
		<div style={{ padding: "8rem" }}>
			<h1>Home</h1>
		</div>
	)
}

// Step 2 - Add an empty getStaticProps function
export const getStaticProps = GlobalProps.getEmptyStaticProps

Refactoring a page with static props

The process is as easy for page with static props. Similarly the two steps are:

  1. Import GlobalProps
  2. Wrap the getStaticProps function implementation with GlobalProps.getStaticProps
// pages/with-static-props.ts

// ORIGINAL

import { GetStaticProps, InferGetStaticPropsType } from 'next';

type PageProps = InferGetStaticPropsType<typeof getStaticProps>;

export default function Page(props: PageProps) {
	return (
		<div style={{ padding: "8rem" }}>
			<h1>WithStaticProps</h1>
			<p>Example value = {props.exampleValue}</p>
		</div>
	)
}

export const getStaticProps: GetStaticProps = async (ctx) => {
	return {
		props: {
			exampleValue: 1,
		}
	}
}

// REFACTORED

// Step 1 - Import Global Props
import { InferGetStaticPropsType } from 'next';
import { GlobalProps } from '../features/GlobalProps/GlobalProps';

type PageProps = InferGetStaticPropsType<typeof getStaticProps>;

export default function Page(props: PageProps) {
	return (
		<div style={{ padding: "8rem" }}>
			<h1>WithStaticProps</h1>
			<p>Example value = {props.exampleValue}</p>
		</div>
	)
}

// Step 2 - Wrap getStaticProps in GlobalProps.getStaticProps
export const getStaticProps = GlobalProps.getStaticProps(async () => {
	return { props: { exampleValue: 1 } }
})

Final step - Consume and enjoy!

There you have it. After every route has been given either the GlobalProps.getStaticProps or GlobalProps.getEmptyStaticProps function, we can finally do what we wanted to do in the beginning. Access the products in the header. And it becomes as simple as the following. Just access all global props with the GlobalProps.use() hook.

// components/Header/Header.tsx

import Link from "next/link";
import { GlobalProps } from "../../features/GlobalProps/GlobalProps";

export function Header() {
	const { products } = GlobalProps.use();

	return <header>
		<nav>
			<Link href="/">
				<a>Home</a>
			</Link>
			<Link href="/products">
				<a>Products ({products.length})</a>
			</Link>
		</nav>
	</header>
}

Final words

This provides a useful abstraction for some use cases, where a concept of global props is useful. And I could see it being used on a lot of different cases. I have worked on projects like the above example where I wanted each page to be aware of products. Similarly on my personal page I have blogs and projects to name a few as global props that are used page-wide as global props. And I imagine there are many more use cases for this.

If you use this, please do let me know. Similarly, if you have any improvements to this article, don’t hesitate to send me a message!

Gradient

💬

Let's chat!

Contact me about any work opportunities, projects, questions or feedback you may have. Even if you just want to say hi. I always reply within 24 hours, usually even faster!