Type to generate custom UI components with AI

Type to generate UI components from text

OR

Browse thousands of MUI, Tailwind, React components that are fully customizable and responsive.

Explore Components

React useContext Hook: How to Manage State Effectively

React, is a Javascript library designed for constructing user interfaces. It also equips developers with effective tools for state management and data sharing among components. In this guide, we’ll delve into a specific tool within React – the React Context API. Grasping how this API was formed is vital for front-end developers aiming to enhance the efficiency of state management in their applications with React useContext.

Problem with State Management in React

When dealing with React components, effective state management is a crucial element in building dynamic and engaging user interfaces. While passing data down through props is a practical approach in straightforward applications, it becomes less manageable as your application expands.

This method is referred to as “prop drilling”, and it’s a way to pass data through props from parent components to child components and can result in code that is harder to maintain.

React Context as a Solution

React Context addresses the issues associated with prop drilling which is a way to pass data through components by introducing a mechanism to share data, such as state or configuration settings, throughout the entire component tree without the need to use `props`.

Unlike the conventional approach of manually passing props through each level of components. It offers a centralized approach where we can share data and ensure data flow without the need to manually pass props at every level.

This means you can avoid the cumbersome task of passing props explicitly at every level, streamlining the process of sharing data and enhancing the overall efficiency of state management in your React application.

Overview of Early State Management in React

In the earlier versions of React, complex state management requirements often required the practice of elevating state to components higher up in the component tree or resorting to an external state management library like Redux.

While these strategies were effective in managing state across the application, they introduced additional complexities when it came to more complex state management. Complexities like more boilerplate code, seemed unnecessary, especially for smaller projects where a more straightforward state management system might be preferable.

Introduction of Context API

In React 16.3, the context API was introduced as a more straightforward and elegant solution to manage shared state in a React application. This API simplified the steps involved in both creating and using creating and using context, offering a more streamlined approach compared to the cumbersome practice of prop drilling and other state management libraries like Redux most especially for really small applications.

Doing so presented a cleaner and more efficient alternative for managing and sharing data across components within a React application.

This improved approach makes your code easier to read and maintain because it reduces the need to manually pass data down the component tree without using props.

The `useContext` Hook

With the release of React 16.8, this React hook, `useContext` was rolled out and made the consumption of context values within functional components more simplified. The React `useContext` hook became a game changer offering a more concise and convenient syntax. An example of the hook in use is shown below.

// Using useContext in a functional component

import { useContext } from ‘react’

function Component() {
  const contextValue = useContext(MyContext);

  return <div>{contextValue}</div>;
};

We can see from the code above that we imported the `useContext` hook from React and used it to access a value from the Context API. This is just an illustration of how the hook is being used.

We will go into more detail as we explore the topic further.

Use Cases for Context

One use case for the React Context API is solving issues related to nested components and prop drilling as we saw at the beginning of this article.

Nested components often face challenges related to prop drilling, especially when multiple layers are involved. React Context provides a cleaner solution, allowing a deeply nested component to access data without the hassle of passing props through each level.

Just like in the example we saw above, every component whether deeply nested or not has access to the context data, and there’s no need to bother about passing props through each level.

import React, { useContext } from 'react'

const DeeplyNestedComponent = () => {
  const contextValue = useContext(MyContext);

  return <div>{contextValue}</div>;
};

In the upcoming sections, we will delve deeper into the basics of React Context, and its practical applications.

Importing and Setting Up the React `useContext`

In this section, we will delve into the fundamental aspects of using `useContext` in React. Understanding the basics is crucial for incorporating this powerful tool into your frontend development workflow.

PS: Engineers waste time building and styling components when working on a project, writing repetitive markup adds time to the project and is a mundane, boring task for engineers. PureCode.ai uses AI to generate a large selection of custom, styled UI components for different projects and component types.

Basic Setup for Using `useContext` in a React Component

To begin utilizing `useContext` you need to import the hook from the React library but before that, we will need to create the context first. The initial steps involved in creating a context variable using the `createContext` function as seen below.

// Importing the useContext hook from React
import React, { createContext } from 'react';

// Initializing a context variable using createContext
export const MyContext = createContext();

// Setting up a functional component
export default const App = () => {
  // Rest of the component code
};

Creating and Initializing a Context for Sharing Values

Creating a context in React involves utilizing the `createContext` function, which, when called, returns a context object containing various properties, with `Provider` being a crucial one. Our primary interaction will be with the `Provider` component. To use the `Provider` component, you need to provide it with a value object.

This value object is where you can include all the data that needs to be accessible further down the component tree. Essentially, the `Provider` allows you to share specific information across your components seamlessly.

// Importing the useContext hook from React
import React, { createContext } from 'react';

// Initializing a context variable using createContext
export const MyContext = createContext();

// Setting up a functional component
export default const App = () => {
  return (
    <MyContext.Provider>
      {/* Child components go here */}
    </MyContext.Provider>
  )
};

Let us see a practical example of how to pass values into the `Provider` component.

// Importing the useContext hook from React
import React, { createContext } from 'react';
import ChildComponent from ‘/ChildComponent.js’
import ChildComponent2 from ‘/ChildComponent2.js’

// Initializing a context variable using createContext
export const MyContext = createContext();

// Setting up a functional component
export default const App = () => {
  const name = “John”

  return (
    <MyContext.Provider value={name}>
      <ChildComponent />
      <ChildComponent2 />
    </MyContext.Provider>
  )
};

Just as we have seen in the example above the variable `name` can be accessed by any component that is nested in the `MyContext.Provider` component. The provider value prop is used to feed the provider component with the data that needs to be shared down through the component tree.

We can add more values to the value attribute in `MyContext.Provider` component like so

// Importing the useContext hook from React
import React, { createContext } from 'react';

// Initializing a context variable using createContext
export const MyContext = createContext();

// Setting up a functional component
export default const App = () => {
  const [name, setName] = useState("John")

  const handleUser = (name) => {
    setName(name)
  }

  const value = {
    name,
    handleUser
  }

  return (
    <MyContext.Provider value={value}>
      {/* Child components go here */}
    </MyContext.Provider>
  )
};

As we can see from the code above we used the `useState` hook to initialize a state variable name with an initial value of “John” and a function `setName` to update the state.

And then a callback function, `handleUser`, takes a parameter `name` and uses the `setName` function to update the state of the `name` variable with the provided value.

Later on, we created an object `value` containing the current state (`name`) and the callback function to update it (`handleUser`). This object will be used as the value for the context provider.

Accessing Context Values for Consumption in Components

Once the context has been created and a `Provider` is set up, you can use `useContext` within a component to access context value just like we have established in earlier examples. This allows you to consume the shared values directly in the `ChildComponent`.

import React, { useContext } from 'react'
import { MyContext } from '/index.js'

export default const ChildComponent = () => {
  const { name } = useContext(MyContext);

  return <div>{ name }</div>;
};

As we can see we now have access to the context value we declared earlier in `App` component.

Dynamically Updating and Managing Context Values

One of the powerful aspects of React Context is the ability to dynamically update shared values. This is achieved by modifying the context value using callback functions and triggering updates in child components.

We already created a callback function called `handleUser` in our earlier examples that can be used in child components to update the current state of `name`.

Let’s see this in action in the example below

import React, { useContext } from 'react'
import { MyContext } from './index.js'

const ChildComponent2 = () => {
  const { name, handleUser } = useContext(MyContext);

  return (
    <>
      <button onClick={() => handleUser("James")}>Update Value</button>
      <h1>{ name }</h1>
    </>
  );
};

export default ChildComponent2;

In the code above, clicking the button in `ChildComponent2` changes the displayed value of `name`. This change happens thanks to a callback function called `handleUser`, which comes from the React context using the `useContext` hook.

In simpler terms, the `handleUser` function handles the process of updating the `name` value when the button is clicked. It’s like a behind-the-scenes worker that makes sure our displayed value stays up to date and reacts to user actions. This way, the code stays straightforward, making it easier to understand and maintain in a React application.

Understanding these basic concepts lays the foundation for leveraging React Context in your projects. In the next sections, we will explore practical examples and use cases to further enhance your comprehension and application of `useContext`.

Use Cases and Practical Examples

Let’s explore real-world scenarios where React Context, and `useContext`, prove to be a valuable tool.

Making sure different parts of a web application can communicate and update together is important. The `useContext` tool in React helps a lot with this. It’s like a manager that keeps everything organized and allows different sections of a website to work together smoothly.

While there are various state management solutions available, such as Redux, `useContext` offers a lightweight and built-in alternative. It is particularly beneficial to smaller and medium-sized projects where a simpler approach is preferable.

Illustrating State Management using `useContext` in a Simple Application

Let’s create a straightforward application where users can input their first name, last name, and occupation. After submission, we’ll display this information in a table. Traditionally, achieving this functionality involved prop drilling, a process that can become complex as the application grows.

However, with the introduction of React context, this task is now much simpler and more streamlined. We’ll leverage React context to efficiently manage and share user details across different components, eliminating the need for extensive prop drilling.

We will start with creating the context

import React, { createContext } from "react";
import "./styles.css";

export const UserContext = createContext();

export default function App() {
  const user = "James"

  const value = {
    user
  };

  return (
    <UserContext.Provider value={value}>
      {/* components go here*/}
    </UserContext.Provider>
  );
}

Building a Form Component

Now that we have created the context we can go on to create other components like the `Form` that will be needed to get the first name, last name, and occupation of the user.

import React, { useContext } from "react";
import "./styles.css";

export default function Form() {
  return (
      <form className="form__wrapper">
        <div className="form__input">
          <label htmlFor="firstName">Firstname</label>
          <input name="firstName" type="text" />
        </div>

        <div className="form__input">
          <label htmlFor="lastName">Lastname</label>
          <input name="lastName" type="text" />
        </div>

        <div className="form__input">
          <label htmlFor="job">Occupation</label>
          <input name="job" type="text" />
        </div>

        <button type="submit">Submit</button>
      </form>
  );
}

Building a `TableItem` and `TableList` Components

Now, we’ll proceed to develop a `TableItem` component, which will be nested within a `TableList` component. This approach aligns with React’s emphasis on reusability and the decomposition of code into smaller, more manageable components. 

By breaking down our code into smaller components, we enhance the overall clarity and comprehension of our application. This modular structure not only improves the ease of understanding but also facilitates the reuse of the `TableItem` component, contributing to a more maintainable and scalable codebase.

import React from "react";

export default function TableItem() {
  return (
    <tr>
      <td>John</td>
      <td>Mark</td>
      <td>Software Developer</td>
    </tr>
  );
}

After this, we will go on to use the `TableItem` component in the `TableList` component.

import React from "react";
import TableItem from "./tableitem";

export default function TableList() {
  return (
    <table className="table__wrapper">
      <tr>
        <th>First Name</th>
        <th>Last Name</th>
        <th>Occupation</th>
      </tr>
      <TableItem />
    </table>
  );
}

Adding Functions to Our Context

As we can see the step-by-step approach to which we made smaller components to handle specific parts of the application. Now we will go to add functions and values to the context so it can be used centrally in all the components in the application.

import React, { createContext, useState } from "react";
import "./styles.css";
import Form from "./form";
import TableList from "./tablelist";

export const UserContext = createContext();

export default function App() {
  const [userDetails, setUserDetails] = useState({
    firstName: "",
    lastName: "",
    job: "",
  });

  const handleChange = (event) => {
    const { name, value } = event.target;
    setUserDetails((prev) => ({ ...prev, [name]: value }));
    };

  const [userList, setUserList] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    // Check if any of the fields are empty before adding to the array
    if (userDetails.firstName && userDetails.lastName && userDetails.job) {
      setUserList((prevList) => [...prevList, userDetails]);
      // Clear the form after adding details to the array
      setUserDetails({
        firstName: "",
        lastName: "",
        job: "",
      });
    }
  };

  const value = {
    userDetails,
    userList,
    handleChange,
    handleSubmit,
  };

  return (
    <UserContext.Provider value={value}>
      <Form />
      <TableList />
    </UserContext.Provider>
  );
}

Firstly, we used the `useState` hook to create an object with firstName, lastName, and job as keys that will hold the actual value from the input fields This way we don’t have to create separate useState hooks to track and hold the data from each input fields. 

Next, we defined a function `handleChange` that handles every change in the input fields. It uses the spread operator to update the value of each key in the object `userDetails`.

We also need a place to save each form entry so we can display it in a table. To do this we created an array called `userList` with the `useState` hook.

To utilize this array that we created we defined a function `handleSubmit` to handle the submission after the form has been completed. We first prevent the default form submission behaviour to avoid page reloads. Then, we check if all the fields in `userDetails` are filled before pushing them to the `userList` array with `setUserList` for display on the table. Finally, we clear the form using the `setUserDetails` function callback. It just sets the firstName, lastName, and job values to an empty string (“”).

Lastly, we added `userDetails`, `userList`, `handleChange`, and `handleSubmit` to the `value` object, then we passed it into the `UserContext.Provider` so it can be available down the component tree.

Modifying the Form and TableItem Components

Now we can modify the `Form` and the `TableItem` components we built earlier to properly use the value that we provided in the context.

import React, { useContext } from "react";
import "./styles.css";
import { UserContext } from "./App";

export default function Form() {
  const { userDetails, handleChange, handleSubmit } = useContext(UserContext);

  return (
    <>
      <form className="form__wrapper" onSubmit={handleSubmit}>
        <div className="form__input">
          <label htmlFor="firstName">Firstname</label>
          <input
            name="firstName"
            type="text"
            value={userDetails.firstName}
            onChange={handleChange}
          />
        </div>

        <div className="form__input">
          <label htmlFor="lastName">Lastname</label>
          <input
            name="lastName"
            type="text"
            value={userDetails.lastName}
            onChange={handleChange}
          />
        </div>

        <div className="form__input">
          <label htmlFor="job">Occupation</label>
          <input
            name="job"
            type="text"
            value={userDetails.job}
            onChange={handleChange}
          />
        </div>

        <button type="submit">Submit</button>
      </form>
    </>
  );
}
userDetails that holds the exact user details in an object

Explanation

We used the `useContext` hook to access values from the `UserContext` and destructured `userDetails`, `handleChange`, and `handleSubmit` from the context. Then, we used the data from the `userDetails` object to assign the respective values to the `value` attribute in the input tag.

We also added the `handleChange` callback to the onChange event handler in the input tag. This is what is responsible for tracking every change that is made to the input field and updating the `userDetails` state as users input information.

Finally, we added the `handleSubmit` function to the form which triggers when the user hits the enter key or clicks on the submit button. 

The `Form` component acts as a controlled form, where the input fields are connected to the `userDetails` state from the context. User input is updated and state is updated with a new value, and the form submission is handled through the `handleSubmit` function, which is provided by the context. This way, the form seamlessly interacts with the shared state and functions managed by the `UserContext`.

userList array on the console after the user has submitted
import React, { useContext } from "react";
import { UserContext } from "./App";

export default function TableItem() {
  const { userList } = useContext(UserContext);

  return (
    <>
      {userList.map((item) => (
        <tr key={item.firstName}>
          <td>{item.firstName}</td>
          <td>{item.lastName}</td>
          <td>{item.job}</td>
        </tr>
      ))}
    </>
  );
}

In the code above we use the `useContext` hook to access the `userList` from the `UserContext`. Since `userList` is an array, we mapped over it, generating a table row (`<tr>`) for each user. Each row contains three table data cells (`<td>`) displaying the user’s first name, last name, and job. 

table showing the TableItem in a TableList

This `TableItem` component is responsible for rendering a table row for each user in the `userList`. It uses data from the shared context (`UserContext`), specifically the `userList` array. This way, whenever a new user is added through the form, the `TableList` component can dynamically update by rendering a new row with the user’s details.

Here’s the final output that includes the form and the table.

End result of the application to illustrate useContext

Tips and Best Practices

Let’s examine the essential tips and best practices when working in React Context and specifically, the `useContext` hook. Adopting these recommendations will contribute to cleaner and more maintainable code in your front-end projects.

Adopting Consistent and Meaningful Naming Conventions

Naming conventions play a crucial role in ensuring clarity and maintainability in your codebase. When working with context variables, it’s essential to follow consistent naming conventions.

  • Be Descriptive

    Choose names that convey the purpose of the context variable clearly.

  • Use camelCase and PascalCase

    Follow the convention of camelCase variable names and PascalCase for context variable names to align with Javascript conventions.

  • Avoid Ambiguity

    Ensure the name clearly represents the data or functionality provided by the context to avoid ambiguity.

    Let’s see an example

    // Bad naming convention
    const MyCtx = React.createContext();
    
    // Good naming convention
    const ThemeContext = React.createContext();

When to Use `useContext`

While `useContext` is a powerful tool for state management, it’s crucial to understand when it is the preferred choice over other state management solutions. Consider the following scenarios.

  • Smaller to medium-sized projects.

    `useContext` is well-suited for smaller to medium-sized projects where a simpler state management approach is preferable.

  • Component-Level State.

    Use `useContext` when managing state that is specific to a component or a small subtree of the component tree.

  • Avoiding Global State Overhead

    If the application doesn’t need a shared state that multiple components must access, it’s simpler to avoid unnecessary complications by not using `useContext`.

    // Example using useContext for component-level state
    const MyComponent = () => {
      const { data, setData } = React.useContext(MyContext);
    
      // Component logic using context state
    };

Error Handling in Context

  • Identifying Common Errors

    Be aware of common errors, such as accessing a context outside of a `Provider` or using a context that hasn’t been properly initialized.

  • Using Default Values

    Provide meaningful default values to contexts to avoid unexpected behaviour when a `Provider` is missing.

These tips will greatly enhance the readability and maintainability of your code while ensuring a good developer experience when working with React Context.

Final Thoughts on React useContext Hook

In conclusion, React Context and the `useContext` hook provide a streamlined approach to state management in React applications. This tool is particularly beneficial for smaller to medium-sized projects, offering a cleaner alternative to prop drilling and external state management libraries.

Adopting clear naming conventions and understanding when to use useContext contributes to code clarity and maintainability.

For the most accurate documentation on `useContext` it is highly recommended to refer directly to the official React docs.

I also recommend this article on Context API and the useContext hook as it builds upon the basics that we have covered here.

If you’d like to learn more about the React Context API, I recommend the video tutorials below:

PureCode.ai can cater for your code development process. It will save you valuable time and effort on a project by providing customized, and ready-to-use components which will allow you to prioritize more important and thought-intensive tasks to speed up the development of your user interface.

Shadrach Abba

Shadrach Abba