DEV Community

Takamichi Oki
Takamichi Oki

Posted on

React useContext for Beginners: Passing Data Without Props

Introduction

props are the basic way to pass values from a parent component to a child component. But as your component tree gets deeper, useContext becomes a better option.

In this article, I’ll walk through how to use useContext and explain the difference between using props and using context.

What is useContext?

useContext is a React Hook that lets you share values between components without passing props through every level of the component tree.

In other words, you can use context to pass data down the tree instead of prop drilling.

Props: the Problem

Before diving into useContext, let’s look at the problems you can run into when you rely on props, especially as your component tree grows.

Here’s an example app that uses props:

On this screen, you can choose your preferred language.

Here’s the source code for the app:

App.jsx

import { useState } from 'react'
import LangHeader from './components/LangHeader';

function App() {
  const [lang, setLang] = useState('English');

  const toggleLang = (e) => {
    const newLang = e.target.value;
    setLang(newLang);
  }

  return (
    <LangHeader lang={lang} toggleLang={toggleLang} />
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

LangHeader.jsx

import React from 'react'
import LangContent from './LangContent'

export default function LangHeader({ lang, toggleLang }) {
  return (
    <>
      <div>LangHeader</div>
      <LangContent lang={lang} toggleLang={toggleLang} />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

LangContent.jsx

import React from 'react'

export default function LangContent({ lang, toggleLang }) {
  const LANGUAGES = ['English', 'German', 'Japanese'];

  return (
    <>
    <div>LangContent</div>
    <p>current setting is: {lang}</p>
    {LANGUAGES.map(l => {
      return (
        <React.Fragment key={l}>
          <label htmlFor={l}>{l}</label>
          <input id={l}
          name='lang'
          type='radio'
          value={l}
          onChange={toggleLang}
          checked={lang === l}
            ></input>
        </React.Fragment>
      );
    })}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode
  • App is the top-level component. It keeps the selected language in state and defines the handler to switch languages.
  • LangHeader receives lang and toggleLang as props from App, then passes them down to its child component.
  • LangContent renders the UI: it shows the current setting and the radio buttons, using the props it receives.

As you can see in the source code, LangHeader receives props only to pass them down to its child. When a deeply nested component needs those props, every component in between has to forward them, even if they don’t use them themselves.

This is often called prop drilling. It’s basically like a bucket brigade.

In this example the component tree is shallow, so it’s not a big deal. But as the tree grows, prop drilling becomes harder to maintain and easier to break.

The Basics of useContext

useContext can solve this problem. With context, you can share values without passing props through every intermediate component, almost like the values “skip” levels in the component tree.

The flow is simple:

  • Create a context
  • Provide a context value
  • Consume the context in a component (useContext)

Let’s go through each step one by one.

Create a Context

First, you need to create a context object using createContext.

The key points are:

  • Call createContext() outside of any component (so it’s created once, not on every render).
  • Pass a default value as the argument. This value is used when there is no matching Provider above in the tree.
import { createContext } from "react";

export const AppContext = createContext(null);
Enter fullscreen mode Exit fullscreen mode

If you plan to share multiple values (e.g., both the current language and a setter/handler), it’s common to use an object as the default value:

import { createContext } from "react";

export const AppContext = createContext({
  value: null,
  setValue: () => {},
});
Enter fullscreen mode Exit fullscreen mode

Provide a Context Value

A Provider is how you supply a context value to components below it.

To use it, wrap the part of your component tree that should have access to the value with <YourContext.Provider>, then pass the value via the value prop.

import { useState } from "react";
import { AppContext } from "./AppContext";

function App() {
  const [value, setValue] = useState("A");

  const handleChange = (e) => setValue(e.target.value);

  return (
    <AppContext.Provider value={{ value, handleChange }}>
      <Child />
    </AppContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Notes:

  • The value you pass can be a single value (e.g. value={value}) or an object (e.g. value={{ value, handleChange }}), depending on what you want to share.

Consume the Context in a Component

To read a context value inside a component, use the useContext Hook.

Pass the context object (the one created by createContext) as the argument.
useContext returns whatever you provided via the nearest <Provider value={...}> above in the tree.

import { useContext } from "react";
import { AppContext } from "./AppContext";

function Child() {
  const { value, handleChange } = useContext(AppContext);

  return (
    <>
      <p>Current value: {value}</p>

      <label>
        <input type="radio" name="demo" value="A" onChange={handleChange} checked={value === "A"} />
        A
      </label>

      <label>
        <input type="radio" name="demo" value="B" onChange={handleChange} checked={value === "B"} />
        B
      </label>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Refactoring the App to use Context

Now, let’s refactor the example app to use context.

generateContext.js

import { createContext } from "react";

export const LangContext = createContext('English');
Enter fullscreen mode Exit fullscreen mode

First, create a context and export it so you can import it later when you provide the value with a Provider.

App.jsx

import { useState } from 'react'
import LangHeader from './components/LangHeader';
import { LangContext } from './generateContext';

function App() {
  const [lang, setLang] = useState('English');

  const toggleLang = (e) => {
    const newLang = e.target.value;
    setLang(newLang);
  }

  return (
    // Wrap the component tree with the Provider
    <LangContext.Provider value={{ lang, toggleLang }}>
      {/* No need to pass props down anymore. */}
      <LangHeader />
    </LangContext.Provider>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Next, import the context you created earlier and wrap your component tree with its Provider.

You can think of this as setting up a “channel” that delivers the values to any components below that need them.

LangHeader.jsx

import React from 'react'
import LangContent from './LangContent'

export default function LangHeader() {
  return (
    <>
      <div>LangHeader</div>
      <LangContent />
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

As you can see, LangHeader (an intermediate component) no longer needs to receive props just to pass them down.

LangContent.jsx

import React from 'react'
import { useContext } from 'react';
import { LangContext } from '../generateContext';

export default function LangContent(  ) {
  const LANGUAGES = ['English', 'German', 'Japanese'];

  // Now you can read the values provided by the nearest Provider.
  const { lang, toggleLang } = useContext(LangContext);

  return (
    <>
    <div>LangContent</div>
    <p>current setting is: {lang}</p>
    {LANGUAGES.map(l => {
      return (
        <React.Fragment key={l}>
          <label htmlFor={l}>{l}</label>
          <input id={l}
          name='lang'
          type='radio'
          value={l}
          onChange={toggleLang}
          checked={lang === l}
            ></input>
        </React.Fragment>
      );
    })}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Finally, consume the value in a deeply nested component and use it directly.

With prop drilling gone, the app still behaves exactly the same as it did before the refactor.

Summary

Context can feel a bit like a global variable, because it lets you share values between components without manually passing props through every level.

The key point is that the Provider’s value often includes state. When that state updates, any components that consume the context will re-render, even if they’re far away in the component tree.

Top comments (0)