How to Use Context with React Hooks
Mike Cronin
Posted on August 6, 2020
Context is probably my favorite React feature, especially when using hooks. It’s not bleeding edge tech anymore, so you should take a second to learn how it works. We’re just going to create a Context component, and then read/set values from it in our main app. It’s going to be a very simple project, but one that shows the basics and how you can build on it in the future. Here is the code on GitHub.
What is Context?
Context lets you have global properties and functions that can be accessed from anywhere in your project. This is pretty much what Redux does, and the best way to distinguish Redux from Context is size: Context is smaller and simpler. The model for a Redux Store is usually a intricate, immutable object, whereas with Context it would be more helpful if you think about it like a floating component that can talk to any other component. You also don't have to use reducers, which can also drastically simplify things.
Setup
Use create-react-app and that's it. We aren't going to have any external dependencies. We're going to create a Context component, give it an internal state, and then share that state with the rest of our app. All our app is actually going to do is save an input string to Context. I encourage you to read them all the same. It's good to know both the hooks and state version, since your company may not be using the latest React.
Step 1: Create a Context component
In src/
create a context/
directory and inside it put index.js
and ContextProvider.js
. Let's fill out index.js
first:
import React from 'react';
const AppContext = React.createContext({});
export default AppContext;
I'm going to explain that second line, but first let's also create ContextProvider.js
:
import React, { useState } from 'react';
import AppContext from '.';
const ContextProvider = ({ children }) => {
const [example, setExample] = useState('Hello there')
const context = {
setExample,
example,
};
return (
<AppContext.Provider value={ context }>
{children}
</AppContext.Provider>
);
}
export default ContextProvider;
Step 1a: What did we do
Alright, let's talk about createContext
and AppContext.Provider
. We actually create our Context in index.js
, this is the "raw" Context if you will. See, context itself is really just a value, but React incorporates it into its system and gives it Consumer
and Provider
components. Now, hooks let us bypass the need for a Consumer
component, but we still need to have a parent Provider component.
What our Provider
component does is take a value
(we're calling it context
, but it can be named anything) and then make it accessible to any of the children components. This value
is our global store. Also, if you aren't familiar with children
props, we'll talk about it in the next step.
Internal state
Notice what we pass into our context
value: it's a useState
hook and its accompanying value. That's the best part about this setup, we're simply tracking a regular component's state. When an external component needs to update the store, there's no magic, it's merely updating the internal state of the Context component. That change is then updated wherever it gets read, like a different version of props. There's nothing new here except where the data is being stored. You can of course add as much as you like to this object, but for now we're keeping it pretty bare bones.
Step 2: Plug your Context into your app
In order for Context to do anything, we need to make it available. Any child component of our ContextProvider
component will have access to the store. That means, we need to put it somewhere very high up in the component chain, so I usually put it at the top in the src/index.js
file:
import React from 'react';
import ReactDOM from 'react-dom';
import ContextProvider from './context/ContextProvider';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<ContextProvider>
<App />
</ContextProvider>
</React.StrictMode>,
document.getElementById('root')
);
This is also where the children prop in our ContextProvider
comes into play. Recall our return statement in our provider component:
return (
<AppContext.Provider value={ context }>
{children}
</AppContext.Provider>
);
By nesting out <App>
inside <ContextPrivider>
, our main app and all its child components are now the children of the <AppContext.Provider>
component. That's what actually lets our app get access to our Context and prevents unnecessary renders. Here's a quick article on props.children if you aren't familiar with it.
Step 3: Use your Context in a component
Alright, here we go! All we're going to make is a little form that lets us set the string value of example
in our Context. And we'll display it with the useEffect
hook and a console log. We're going to keep things simple and do it all in our main src/app.js
file:
import React, { useContext, useState, useEffect } from 'react';
import './App.css';
import AppContext from './context';
const App = () => {
const { example, setExample } = useContext(AppContext);
const [formText, setFormText] = useState('');
useEffect(() => {
console.log('context here: ', example);
}, [example]);
const handleChange = (e) => {
setFormText(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
setExample(formText);
};
return (
<div className="App">
<form onSubmit={handleSubmit}>
<label htmlFor="example">Example: </label>
<input
type='text'
value={formText}
onChange={handleChange}
/>
<button>DO IT</button>
</form>
</div>
);
};
export default App;
There's the whole thing, and here are the parts that use Context:
import AppContext from './context';
// ...
const App = () => {
const { example, setExample } = useContext(AppContext);
useEffect(() => {
console.log('context here: ', example);
}, [example]);
// ...
const handleSubmit = (e) => {
e.preventDefault();
setExample(formText);
};
return (
// ...
<input
type='text'
value={formText}
onChange={handleChange}
/>
We just feed our Context into the useContext
hook and then pull out the properties we want to use. The way you use those properties is pretty much the same as you would a useState
function or value. Remember the Context object is the one defined in index
not the ContextProvider
component, that's only ever used in a single place. This is surprisingly simple, but that's all thanks to hooks. They all work together seamlessly that Context really fits right in.
That's pretty much it
There used to be a bit more pomp and circumstance when using Context with class based components, but hooks make it like another useState (if you need class based components, check out Wes Bos's tutorial, I just adapted the pattern for hooks). And of course things can get more complex, like multiple Contexts or a useReducer
instead of useState
, but at it's core, it's a simple concept.
happy coding everyone,
mike
Posted on August 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.