loading...
Cover image for Sharing state using React's Context API

Sharing state using React's Context API

sunnysingh profile image Sunny Singh Originally published at sunnysingh.io ・4 min read

Global state in React is synonymous with libraries like Redux. If you ever needed to share state like the current route or data from an API with multiple components, then you may have reached for Redux yourself.

Newer versions of React (16.3+) include a built-in way to share state, which means not having to pull in an external library. This is known as the React Context API and it can be a bit tricky to learn. I hope to provide a simplified explanation and tutorial so that you can quickly add global state to any of your React apps.

The problem we're trying to solve

Before I dive into the Context API, let me describe a problem scenario first.

Let's say that we have a dashboard where a user can update their username. The username is displayed throughout the dashboard so that means the username will be stored in component state and then passed to other components via props.

Without the Context API, we'd have do something like this:

class Dashboard extends React.Component {
  state = { username: '' };

  render() {
    return (
      <div>
        <WelcomeMessage username={this.state.username} />
        <SettingsForm
          username={this.state.username}
          updateUsername={newUsername => {
            this.setState({ username: newUsername });
          }}
        />
      </div>
    );
  }
}

The username is stored in the state of the Dashboard component, and then passed via a username prop to both of the <WelcomeMessage> and <SettingsForm> components. An additional prop is passed to the form to update the state, which will then re-render the dashboard with the new username.

It's hard to see any problems with this right now. Consider what might happen though when we add more components to the dashboard that are deeply nested.

<Dashboard>
  <WelcomeMessage>
    <MessageList>
      <UserMessage>
        <p>Need to show username here...</p>

In this example I'm attempting to show that <UserMessage> is 3 component levels deep inside the dashboard. To pass the username down to it, we need to do what is known as "prop drilling":

<Dashboard>
  <WelcomeMessage username={this.state.username} />
    <MessageList username={this.props.username} />
      <UserMessage>
        <p>Hello {this.props.username}!</p>

This can get extremely tedious as we add more state and nested components. Plus, there's a chance that we need to access the username outside of the dashboard.

How to use the Context API

A solution to this problem is to use the built-in React Context API.

It allows you to avoid prop drilling, which means in our earlier example the <UserMessage> component will have direct access to the username state that was initially stored in the <Dashboard> component.

Note: A fully working example is available on CodeSandbox.

Create provider and consumer components

Let's start by creating a file for your context. I'll call it user-context.js.

In that file, add the following:

import React, { createContext } from 'react';

const UserContext = createContext({
  username: '',
  updateUsername: () => {},
});

export class UserProvider extends React.Component {
  updateUsername = newUsername => {
    this.setState({ username: newUsername });
  };

  state = {
    username: 'user',
    updateUsername: this.updateUsername,
  };

  render() {
    return (
      <UserContext.Provider value={this.state}>
        {this.props.children}
      </UserContext.Provider>
    );
  }
}

export const UserConsumer = UserContext.Consumer;

Let's break this file down.

First, the user context is created using createContext(). The values here will be overridden by the UserProvider.

Next, we create a UserProvider component that will serve as the parent component to hold and manage the shared state. Think of this as an equivalent to the <Dashboard> component in our earliest example.

Finally, we export a UserConsumer component which will allow components to access the shared state.

Using the provider

The <UserProvider> component needs to wrap around all components that share state. The simplest way is to add it in your main app component, which is usually the one that gets rendered to the DOM by React.

import React from 'react';
import ReactDOM from 'react-dom';
import UserMessage from './UserMessage';
import SettingsForm from './SettingsForm';
import { UserProvider } from './user-context';

function App() {
  return (
    <UserProvider>
      <UserMessage />
      <SettingsForm />
    </UserProvider>
  );
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

We are also importing two other components here: UserMessage and SettingsForm. These two components will be accessing the shared user state.

Using the consumer to read state

One use case for shared state is to display it. In this case, we'll display the current username. Create a file called UserMessage.js and add the following to it:

import React from 'react';
import { UserConsumer } from './user-context';

export default function UserMessage() {
  return (
    <UserConsumer>
      {({ username }) => <h1>Welcome {username}!</h1>}
    </UserConsumer>
  );
}

In this file, we've created a UserMessage component which displays a "Welcome username" message. The username is retrieved from the UserConsumer component that is being exported from user-context.js.

Inside of <UserConsumer>, we pass what is called a render prop. In this case, it's a function where we can grab parts our state and render something with it.

Using the consumer to update state

Another use case for shared state is to update it. In this case, we'll provide a form for the user to update their username. Create a file called UserSettings.js and add the following to it:

import React from 'react';
import { UserConsumer } from './user-context';

export default function UserSettings() {
  return (
    <UserConsumer>
      {({ updateUsername }) => (
        <div>
          <h2>Settings</h2>
          <label htmlFor="username">Username: </label>
          <input
            id="username"
            type="text"
            onChange={event => {
              updateUsername(event.target.value);
            }}
          />
        </div>
      )}
    </UserConsumer>
  );
}

This is similar to the previous example, except instead of grabbing the username we grab the updateUsername function to update it.

Overview

If you're confused at this point, I highly recommend that you look at the working CodeSandbox example which brings everything together.

Also, here's a quick overview of the main concepts:

  • A provider component wraps the entire app to manage shared state.
  • A consumer component is used to access or update shared state.
  • The user-context.js file exports both of these components and the shared state is stored in the <UserProvider> component.
  • The <UserMessage> and <SettingsForm> components read and update shared state by simply importing and using the <UserConsumer> component.
  • You can read and share state from anywhere in your app assuming that <UserProvider> is wrapping your entire app.

That's it. Feel free to use this feature to share state for navigation, modals, or even data. The power is in your hands 💪

Note: This article was originally written for my personal blog. I am republishing it here for the amazing DEV community.

Discussion

pic
Editor guide
Collapse
nickdevyyc profile image
Nick Hiebert

Great post, Sunny!

I'm understanding ReactJS code more the more I look at it. What I really like about this post is you're showing a complicated/confusing way of how a task can be done, then showing how to simplify it using a newer built-in tool (React Context API).

There's a good chance I'll run into a situation like this in the near future and this makes a good piece to refer back to.

It's a great time to be a web developer, lots of innovation and coding standards being improved.

Collapse
sunnysingh profile image
Sunny Singh Author

That means a lot to hear that Nick, thank you. I try to be empathic in my tutorials so that someone who is trying to learn something new is not frustrated over complicated wording or assumptions the writer makes of the reader's skills. Showing examples of how I personally did it "the wrong way" and then showing the simpler way I think resonates with a lot of developers.

Collapse
nickdevyyc profile image
Nick Hiebert

No problem at all Sunny. I'll let you know if anything comes up through my journey learning React .

Collapse
sunnysingh profile image
Sunny Singh Author

Also definitely let me know what parts of React you found difficult to understand while learning it. I would love to tackle those topics in future articles.

Collapse
lexswed profile image
Lex Swed

Seems like it's common to avoid Redux :D
I had exactly same thing: dev.to/lexswed/building-global-sto...

Collapse
simoroshka profile image
Anna Simoroshka

I am also making my own redux-like state management with context (and hooks) 😀

Collapse
leob profile image
leob

But I still don't see why people say that Redux is complicated, the basics of it are pretty simple. If the problem is "manage shared or global state" and Redux is a well-known solution (and not that hard to understand or implement), then why reinvent the wheel.

Especially since the alternative solutions (based on context or whatever) also tend to increase in complexity with time, and finally you end up with something like ... Redux!

(if you really want a dead simple solution then just import a Javascript module and keep your state there)

Collapse
lexswed profile image
Lex Swed

to learn and evolve. yes, redux is super simple. react-redux is a bit more complex. redux-thunk/sagas are essentials for good business logic split. but at the end you forget where you need to write this piece of code and where you have to. redux is awesome solution - look at how long it is there and it hasn't changed much. but we are much more powerful now, then 3 years ago.

Thread Thread
leob profile image
leob

Yes, sometimes it can be refreshing to try something new or use an alternative way ... if we didn't think like that then we'd still be doing COBOL with punch cards. On the other hand, I sometimes have the feeling that we're going around in circles and essentially end up where we were before :-)

Collapse
sunnysingh profile image
Sunny Singh Author

Haha yeah. I think most people jump on the Redux hype train, realize that they over engineered their apps, and then look for simpler options.

I like that your post goes more into how to create a store. I find the context API hard to understand at first so I decided to make a basic intro into using it.

Collapse
leob profile image
leob

Folks, maybe this is the solution ... medium.com/swlh/no-boilerplate-glo...

Collapse
leob profile image
leob

Good explanation ... I believe the new "Hooks" API also supports Context, see this article: dev.to/oieduardorabelo/react-hooks...

However, I wonder in which way this is really simpler than using Redux. If you have a limited amount of "shared" data (user is a good example, another one might be a shopping cart) then Context seems to be an attractive approach as it's arguably a little bit simpler than Redux.

However if the number of 'shared' items expands beyond a few then you'll end up with a large amount of 'wrappers' (providers and consumers) al over the place, because what you don't want to do is lump all your shared state into one context provider (if you do that then you're just re-implementing a Redux store in a funny way).

In other words, I have the feeling that "Redux" lets me scale my application better as it grows in complexity.

So, in which scenarios would you recommend using Context rather than React? Both seem to achieve more or less the same goal.

Collapse
sunnysingh profile image
Sunny Singh Author

Thanks leob.

Regarding Hooks, they're fairly new so I wanted to keep the tutorial compatible with most people's codebases. I'll probably do a follow-up for Hooks in the future, but Eduardo's post looks great.

Regarding Redux, I don't think that you should never reach for it, but don't reach for it too early. In one of my recent apps, I decided to forego Redux entirely and the lack of architecture provides flexibility in deciding how everything will evolve. Perhaps Redux or another state library is the solution, but the codebase isn't big enough for me to worry about it yet.

I personally recommend using local state and context until you feel like you need something more powerful or structured. If your app is very complex and also has a large team, then Redux may be a good option for that. For most "non-Facebook-level" apps though, I think staying with context or using a simpler state library built upon context (e.g. Laco, Unstated) will work out really well.

Collapse
leob profile image
leob

I agree with you to keep state locally within the component, if that piece of state is only needed within that component. So don't make state "global" or centralized if it's needed only in 1 place. No disagreement there.

However ... I didn't know how the Context API works, but now that I know, it I wonder in what way it's really simpler than Redux. If I I understand it correctly, it's just an alternative way to have "global" or centralized state which you can access wherever you want, do I see that correctly?

In that case I would argue that conceptually it isn't really that different from Redux, only "syntactically" and in its mechanics, the way you program it - what it achieves it isn't that different. In the end both sort of look like a glorified way to inject a singleton object with some variables and getters/setters.

In other words, I don't really see the point, unless it cuts down a lot on "boilerplate". Redux also isn't that hard to understand or use, and it's a well known pattern. I'm never really a big fan of "10 different ways to do the same thing", it makes it a lot harder for (new) developers to understand an unfamiliar codebase.

(this is also why I'm generally a fan of the Vue framework - Vue is more opinionated and "batteries included" and mostly advises one recommended way of doing something)

Actually I could make the opposite argument: if you start out with "Context" because your app is 'simple', and later on you realize (as you said) that you'd better use Redux because your app has become more complicated, then you'll have to rewrite all of your context-based code to Redux. Then why not start with Redux right away? Especially because it isn't that hard, at least in my opinion.

So yes, I agree with you when you say "don't reach for Redux too early", but the same applies to Context - don't reach for Context too early. As long as you can manage state locally within components, or you just need to pass it down 1 or 2 levels (via props) then use neither Redux nor Context.

Once you realize that you do need centralized state, then use Redux or use Context, whatever floats your boat (but as I said I don't really see how Context is that much simpler than Redux).

Thread Thread
sunnysingh profile image
Sunny Singh Author

Ah I see what you mean.

Yes you're correct, there is no difference in what Redux and Context API are made to do.

I also definitely agree with local state. Don't reach for Context or anything global if your components don't need that yet.

The topic of whether Context is actually simpler or if it's better to use a well-known library like Redux is for another discussion I believe. There's also different ways of implementing Redux so I think even if I make some points against Redux, it may not actually apply to every project that uses Redux.

Collapse
dmikester1 profile image
Mike Dodge

Sunny, excellent article on context! This is something I have been struggling with a lot lately. I'm running into an issue where I'm fetching data from an API and loading it into context to use in my app. But my app tries to use the data before it's available and getting 'undefined' errors. Any ideas how to overcome this? Looks like miodrag is having a similar issue.

Collapse
sunnysingh profile image
Sunny Singh Author

Thanks Mike! It's ok for the data to be undefined before it's available, that's how you are able to detect loading states. I suggest you return some default content or a loading spinner in your component's render so that the rest of the component does not try to reference something like data.user.name before it's set.

Collapse
srbin12 profile image
miodrag

Hi, do you wish help me in next problem. My code:

function noviData() {

fetch("http://localhost:3000/posts.json")
    .then(response => {
        return response.json();
    })
    .then(data => {
        console.log("parsed json of function noviData", data);
        //alert([json.name, json.id, json.email]);
    })
    .catch(function(ex) {
        console.log("parsing failed", ex);
    });

}
How can use data of this function from some another function or component or const or context?
If you have some advice or tutorial examples.
Regard
Mr.Miodag

Collapse
mikakaakinen profile image
Mika Kaakinen

An excellent article! Thank you, Sunny!

Collapse
tmodurr profile image
Tom Moran

Hey Sunny,

Nice article and working on refactoring a simple app away from prop drilling.

In your example, you're only managing a single state attribute but to use another example, what if I needed to track 15 attributes/object in global state. Is the expectation that I would need to declare the 15 object properties and associated update functions in both the user context and the user provider? Are there other techniques where I could create a single object and then update that object on a property by property basis (aka nested object compared to a single object property)?

I know there's a valid reason, I'm just failing to grasp why we have to define the UserContext props and methods in the UserContext object. Is that essentially enforcing the structure that get's implemented in the provider? Basically wondering why there is this "write twice" approach.

Collapse
obsessivecoder profile image
Jared Huffstutler

As an experienced programmer and web developer, I found this article extremely useful for understanding the basic concepts of the context API. It is also "simple" enough for beginners. This article is thorough, complete, and explains things very well. Thank you and great job!

Collapse
andresmgsl profile image
Andres Guerrero

Thanks man. Really helped me!

Collapse
whytekieran profile image
Ciaran Whyte

Hi Sunny, great article, thanks

Collapse
srbin12 profile image
miodrag

I am use your sample and discover how can have more inputs in app. But, and how can use sample for make message, there have text, image, and user name