Getting Started with React useContext Hook and React Context

React context makes it easy to create globally accessible data, and states. The useContext hook allows you to work with React contexts from anywhere and pass its data throughout your app. This tutorial will show you how to create new context, how to get values from it and how to change them.<!--more-->

React context, global states, prop drilling

When you work with data they are usually one of two types, global or local. Global can be accessed from anywhere. Local only from the place where they are defined, and down the tree. This also applies to states. You can have global states and you can have local states. Which one is the best choice depends on situation.

React context API makes it easy to create these global states. That said, there is one problem with these global states. They are often difficult to use in nested components. It can take a lot of prop drilling to get the data from the top to where you need them. You may have to pass these data through multiple components.

One way to solve this is making those data local. However, this would lead to duplicate code. It would also go against the idea of having one source of truth that is globally accessible. Another solution is to skip all prop drilling and simply reach to the context from the component where you need those data.

This is the goal of the React useContext hook. The React useContext hook promises to help you with two things. First, to help you reach out to any context and from anywhere. Second, to work with values exposed through this context. This includes both, getting those values as well as changing them. Let's take a look at how it works.

The context

Using React context requires getting done few things. First, you have to create a context. You achieve this by using createContext() method shipped with React. This context will be the global state available for use across the app. Well, at least one them because your React app can contain infinite number of contexts.

// context.jsx

// Import createContext() method from React:
import { createContext } from 'react'

// Create new context:
export const newContext = createContext()

Notice that we are declaring the context as empty, basically assigning it undefined. Don't worry. This doesn't mean this context will be empty forever. It will be empty just for now when you create it. Later, in the next step, you will add values to it. Also notice that we are exporting the context.

The reason for this is simple. The useContext hook accepts a context as a parameter. So, if we want to use the useContext hook to access the context anywhere in the app the context itself must be also accessible anywhere. This means we must export it from where it is.

The context provider

The second thing you have to do is to create a provider for your new context. This provider is a component that provides your app with the value(s) stored inside the context. Provider wraps all components that should be able to access the context. This is important to remember.

Components will be able to communicate with provider only if they are provider's children. It doesn't matter where in the component tree they are. What matters is that the provider is used as a wrapper somewhere in the tree above. In general, provider is used as a wrapper for the entire app.

This guarantees that any component in the app will be able to communicate with the provider. If you have multiple providers, you can wrap one inside another while keeping the app as the last child. This will ensure the app has access to all providers up the tree. Now, let's create the provider.

Creating the context provider

Creating the provider is similar to creating a regular React component. Nowadays, provider is usually created as a function component. You give this component some name. It is a good practice to end the name with "Provider". It makes it easier to understand the code when you read it.

Inside this component, you can use any react hook you want. For example, you can use useState hook to create new state for the provider. You can then expose this state by setting it as a value for the provider. This will make it available for any component wrapped with the provider.

You can also use useCallback hook to create memoized functions. These functions can work with the state, update its values. You can also expose these functions by setting them as values for the provider. Again, this will make them available for components wrapped with the provider.

The most important part is where the rendering happens, what follows the return statement. Here, you will use the context for the first time. The context you've previously created also contains a provider component your new provider will render. You can access this provider component using object dot notation (newContext.Provider).

Since we want to use this provider as a wrapper, it should render any children it wraps.

// context.jsx

// Import createContext() method from React:
import { createContext } from 'react'

// Create new context:
export const newContext = createContext()

// Create new provider component:
export const NewProvider = (props) => {
  return (
    {/* Render Provider provided by previously created context: */}
    <newContext.Provider>
      {/* Render Provider's children: */}
      {props.children}
    </newContext.Provider>
  )
}

Make sure to also export your new Provider component so you can use it where you need it. The next step is to take the Provider and use it as a wrapper for the components for which you want to make the data provided by this provider accessible. You can also use it to wrap the main app component.

This will make anything exposed by the provider accessible to any component in the app.

// index.jsx

// Import React and React-dom:
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'

// Import the NewProvider component:
import { NewProvider } from './context'

// Import app component:
import App from './App'

// Create the main component:
const rootElement = document.getElementById('root')
ReactDOM.render(
  <StrictMode>
    {/* Use the NewProvider to wrap the whole app: */}
    <NewProvider>
      {/* The app component rendering all other components: */}
      <App />
    </NewProvider>
  </StrictMode>,
  rootElement
)

Adding state to the context provider

The provider itself is useless if it doesn't provide any value, or values, to the app. In order to fix this, you need two things. First, you need some value, some data, you want to be available through the provider. Second, you have to make this data accessible from the provider.

The first can be fixed by creating new local state inside the provider. The useState hook will be perfect for this. The value of this state will be what you want to share across the app. Since useState hook also creates an update function, this will also give you a way to update this shared state.

To fix the second thing, you have to add value attribute to the myContext.Provider component returned by the NewProvider component. The value of this attribute can be anything from a primitive data type to an object. If you want to share a single value, the first will be sufficient.

If you want to share multiple values, or values and functions, it will be better to use an object. It is nice to make values available across the app. Even better is to also allow changing these values across the app. So, let's go with the object. Let's create new state and expose both, the state and its update function via the provider.

// context.jsx

// Import createContext() method and useState hook from React:
import { createContext, useState } from 'react'

// Create new context:
export const newContext = createContext()

// Create new provider component:
export const NewProvider = (props) => {
  // Create local state:
  const [state, setState] = useState('')

  // Prepare values to share:
  const val = {
    state, // The state itself
    setState // The state update function
  }

  return (
    {/* Set "val" as the value for "value" attribute: */}
    <newContext.Provider value={value}>
      {props.children}
    </newContext.Provider>
  )
}

Accessing context with the useContext hook

You are almost done. You have context, you have provider and you have something to share via the provider. You have also wrapped the app with the provider and exposed some value via the Provider's value attribute. You can now access the state and setState function exposed via the provider anywhere in the app.

To achieve this, you need just two things. The first thing is the React useContext hook. The second thing is the exported context, the one you created in the beginning with the createContext() method. When you combine these two you will have immediate access to state and setState you created in NewProvider component.

Let's create the main App component. You saw this component in the index.jsx file as the direct child of the provider (Creating the context provider section). This component will be simple. It will contain two components: heading showing welcome message and current value of state and input to update the state via setState.

You will get both, state and setState, from the newContext context. Remember that this context is provided by the NewProvider component. You will get those values by calling the React useContext hook and passing the newContext context as an argument.

// Import useContext hook from React:
import { useContext } from 'react'

// Import newContext context:
import { newContext } from './context'

// Create the App component:
export default function App() {
  // Access the state and setState values in newContext:
  const { state, setState } = useContext(newContext)

  return (
    <div>
      {/* Display the value of "state" */}
      <h1>Hello {state}</h1>

      <h2>Change name:</h2>
      {/*
        Use "setState" update function to update the current value
        of "state" with the current value of input:
      */}
      <input type="text" onChange={(e) => setState(e.target.value)} />
    </div>
  )
}

Multiple contexts

There is basically no limit to how many contexts, and providers, you can have in your React app. You can have as many as you want, as long as you remember to add each provider as a wrapper. For example, we can add additional context for email to this simple sample app. This will require new context and new Provider component.

First, let's create new context for email. This will be almost a mirror copy of the context you already have. You will mostly change just the names.

// email-context.jsx

// Import createContext() method from React:
import { createContext, useState } from 'react'

// Create new context:
export const emailContext = createContext()

// Create new email provider component:
export const EmailProvider = (props) => {
  // Create local state for email:
  const [email, setEmail] = useState('')

  // Prepare values for sharing:
  const val = {
    email,
    setEmail,
  }

  // Render emailContext.Provider exposing "val" variable:
  return (
    <emailContext.Provider value={val}>
      {/* Render children components: */}
      {props.children}
    </emailContext.Provider>
  )
}

Next, you have to import the email context in the main file, where you render the App to the root element. When you have multiple providers their order doesn't really matter. Important thing that the app, or some component where you want to use data from those providers, is wrapped with those providers.

import { StrictMode } from 'react'
import ReactDOM from 'react-dom'

import { NewProvider } from './context'

// Import new email provider:
import { EmailProvider } from './email-context'

import App from './App'

const rootElement = document.getElementById('root')
ReactDOM.render(
  <StrictMode>
    {/* Add email provider as another wrapper of the App component: */}
    <EmailProvider>
      <NewProvider>
        <App />
      </NewProvider>
    </EmailProvider>
  </StrictMode>,
  rootElement
)

With that, you can now use the React useContext hook with emailContext to access the email and setEmail anywhere in the app.

import { useContext } from 'react'

import { newContext } from './context'

// Import new email context:
import { emailContext } from './email-context'

export default function App() {
  const { state, setState } = useContext(newContext)

  // Access the email and setEmail values in emailContext:
  const { email, setEmail } = useContext(emailContext)

  return (
    <div>
      {/* Render the value of "email": */}
      <h1>
        Hello {state}, {email}
      </h1>

      <h2>Change name:</h2>
      <input type="text" onChange={(e) => setState(e.target.value)} />

      <h2>Change email:</h2>
      {/*
        Allow to to update the current value of "email"
        via the "setEmail" update function and text input:
      */}
      <input type="text" onChange={(e) => setEmail(e.target.value)} />
    </div>
  )
}

Conclusion: Getting started with React useContext hook and react context

Creating global states with React context is very easy. With the help of React useContext hook it is also easy to access these contexts and their data. I hope that this tutorial helped you understand how to create contexts and their providers and how to use useContext hook to communicate with them.

17