Using React Context, the Right Way

Photo by Ferenc Almasi on Unsplash

*Feel free to move on to the next part if you already know what Context is.

So what is Context? Context is well, context about your app. Or simply, it is your app’s global state. Consider, that when you use useState (or this.state if you aren’t using hooks), you are more or less setting a local state that defines that component it is set in. But what if we needed that state in a component that is a deeply nested (or even just a few steps down)?

Before Context (or Redux), the approach was to prop drill. i.e. pass the state down from child to child until it got to where it was needed, and it made everything harder. And now we have Redux…and now there is Context (which, imo, is better than Redux in the sense that you don’t need an additional library to work with a global state). Now you no longer have to prop drill, you can just pass it directly to the components that need it!

So how do we use React Context?

iRobot — That detective, is the right question
  1. Create a blank React App

To start off, let’s get a blank React project started using npx create-react-app (or whatever your preferred method is). You could use your existing project, but for the sake of not accidentally making a mess, I’d recommend starting anew and then implement what you learn in your own project. This is usually how I setup my React project file structure:

File Structure

2. Create context file

After you have created the React app, create a directory called state, which is the folder I have selected in this screenshot. Next, create a file called Context.jsx inside. This can actually be called whatever you want, but make sure it’s titled in a way you know that it is your context. I highly recommend putting ‘context’ somewhere in the name.

Context.jsx

If you wonder why I’m using JSX file extensions, it is because it is proper to use the jsx when your file contains JSX elements. It makes it easier to distinquish from a file with regular, vanilla JS and that of a React component. If you are using Typescript it would be TSX, or TS (for vanilla Typescript code).

3. Creating the Context

To create a context, first we must import createContext from react and then make a variable assigned the createContext function, passing it an empty object. createContext() expects at least one argument defining a default variable. If you have global states that you don’t want them to change you could put them here and exclude them from the next part (I don ‘t think they’d be overridden but if they are let me know).

import { createContext } from 'react';

const Context = createContext({})

Lastly, export it as default.

export default Context;

Our finished Context should look something like this:

Context.jsx

4. Create the Provider

Where there’s context, there’s state. Wait a minute, isn’t context our state? Well no, not exactly. You could technically get the context and it would return its default value, but there’s one problem…you can’t change it. That kinda really makes the context useless unless the context is truly constant and won’t change through the lifecycle of your app.

Now, create a file called Provider.jsx, and import the Context and useState from react.

import React, { useState } from 'react';
import Context from './Context';

Create the Provider component:

const Provider = ({ children }) => {}

Now let’s make the global states.

const [coords, setCoords] = useState({lat: 0, lng: 0});

const value = {
coords,
setCoords
}

You’ll also see that I made an object shorthand with our newly made states. This will make it a little easier to pass them to our provider, but you don’t have to do this, you could just pass each state directly to the provider. I’d not recommend that as it would make things hard to read. You could create different objects if you want to organize your states into categorized objects, though.

Now let’s provide our children some food…erm, context.

return (
<Context.Provider value={value}>
{children}
</Context.Provider>
);

To understand what is going on here, every context has a provider object that contains all the contexts supplied to it. Here we are passing the structured object we created the states we set. This will modify the Context values which will then subsequently be passed down to its children prop.

The children prop is something provided by react that contains all the children of a react Component. In this event, all the children of the Provider.

This is the end result:

Provider.jsx

5. Provide our app with our context

Now technically, you can provide where ever you need the context, and you can also have more than one provider. But for this lesson, we’ll be exposing the entirely of our app to them.

Open up your index component, and import the Provider Component.

import Provider from './state/Provider';

Now as the default React.Strict causes a warning on our console, I remove the <React.Strict> fragment to prevent the need to go through the undertaking of fixing that warning.

ReactDOM.render(
<Provider>
<Router>
<App />
</Router>
</Provider>,
document.getElementById('root')
);

Now wait, what? I don’t have Router in mine! I’ll explain why we have this…go ahead and import Router from react-router-dom (you’ll need to install the package as well) and include the Router component as I did.

As for providing our app with the Context we just created? well…

So now that you got Context all over your app, it’s a bit pointless if you can’t change it right? At least I think so anyways. I want to be able to change between light and dark mode on all the pages once and keep it that way, damn it.

So at this point, you probably already know how to create pages and navigate between them. At least I hope you do, because this tutorial isn’t for that and I’m not going to lay out those steps, sorry…(If you’re super nice maybe I’ll write a tutorial on React Routes, Switches, and Links).

So let’s do this. Create a simple form page with two inputs and of course you’re submit button (you could also have a local state to hold the values of your two inputs, and a useEffect to change your context, if you needed something like that). This is what my file structure now looks like:

So first, let’s learn how to retrieve the Contexts we supplied our children. Open up your Home page and import useContext from React. Let’s also include useEffect as well, like so:

import React, { useContext, useEffect } from 'react';

Now, we create a constant that will contain the coords context (or subsequently whatever contexts you need).

const { coords } = useContext(Context);

Now let’s add our useEffect and console.log our coordinates when the page loads.

useEffect(() => { console.log(coords) });

Try it out and you will notice that it logs {lat: 0, lng: 0}. Perfect. This is my Home page setup:

Home.jsx

So now to change it. There’s many ways we can do this, but for coordinates, let’s create a simple form with two inputs and a submit button. This is what my form looks like. Nothing stylish, just a simple, poorly accessible form:

ChangeContext.jsx

The Change.jsx you saw earlier simply renders this component.

So you will notice that When you click submit, you’ll get a console.log with your new coords. But if you go back to your home page, you’ll notice that they’re reset back to their default state. That’s the nature of React. Once you navigate back to your home, the form page derenders and your homepage derenders when you navigate away.

But, it is actually changing! For proof, import the Context into your App component, and console log the coords there. Navigate to the form you created and change the coordinates. You will see two logs now. One from your App (which is always rendered) and one from your form page. So if it doesn’t persist, what is the true point in context?

Well, unlike this simple React App, your React App will probably be more complex with a lot more components embedded in each other and rendered on screen. The true idea of Context is that you can set it on your parent component (the page that is) and pass it down to those deeply nested child components, without having to prop drill.

If you want the Context to persist across rerenders you’d want to use localStorage or sessionStorage. (you’d set the default context value to something like sessionStorage.getItem(‘item’). You’ll want to set it at a level higher than where you’re providing the Context and make sure that it doesn’t load until storage is set. Depending on your use case, you may just want to make a global variable with the value of that sessionStorage in the scope of component you want it in.

You can also look into Redux. I have yet to use it, but one of its features is state persistence.

And that my friends, is how you do React Context. I hope you find this useful.