Sharing State with Context in React
Hey! Just so you know, this article is over 2 years old. Some of the information in it might be outdated, so take it with a grain of salt. I'm not saying it's not worth a read, but don't take everything in it as gospel. If you're curious about something, it never hurts to double-check with a more up-to-date source!
Ever since I first started playing with React, I commonly came across the issue of state management, which involved the use of prop drilling and spaghetti code.
If I'm being completely honest, looking back on it, my first React applications were a real mess. Yes, they worked but they are no fun to maintain or work on.
For those unfamiliar with prop drilling and the joys that come with shared state management between components in React, you are lucky.
To best illustrate what prop drilling is and the issues that come with it, let's take a basic example.
A Header Navigation Toggle Example#
Every website or at least every modern website has some form of navigation toggling on mobile devices.
If you have 6+ items in your header navigation, you may hide these on mobile behind the action of a hamburger menu button.
Seems pretty simple to build, right?
We have our header component and a navigation component.
Within our header component, we use the React Hook, useState (available as of 16.0). With this, we create a boolean called 'showHeader' and a function called 'setShowHeader'which allows us to pass a boolean to flip between showing and hiding.
When the state is set to true, we show the header, and when it's set to false… we hide it!
By creating a button in the header component we can modify the state on click.
So far so good, but next we need to feed the current state to the navigation component, as well as the function to set the state to the navigation component. This is because we will want a button in the navigation on mobile to close the navigation component. Without this, the navigation overlay would block the header button and we'd be stuck with the navigation showing.
To pass the state and the function, we'll need to do this using props.
Passing information one level down in React isn't uncommon and not a completely bad practice, but when you have to feed the data or function 2 or 3+ levels deep then you're practising prop drilling.
The reason this becomes troublesome is that moving any components position in the project structure or changing props in one component can break logic within your application. This inevitably creates a balancing act which becomes unmaintainable.
So we know our problem, what's the solution, Jack?
React context is our solution queue choir chanting
React context is a way of managing and setting application state that isn't restricted to one component, has no need for prop drilling and can be created in a couple of lines of code.
For anyone familiar with Redux (I'm not at all), I've heard experienced React developers compare React context to Redux. You have reducers, state and dispatch payloads to the state.
For anyone not knowing what I'm talking about, no biggie, I'm about to break it down for you.
Think of React Context as a global wrapper (It quite literally becomes one) that stores application state. This could relate to whether or not your header navigation is showing, a filtered search result, the meta-information related to a logged-in user, literally anything related to your app.
Then with the use of React's 'useContext'hook we can tap into this global application state and feed the data into our component of choice.
Not only that, we can create dispatched actions which will mutate and update the global state.
You can create predefined dispatch actions like 'filter search'and call on it with no parameters.
Alternatively, you can create actions like 'setState'where you accept arguments to be used in setting the global state (toggling isShowing between true and false).
This All Sounds Great, But Where's the Catch?#
Well unlike React 'useState', we'll need to set up a couple of files instead of the single component. Not to mention that some of the hooks, practices, and overarching concepts are just above beginner level for React developers.
In other words, you'll spend more time setting it up. However, in the long run, you'll be saving yourself potentially hours of work in the future and your code will be just that much nicer to work with and maintain!
Stop selling me on it, show me some code.
An Alternative Header Navigation Toggle#
Alright, let's take this Header navigation example that we hold so close to our hearts and see how we can refactor it to use React context.
So we have three key files that we'll explore -
- Header.jsx (The logic we explore in this applies to all components)
Our application context component will contain our general application state, as well as functions to mutate the state when we want to do so in components.
To create our general state, we need to import 'createContext'from React.
Next, we create a name for our context and assign the constant/variable with the createContext function with a value of null. The name you pick will be important as you will literally use this name to import the state and dispatch events in all components, later on, so choose something sensible.
Then finally we export this named context from the component.
Next, we will create a base template component to use in our application.
Think of this as the foundation set up for all future pages, in my example, I've included the header and footer as I will want this included on all pages and saved myself the need to import it in each file, instead, I import the base template and it brings my components along for me.
To start off the file, we are importing the 'useReducer'hook from React and the application context component we just created.
A reducer is a way of managing and mutating state based on the current state and action or 'dispatch'event which we will be digging into within our component files.
In this example, I've created a dispatch event which can be best thought of as a function. This event or function is named 'toggleHeader'and uses the same logic as our 'useState'example, where we are toggling a boolean between true and false.
My event first creates a new variable duplicating the values found in the current state value, it then inverts the current isShowing value and returns the new state variable.
Finally, we create our base component template.
Our component accepts children as a prop, which allows us to render whatever HTML/content we want to between the header and footer components.
Next, we call on the 'useReducer' hook we imported at the start of the file. The useReducer hook accepts a reducer which we've just defined above, a holder of events and functions to process and mutate state.
The second parameter it accepts is an initial state for the application context. By default we want the navigation to be hidden so we set isShowing to false on initial application load.
Finally, we return an ApplicationContext.Provider component with the initial state and dispatch events from the useReducer hook we've just defined.
What this Provider component allows is for any children components to use hooks and the import of the ApplicationContext file to tap into the current values found in state and access to the dispatch events we've defined in our applicationReucer.
If this doesn't make sense now, it will when we've finished our final file, so stick with me!
Finally, we export the base template component to be used on any page we create.
Header / All Components#
Ok, we're nearly there, the pay off of using context!
To start our file, we import the hook 'useContext' instead of our usual 'useState'hook.
Next, we import the application context that we defined in our first file. This file holds the state and dispatch events.
Next, we go about creating our Header template.
To access the current state value and dispatch events, we will use the 'useContext'hook and pass in the value from ApplicationContext.
Now we have access to the initial values defined in our reducer and all of the actions in the form of two constants 'state' and 'dispatch'.
We can destructure the isShowing value from state immediately and have access to the current boolean value in our application.
Now we have our context application using the same logic our initial useState example did.
So now we have our context, we can extend the state of our application without passing state or functions as props. Instead, we call on them as and when we need them with an import and the use of a React hook.
You may have noticed the setup and extra files contributed to a longer time to create a basic example. So why would we want to set up our application using this method?
Well, let's take the introduction of another component.
The footer component.
If we wanted to share the current state of the header toggle 'isShowing' value to the footer component, with 'useState'we would need to define the value at the root of our project and prop drilling it to both the header and footer components.
With our 'useContext'example, within our Footer, we import the context hook and application context file and immediately have access to all of the state values and dispatch events without prop drilling.
Again, if you've ever come across prop drilling you'll understand the benefits more so than those who haven't. I invite you to fork my example and play around with the files to become familiar with the possibilities the new method offers.
I've been using React for 1-2 years now and would say this single bit of knowledge has helped me develop clean applications with easy to manage codebases. I've even hit some interesting use cases where reusable components unrelated to each other can be quickly modified to use one another with the global state and dispatch events tieing them together.
Thanks for reading, and please share your built examples with me on Twitter @whatjackhasmade