loading...

React Hooks: How to create and update Context.Provider

oieduardorabelo profile image Eduardo Rabelo ・4 min read

If it is your first time hearing about "React Hooks", you can watch the React Conf introduction talk about it. It is worth it!

I'll not spend too much time explaining the new API, for that, you can go to their docs. React team did a amazing work explaining all the whys and how they got there.

Getting Started

Everything is better with a hands-on example, let's start it with:

$ mkdir react-hooks-contex-provider
$ cd react-hooks-contex-provider
$ yarn init -y
$ yarn add react@^16.7.0-alpha.0 react-dom@^16.7.0-alpha.0
$ yarn add parcel-bundler

With this boilerplate, we've:

  • React in alpha version with all hooks use* available
  • Parcel Bundler to run our local example

Let's add our HTML file:

$ touch index.html

Add some HTML:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>React Parcel</title>
</head>
<body>
  <div id="root"></div>
  <script src="./src/index.js"></script>
</body>
</html>

As we can see in our HTML, we've a ./src/index.js file, let's create it:

$ mkdir src
$ touch src/index.js

Add some JavaScript:

import * as React from "react";
import * as ReactDOM from "react-dom";

import { ContextOneProvider } from "./ContextOne";
import { App } from "./App";

function main(target, container) {
  ReactDOM.render(target, container);
}

main(
  <ContextOneProvider>
    <App />
  </ContextOneProvider>,
  document.getElementById("root")
);

Nothing different here. We have our familiar ReactDOM.render rendering a component called App wrapped in a context called ContextOneProvider.

Creating our ContextOne file

A follow up from our ./src/index.js can be our ./src/ContextOne.js, let's create it:

$ touch src/ContextOne.js

And use the code snippet below:

import * as React from "react";

let ContextOne = React.createContext();

let initialState = {
  count: 10,
  currentColor: "#bada55"
};

let reducer = (state, action) => {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return { ...state, count: state.count + 1 };
    case "decrement":
      return { ...state, count: state.count - 1 };
    case "set-color":
      return { ...state, currentColor: action.payload };
  }
};

function ContextOneProvider(props) {
  // [A]
  let [state, dispatch] = React.useReducer(reducer, initialState);
  let value = { state, dispatch };


  // [B]
  return (
    <ContextOne.Provider value={value}>{props.children}</ContextOne.Provider>
  );
}

let ContextOneConsumer = ContextOne.Consumer;

// [C]
export { ContextOne, ContextOneProvider, ContextOneConsumer };

We have some new faces here, eh? 90% of the code is quite familiar, let's examine items [A], [B], [C].

  • [A]: We're using the new React Hooks API here, called useReducer. If you’re familiar with Redux, you already know how this works. It will return the state object and a dispatch function to send updates to the store state. We're creating a value object with both and we'll send it to our item [B].
  • [B]: Here, we're using our context provider to inject the value object, making it available to all consumers. Previously we saw that we're using it to wrap our <App /> in ./src/index.js, meaning, all children components from <App /> would be able to pull out this context to use.
  • [C]: At first look, this export is odd. We're exporting the default context object created by React, ContextOne, our custom provider, ContextOneProvider and an alias to the consumer key, ContextOneConsumer. To use the new Reactk Hooks API for context, called useContext, we need to pass the default object created by React, our first export. The second export, ContextOneProvider, is our custom provider, where we need to use it to inject what we want in our app context. The last export, ContextOneConsumer, is just a convenience to subscribe to context changes, this is a stable feature from React.

Creating our App file

Last, but not least, let's focus on our ./src/App.js file:

$ touch src/App.js

And paste some JavaScript:

import * as React from "react";

import { ContextOne } from "./ContextOne";

export function App() {
  // [A]
  let { state, dispatch } = React.useContext(ContextOne);

  // [B]
  React.useEffect(
    () => {
      document.body.style.backgroundColor = state.currentColor;
    },
    [state.currentColor]
  );

  // [C]
  let inc = () => dispatch({ type: "increment" });
  let dec = () => dispatch({ type: "decrement" });
  let reset = () => dispatch({ type: "reset" });
  let setColor = color => () => dispatch({ type: "set-color", payload: color });

  return (
    <React.Fragment>
      <div style={{ textAlign: "center" }}>
        <p>
          Current color is: <b>{state.currentColor}</b>
        </p>
        <p>
          Current count: <b>{state.count}</b>
        </p>
      </div>
      <div style={{ paddingTop: 40 }}>
        <p>Count controls:</p>
        <button onClick={inc}>Increment!</button>
        <button onClick={dec}>Decrement!</button>
      </div>
      <div>
        <p>Color controls:</p>
        <button onClick={setColor("green")}>Change to green!</button>
        <button onClick={setColor("papayawhip")}>Change to papayawhip!</button>
      </div>
      <div>
        <p>Reset changes:</p>
        <button onClick={reset}>Reset!</button>
      </div>
    </React.Fragment>
  );
}

Woww, how about now? Again, 90% of the code is familiar, let's examine the other 10%:

  • [A]: We're using the new React Hooks API called useContext to consume our context (attention here, we're importing our ContextOne object, created by React). When the provider updates, this Hook will trigger a rerender with the latest context value.
  • [B]: Another new React Hooks API called useEffect. Think of effects as an escape hatch from React’s purely functional world into the imperative world. Any mutation, subscription, timers and other side effects you can use this hook to do it. As first param, we're passing a function with our effect, change the body background color. As second param, we're passing a array, this array is telling react to "hey, just run my effect when these props/values has changed".
  • [C]: Plain JavaScript arrow functions, but, worth notice, we're using the dispatch function from our context to update our provider.

Running our example

Now, we've reached the real deal, let's run our example:

$ yarn parcel index.html

You should see something like:

Open your localhost:1234:

Conclusion

React Hooks API is extremely powerful. The community on Twitter is on 🔥. We already have great examples on GitHub.

What do you think? Are you hooked? :P

Discussion

pic
Editor guide
Collapse
jeyloh profile image
Jeyloh

Huge help! But I have a question regarding actions. The dispatch functions in App.js:
let inc = () => dispatch({ type: "increment" });

How can we move these in a separate function outside of the React component? I'm playing around with your example, and the Hooks/Context docs, but I'm still to find a nice pattern.

I wanted to move out my functions from App.js and useReducer inside these functions, before I realized Hooks are only supported in custom hooks and components.

Can I pass state and dispatch to the actions to use them, or name the actions like this "useAction1", "useAction2" etc. and then call these from the React component? Seems like an anti-pattern.

The reason I want to move them is to reuse actions from many components and only update the reducer state instead of returning data. My example consists of multiple API calls, localStorage etc.

Cheers, again - huge help with this article :)

Collapse
nickempetvee profile image
NickEmpetvee

Great tutorial. I've run into an issue. Would you mind providing a little help? I've copied your code line for line, but into an environment created by create-react-app. Code here: codesandbox.io/s/3xxykj5wjq

The error I get is:

TypeError
React.useReducer is not a function
ContextOneProvider
/src/ContextOne.js:25:32
  22 | 
  23 | function ContextOneProvider(props) {
  24 |   // [A]
> 25 |   let [state, dispatch] = React.useReducer(reducer, initialState);
     |                                ^
  26 |   let value = { state, dispatch };
  27 | 
  28 |   // [B]
Collapse
nickempetvee profile image
NickEmpetvee

Solved! I was on an earlier version of React.

Collapse
eecolor profile image
EECOLOR

It seems your tutorial introduces the caveat described here: reactjs.org/docs/context.html#caveats

This causes all consumers of the context to re-render each time.

Collapse
jmbl1685 profile image
Juan Batty

Great!! this helped me a lot, thanks.

Collapse
antonio_pangall profile image
Antonio Pangallo

Hi Eduardo,
I would like to get your opinion on:

github.com/iusehooks/redhooks

It reimplements redux's api using Hooks and Context.

Thanks.

Collapse
domitriusanthony profile image
Domitrius

Great reference! Thanks Eduardo.

Collapse
neillindberg profile image
neillindberg

This was a great help!
First step: Forget everything from the introduction to Hooks as a less complex way to create a singleton of sorts, per the React Docs.
Second step: Redirect to Eduardo's intro.

Collapse
visarts profile image
Brendan B

Could you show examples of how to use ContextOneConsumer? Or does the new useContext api eliminate the need for the Consumer?

Collapse
danielkocean profile image
DanielKOcean

Seems you are right, "useContext accepts a context object (the value returned from React.createContext) and returns the current context value, as given by the nearest context provider for the given context."
reactjs.org/docs/hooks-reference.h...

Collapse
oieduardorabelo profile image
Eduardo Rabelo Author

thanks for your time Richard and indeed we already have some abstractions like github.com/siddharthkp/storehook with a similar api