React Context - useContext Hook

React Context - useContext Hook

·

7 min read

Introduction

In React, if you want to use a particular data in multiple components, what you would usually do is create the data in the parent component and pass it as a prop to all the children components that need it.

import { useState } from 'react';
import { Modal } from './Modal';

export const App = () => {
    const [showModal, setShowModal] = useState(false);

return (
    <div className='App' >
        <button onClick={() => setShowModal(true)}>Show Modal</button>
        {showModal && <Modal setShowModal={setShowModal} />}
    </div>

)}

In the example above, the Modal is only rendered if showModal is true. The button has an onClick method that displays the modal when the user clicks on it. The setShowModal is passed as a prop to the Modal component so it can be used to close the modal by changing the value of showModal to false.

Prop Drilling

Now consider a case where you need to use a particular data in several nested components, for example, a user's authentication status or a user's avatar:

In the illustration above, the user object is needed both in the Navbar in order to display the username, and in the Avatar component which displays the user's profile picture. If we were to use props in this case, we would have to pass it through several components like thise:

A horizontal flowchart showing all the components you would need to pass a prop through before you can use it in the 'avatar' component.

We would create the user object in the App component and then pass it through ArticleList, ArticleDetails and ArticleComments before we can use it in the Avatar component.

Using props in this type of scenario results in prop drilling, which can make your code hard to maintain because you're importing components you don't need just to be able to pass it down to a child component.

Prop drilling is the act of passing data through several nested children components, in a order to deliver this data to a deeply-nested component. One common issue with this approach is that often, most of the components through which this data is passed have no actual need for it. They are simply used as mediums for transporting it to its destination component.

Context

Context is a way to share values between components without having to pass props down manually at every level of the component tree. It is designed to be a way to share values that are considered "global" for a tree of React components, such as a current authenticated user, theme, or preferred language.

To create a context, you use the createContext function from react. This function takes a default value for the context, which will be used as the initial value of the context. For example:

import { createContext } from 'react';

const ThemeContext = createContext('light');

The ThemeContext object created above is a plain JavaScript object with a Provider component and a Consumer component. You can use the Provider component to provide a value for the context, and the Consumer component to access the value in a functional component. However, the useContext hook provides a simpler way to access the value in a functional component, so we are going to focus on using it.

Note: most developers prefer to set the default value of the context as null or simply leave it blank and then define a state for changing the value of the context.

To provide a value for the context, you can use the ThemeContext.Provider component. This component takes a value prop, which is the value that will be made available to all components consuming the context.

Here is an example of how to use the ThemeContext.Provider component:

import { ThemeContext } from './themeContext';

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Content />
    </ThemeContext.Provider>
  );
}

In the example above, the Content component and all of its children will have access to the value of the ThemeContext, which is set to "dark".

To access the context value in a functional component, you can use the useContext hook. The useContext hook takes a context object as an argument and returns the current context value for that context. Here is an example of how to use the useContext hook:

import { useContext } from 'react';
import { ThemeContext } from './themeContext';

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>Click me!</button>;
}

In the example above, the Button component is able to access the value of the ThemeContext without having to pass it down as a prop. This can be especially useful in large component trees where props can become difficult to manage.

It's important to note that the useContext hook will only update the component if the context value changes. If the context value does not change, the component will not re-render. This means that if you need to update the context value from a child component, you will need to use the useState hook or another way to trigger a re-render.

Best Practices

  • When working with multiple global states in your application, it is advised to create different contexts for each of them. For instance, if your application requires a global state for a user object and also a theme toggle, you should create different contexts for each of them instead of combining all in a single context.

  • Create a separate component for a context instead of writing it in your App component.

import { createContext, useState } from 'react';
// initialize a context
export const AuthContext = createContext();

export const AuthContextProvider = ({ children }) => {
    // data to be passed to the context
    const [user, setUser] = useState({name: 'John Doe', email: 'johndoe@example.com'});

  return (
    // context provider to wrap all components using the context
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
};

By doing it like this, you can now wrap all the components that require the context with AuthContextProvider.

import { AuthContextProvider } from './AuthContextProvider'

export const App = () => {
    return (
        <AuthContextProvider>
            <Navbar />
            <Homepage />
            <ProductPage />
        </AuthContextProvider>
    );
};

This makes your code cleaner, easier to understand, and more composable.

  • Create a custom reusable hook that returns the value of the context so it can be used within any child component without importing the useContext hook.
import { useContext } from 'react';
// import the context
import { AuthContext } from './ThemeContext';

export const useAuthContext = () => {
    // get the value of the context 
    const user = useContext(AuthContext)
    // check of the component trying to use the context is inside the Provider
    if (!user) {
        throw Error('useAuthContext must be inside an AuthContext Provider');
    }
    // return the context object
    return user
}

In the example above, we set user to the value we get from consuming AuthContext. The conditional statement throws an error if a component is unable to get the value of the context, which means that component is not inside the AuthContext provider.

This makes it a whole lot easier to use the context. All you have to do is just import useAuthContext in any component that needs it and then destructure user from it.

import { useAuthContext } from './useAuthContext';

export const Navbar = () => {
    const { user } = useAuthContext();

    return (
        <nav>
            <h3>Hello {user.name}</h3>
        </nav>
    );
};
  • Avoid using context for states that will change too often within the lifecycle of your application. For example, states like form inputs that change each time the user interacts with the form.

Drawbacks of Using Context

One of the major side-effects of using context is that each time any value in the context changes, all the components using the context are re-rendered, even if they don't need to. This can have a negative impact on the performance of your application.

Conclusion

Using context can make your codebase cleaner and easier to understand /maintain when done correctly. However, it can also affect the speed and performance of your application if used inappropriately.

The rule of thumb is that if the your state is only used within a few components or the value of that state is going to change continously during the lifecycle of the application, then its better to use props, or any another state management tool.

Don't rush to use global states in your application as they can easily introduce unexpected bugs and also cause performance issues in your application. It's okay to use props if the data is only used in a few nested components .Context should only be used for states that are needed throughout the application.

Did you find this article valuable?

Support Paul Saje by becoming a sponsor. Any amount is appreciated!