Using React Context API in NextJS Applications
Global state in React has always been an area that has been a bit hit and miss. At the start there was the concept of passing props down the component hierarchy, and using callbacks as props to get data back up the chain. As you can imagine, this quickly got messy, and led to the development of third-party global state management tools such as Redux and MobX. It stayed this way for a number of years, until developers started to tire of the boilerplate needed for such state-management tools, and were looking for something native and easier to work with. With that, came the React team, and the introduction of the context
API.
For those unaware, the context
API is a way of managing contexts and variables in React, which also includes the global state. The essence of the context
API is that the developer defines the context, and then passes it into a wrapper object (usually around the entry component itself) which allows child components to directly get and set data within the context object. This works extremely well for single-page React applications, but unfortunately starts to come undone with isomorphic frameworks such as NextJS.
Before we start to get to the details of why this doesn't play nicely with NextJS, let’s begin by reminding ourselves briefly about how global state is managed in React using the context
API. Taking many cues from tools such as Redux, we usually define our global context in a seperate file, which is often called something like store.js
. Let’s have a look at a typical implementation:
import React from 'react';
export const Context = React.createContext();export class Provider extends React.Component {state = {
name: 'My Name'
}render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}}
As you can see, a context has been created using React.createContext()
and this component is exported with the name Provider
which wraps whichever children come below it. Some basic initial state has been provided to the context, which is essentially just a name for demonstration purposes.
In a typical React application, we can simply wrap our root component in this provider component, and then access the context as follows:
import React from 'react';
import { Context } from 'store.js'export default class Test extends React.Component { static contextType = Context render() {
return <h1>{this.context.name}</h1>
}}
This simple component can then import the context from the store.js
file, and then consume it with the static
property contextType = Context
. This then enables the component to access the context directly as this.context
in a similar way that a component would access this.state
or this.props
.
So why does this not work so well in isomorphic frameworks such as NextJS? Essentially it comes down to the fact that the actual root component is abstracted away from the developer. NextJS follows a different paradigm to usual React applications, and structures the application into individual pages. This confuses many developers when it comes to implementing the context
API, as it is assumed that we need to wrap each individual page in the provider
component. This of course doesn’t work, as each time a different page is loaded in the browser, the context is reinitialised. So what exactly is the solution?
It turns out that the root component is accessible in NextJS, it just isn’t exactly obvious. In keeping with the NextJS pages
paradigm, the developer needs to create a new page called./pages/_app.js
The format of this _app.js
file needs to be consistent with NextJS’s API, as-per the following example:
import React from 'react'
import App from 'next/app'
import Head from 'next/head'
import { Provider, Context } from 'store'export default class MyApp extends App { render() {
const { Component, pageProps } = this.props;
return (
<div>
<Head>
// stuff.....
</Head>
<Provider>
<Component {...pageProps} />
</Provider>
</div>
)
}
}
By extending NextJS’s App
component in this way, we are now able to wrap our persistent root component in our global state provider using React’s context API and ensure that the global state will be persistent regardless of navigation through different pages.
Needless to say that React’s context
API has gone a long way in terms of simplifying the developer experience with global state management. Perhaps it isn’t quite as in-depth as tools such as Redux with their corresponding dev tools and mandatory use of actions / reducers (which can also be configured with the context
API if needed), it certainly has moved the React ecosystem forwards for numerous developers.