Understanding React Context API

Understanding React Context API

Introduction

React context API is a concept I feel gets neglected. Yes, I was guilty of this. It's super important you get to understand how it works and why it's needed. By the end of this article, you should have a solid understanding of it.

We will be using hooks in the examples in this article. If you are not familiar with hooks, please visit the React hooks documentation to get yourself accustomed with it.

What is React Context?

Context API in react is no rocket science. It is a method of passing data from a parent component down its tree or child components. It saves us from passing props all through the various level of components when trying to reach for a deeply nested component. With context, we avoid unnecessary prop drilling and cleaner code.

The context API is like a store which has a seller and different buyers who get goods from the store via the seller. The store is the context object, theseller is the Provider while the buyer is the consumer who buys goods from the store via the seller.

The image below illustrates the React context API in a nutshell;

pexels-quintin-gellar-696205.jpg

For more information on React context, visit the official documentation here

Concepts in React Context

  1. CreateContext
  2. Provider
  3. Consumer
  4. DisplayName
  • CreateContext:

This is used to create a context object. Whenever a component is rendered that is subscribed to this context, it reads the value of the closest matching Provider. It only uses the default value if no Provider was specified.

An example is given below;

const context = React.createContext(defaultValue);
  • Provider

The provider is responsible for creating the context which would be consumed by the consumer. This is usually the parent component, encompassing every other child component.

All child components subscribed to a producer, re-renders whenever the value prop of the provider changes.

Example of a Provider is given below;

<Context.Provider value={/* some value */}>
    {/* Child components goes here */}
</Context.Provider>
  • Consumer

This is the component that subscribes to context change, it feeds on the value prop from the Provider. This lets you subscribe to a context within a function component.

Example of a Consumer is given below;

<Context.Consumer>
     {value => console.log(value)} // Render something with the context value
</Context.Consumer>
  • DisplayName:

The context object accepts a displayName string, which is used to set the name of a context in React DevTools.

An example is given below to best explain this;

  // Here we will give the context a display name
  // of 'Custom Display Name'
  Context.displayName = 'Custom Display Name';

In React DevTools, you should see something similar to what we have below;

react-devtools.png

From the above, we can see the custom name assigned to the context created.

If you understand all that we've discussed up till this moment, you are one step away from becoming a context API god!.

Utilizing Context API

Now let’s look at how to implement context in a React application.

If we have an app that has two theme modes, i.e. the light and dark mode and we want to change the colour of backgrounds and texts based on the current theme. The old way of doing this without context API will be by passing a theme prop value from the parent component down to the child components as shown below;

import React, { useState, Fragment } from 'react';

// The theme mode object containing different styles 
// With respect to the current theme mode
const themeMode = {
  light: {
      background: '#fff',
      text: '#000'
  },
  dark: {
      background: '#000',
      text: '#fff'
  }
}

// The parent component where the current theme value
// will be passed down to the child components
// via prop drilling.
const App = () => {
  // Setting the current state of the theme to 'dark'
  const [theme, setTheme] = useState(themeMode.light);
    return (
      // pass the current theme value down to the compoenent via props.
      <Container theme={theme} />
    )
}

// A component in the middle receiving theme value via props
const Container = props => {

  return (
    // Here we will set the background color of the container component
    // with respect to the current theme
    // Notice how the value of the theme is gotten from the props
    <div style={{ backgroundColor: props.theme.background }}>
      <ThemedContent theme={props.theme} />
    </div>
  );
}

const ThemedContent = props => {
  return (
    // Pass down the theme value further
    <Fragment>
      <TextContent theme={props.theme} />
      <Button theme={props.theme} />
    </Fragment>
  );
}

const Button = props => {

  // Get the background and text value using the current theme value
  const btnStyles = {
    backgroundColor: props.theme.background,
    color: props.theme.text
  }
  return (
      <button style={btnStyles}>Here</button>
  )
}

const TextContent = props => {
  return (
    <div style={{ color: props.theme.text }}>
      <h2>Text Title Goes Here</h2>
      <h4>Text Sub Title Goes Here</h4>
      <p>All the text content paragraphs go here and every bit of details of the app would live here.</p>
    </div>
  )
}

export default App;

Notice how cumbersome it is to get the current theme value, we had to pass down the current theme value from the parent down to the child components using props. Imagine having 50 child components that depend on the theme value.

A situation like this, context comes in mind. Now, let’s refactor the code to use context as shown below;

// import the createContext and useContext hooks
// CreateContext is used foe creating a context while
// useContext is used for cosuming the context
import React, { useState, useContext, createContext, Fragment } from 'react';

// The theme mode object containing different styles 
// With respect to the current theme mode
const themeMode = {
    light: {
        background: '#fff',
        text: '#000'
    },
    dark: {
        background: '#000',
        text: '#fff'
    }
}

// Create a theme context with default value 'themeMode.dark'
const ThemeContext = createContext('themeMode.dark');

// The parent component (Provider)
const App = () => {

    return (
        // Here we are setting the light mode as the context value
        <ThemeContext.Provider value={themeMode.light}>
            <Container />
        </ThemeContext.Provider>
    )
}

// A component in the middle doesn't have to
// receive the theme prop anymore.
const Container = () => {
    // Get the themeContext value
    const theme = useContext(ThemeContext);
    return (

        // Here we will set the background color of the container component
        // with respect to the current theme from the ThemeContext
        <div style={{ backgroundColor: theme.background }}>
            <ThemedContent />
        </div>
    );
}

const ThemedContent = () => {
    return (
        <Fragment>
            <TextContent />
            <Button />
        </Fragment>
    );
}

const Button = () => {
    // Get the themeContext value
    const theme = useContext(ThemeContext);

    // Get the background and text value using the current theme value
    // from the ThemeContext
    const btnStyles = {
        backgroundColor: theme.background,
        color: theme.text
    }

    return (
        <button style={btnStyles}>Here</button>
    )
}

const TextContent = () => {
    // Get the themeContext value
    const theme = useContext(ThemeContext);
    return (
        <div style={{ color: theme.text }}>
            <h2>Text Title Goes Here</h2>
            <h4>Text Sub Title Goes Here</h4>
            <p>All the text content paragraphs go here and every bit of details of the app would live here!.</p>
        </div>
    )
}

export default App;

Why Context?

Context is highly recommended when you want to pass many data down to the child components of a parent component. Whenever data needs to be shared across child elements or components down the tree, context should always come to mind.

However, context makes components usability difficult, so it’s advisable to use component composition when passing data peculiar to one or few components down the tree.

For example;

const [wallet, setWallet] = useState(4000); 
<Orders wallet={wallet}>
<Cart>
    <Products>
        <Checkout />
    </Products>
</Cart>
</Orders>

From the above, we intend to know the amount left in the wallet to know if the user can proceed with the checkout or not, a more efficient way to do this is passing the Checkout component down as props i.e. using component composition rather than using context or passing the props sparingly on all child components. Check out more on this from the documentation.

An example of this is shown below;

const [wallet, setWallet] = useState(4000); 

const CheckoutWallet = () => {
        // Here the wallet value is passed directly to only
        // the Checkout component which only needs it.
    return <Checkout wallet={wallet} />
}
<Orders>
<Cart>
    <Products>
        <CheckoutWallet />
    </Products>
</Cart>
</Orders>

Updating Context

Sometimes, a child element or a consumer might want to be able to change the context value of the Producer. Now, let's make the button in the Button component be able to toggle between light or dark theme when clicked.

The final code should look like what we have below;

// import the createContext and useContext hooks
// CreateContext is used foe creating a context while
// useContext is used for cosuming the context
import React, { useState, useContext, createContext, Fragment } from 'react';

// THe theme mode object containing different styles 
// With respect to the current theme mode
const themeMode = {
    light: {
        background: '#fff',
        text: '#000'
    },
    dark: {
        background: '#000',
        text: '#fff'
    }
}

//  We added a changeTheme property to the context object
// which holds the default function.
// Ensure the context value the consumer is expecting matches
// the structure of the context object here.
const ThemeContext = createContext({
    theme: themeMode.light,
    changeTheme: () => {}
});

// Give the context a unique display name on React DevTools
ThemeContext.displayName = 'Custom Display Name';

// The parent component (Provider)
const App = () => {

    // function to toggle the theme state
    const toggleTheme = () => {
        setTheme(prevState => prevState === themeMode.light ? themeMode.dark : themeMode.light);
    }
    // theme state either dark or light
    const [theme, setTheme] = useState(themeMode.dark);

    return (
        // Here we are setting the light mode as the theme and 
        // toggleTheme function as changeTheme
        <ThemeContext.Provider value={{theme: theme, changeTheme: () => toggleTheme()}}>
            <Container />
        </ThemeContext.Provider>
    )
}

// A component in the middle doesn't have to
// receive the theme prop anymore.
const Container = () => {
    // Get the themeContext object value
    const themeObj = useContext(ThemeContext);
    return (

        // Here we will set the background color of the container component
        // with respect to the current theme from the ThemeContext
        <div style={{ backgroundColor: themeObj.theme.background }}>
            <ThemedContent />
        </div>
    );
}

const ThemedContent = () => {
    return (
        <Fragment>
            <TextContent />
            <Button />
        </Fragment>
    );
}

const Button = () => {
    // Get the themeContext value i.e cosuming the context from the Provider
    const themeObj = useContext(ThemeContext);

    // Get the background and text value using the current theme value
    // from the ThemeContext
    const btnStyles = {
        backgroundColor: themeObj.theme.background,
        color: themeObj.theme.text
    }
    return (
       // Button toggling between theme using the 
       // changeTheme function in the context object
        <button onClick={themeObj.changeTheme} style={btnStyles}>Here</button>
    )
}

const TextContent = () => {
    // Get the themeContext value
    const themeObj = useContext(ThemeContext);
    return (
        <div style={{ color: themeObj.theme.text }}>
            <h2>Text Title Goes Here</h2>
            <h4>Text Sub Title Goes Here</h4>
            <p>All the text content paragraphs go here and every bit of details of the app would live here!.</p>
        </div>
    )
}
export default App;

Conclusion

  • Only use context when you have to pass data down to a lot of child components.
  • Know when to use composition over context.

Thank you for going through this article. If you find it useful in any way, kindly drop a like or comment.

Credits