DEV Community

Cover image for Beginners Guide To The Context API in React
Yahaya Oyinkansola
Yahaya Oyinkansola

Posted on

Beginners Guide To The Context API in React

When building applications with React, one of the most common things you would do is pass data down as props, that data is usually state that you want to use in another component. As your application starts to grow, passing data down to your components via props can hurt the performance of your application. This is the reason why solutions like the Context API was created. Let's learn what it's all about.

What is The Context API?

First of, I am pretty sure we have written code like this:

import PersonalInfo from "@/components/PersonalInfo";

function App() {
  return (
    <PersonalInfo
      inputs={inputs}
      formErrors={formErrors}
      handleInputChange={handleInputChange}
    />
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This is a typical pattern in React to pass data down to your components as props. No matter how you learn React, every tutorial basically teaches this same concept. This principle works for the most part, and there is nothing wrong with it, but the problem starts when we have state that is being used globally within our application. For example, say we have a isUserLoggedIn state that checks if a user is logged in, you can imagine this being used across multiple components in our application, passing down this data as a prop can easily become a problem because if the state where to be updated, every component that uses that state via props will cause the component to re-render.

With the context API, you make your data globally available via a Context, you can then access that data within any component in your application. It is important to note that the Context API is not a state management library like Redux or Zustand, some people get this mixed up, what the context API solves is making it easier to pass data around your components without having to use props, which can cause components to re-render unnecessarily if they don't depend on the state. That's the important distinction to note because when your application starts to scale, there could be multiple components receiving props they don't need, which they need to pass down to another component, this passing of data can cause components to re-render unnecessarily.

To create a context in React, you use the createContext function:

import { createContext } from "react";

// You give the context a name that represents it
export const Context = createContext(null);
Enter fullscreen mode Exit fullscreen mode

But first, what really is a Context?

Before we start diving into how to use the Context API, it's important we first of all understand what a context is as that will make things clearer.

A Context is the situation or background that helps explain the meaning of a statement, an idea or event. Simply put, a context basically provides clear information as to the meaning of something.

In the case of React, a Context provides surrounding information to components that need it, without requiring you to pass that information manually via props. The Context API allows us share state that is used globally in our application by wrapping the app in a specific context. This then enables us to access data in the component we need it in.

Now that we have better understanding of what a Context is, let's get into how to create a context in React.
To create a context, you use the createContext function as we saw earlier:

import { createContext } from "react";
export const Context = createContext(null);
Enter fullscreen mode Exit fullscreen mode

Once you create a context, you then wrap your entire application inside a Context Provider which ensures any data passed in that context is made accessible from any component.

import ChildComponent from "./ChildComponent";
import Context from "./Context";

function App() {
  const [count, setCount] = useState(0);

  return (
    <Context.Provider
      value={{
        count,
        setCount,
      }}
    >
      {/*       
        The component here wraps the entire application.
        In this case, I am just using a single component
      */}
      <ChildComponent />
    </Context.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

<Context.Provider> essentially provides (i.e. makes available) to components the data we pass into the context. Note that Context in this case is the name of the context we created. If we name the context "Testing" for instance, then it would have been <Testing.Provider>, usually, the convention is a name followed by the word "Context" e.g AuthContext. The Context Provider takes in a value prop which we use to define the data our context holds. This way, any value we need can be pulled out from the context.

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

export default function ChildComponent() {
  const { count, setCount } = useContext(Context);
  return <h1>This is count {count}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we just extracted the data we needed from our context in this child component. It's not enough to create a context and wrap your entire application inside it, you still need to explictly tell React you want to use that context, else, it won't know where we are pulling data from, in this case, that being count and setCount. To use a context, we use the useContext function and pass in our context. Now, even if we pass more data into the value prop in our context provider, so long as we only use count and setCount here, they are the only things that will cause the component to re-render when there is a change.

An Example

To further illustrate how the Context API works, let me use an example from a project I built from Front End Mentor. This is the project. This application calculates the age of a user in days, months, and years. This is how the code looked initially:

import { useState } from "react";
import "./App.css";

const App = () => {
  const [formValues, setFormValues] = useState({
    day: "",
    month: "",
    year: "",
  });

  const [formErrors, setFormErrors] = useState({
    dayErr: "",
    monthErr: "",
    yearErr: "",
  });

  function handleFormValues(e) {
    setFormValues((prevState) => {
      const newFormValues = {
        ...prevState,
        [e.target.name]: e.target.value,
      };

      return newFormValues;
    });
  }

  function validateFormFields(field, value, max) {
    if (value == "") {
      return "This field is required";
    } else if (isNaN(value) || value < 1 || value > max) {
      return `Must be a valid ${field}`;
    } else {
      return "";
    }
  }

  function handleSubmit(e) {
    e.preventDefault();

    const { day, month, year } = formValues;
    const dayErr = validateFormFields("day", day, 31);
    const monthErr = validateFormFields("month", month, 12);
    const yearErr =
      year == ""
        ? "This field is required"
        : isNaN(year) || year > currentYear
        ? "Must be in the past"
        : "";

    setFormErrors((prevState) => {
      const newFormErrors = {
        ...prevState,
        dayErr,
        monthErr,
        yearErr,
      };

      return newFormErrors;
    });

    ...
  }

  return (
    <>
      <main className="age-container">
        <form onSubmit={handleSubmit}>
          <div className="form-container">
            <div className="input-group">
              <label
                htmlFor="day"
                className={formErrors.dayErr ? "text-error" : ""}
              >
                Day
              </label>

              <input
                type="text"
                id="day"
                className={formErrors.dayErr ? "input-error" : ""}
                name="day"
                value={formValues.day}
                onChange={handleFormValues}
                placeholder="DD"
              />

              <span>{formErrors.dayErr}</span>
            </div>

            <div className="input-group">
              <label
                htmlFor="month"
                className={formErrors.monthErr ? "text-error" : ""}
              >
                Month
              </label>

              <input
                type="text"
                id="month"
                className={formErrors.monthErr ? "input-error" : ""}
                name="month"
                value={formValues.month}
                onChange={handleFormValues}
                placeholder="MM"
              />

              <span>{formErrors.monthErr}</span>
            </div>

            <div className="input-group">
              <label
                htmlFor="year"
                className={formErrors.yearErr ? "text-error" : ""}
              >
                Year
              </label>

              <input
                type="text"
                id="year"
                className={formErrors.yearErr ? "input-error" : ""}
                name="year"
                value={formValues.year}
                onChange={handleFormValues}
                placeholder="YYYY"
              />

              <span>{formErrors.yearErr}</span>
            </div>
          </div>

          <div className="button-container">
            <button type="submit" className="submit-btn">
              <img src="/icon-arrow.svg" alt="" />
            </button>
          </div>
        </form>

        ...
      </main>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

First off, I moved the day, month and year input fields into a reusable component called <InputGroup />. This way, I can just use that component and pass in the data that's needed:

function InputGroup({
  label,
  placeholder,
  name,
  value,
  error,
  OnInputChange
  ...
}) {
  return (
    <div className="input-group">
      <label
        htmlFor={label}
        className={error ? "text-error" : ""}
      >
        {label}
      </label>

      <input
        type="text"
        inputMode="numeric"
        id={label}
        value={value}
        onChange={OnInputChange}
        name={name}
        className={error ? "input-error" : ""}
        placeholder={placeholder}
      />
    </div>
  );
}

export default InputGroup;
Enter fullscreen mode Exit fullscreen mode

Then over in the main App component:

import InputGroup from "./components/InputGroup";

const App = () => {
  const [formValues, setFormValues] = useState({
    day: "",
    month: "",
    year: "",
  });

  const [formErrors, setFormErrors] = useState({
    dayErr: "",
    monthErr: "",
    yearErr: "",
  });

  function handleFormValues(e) {
    ...
  }

  function handleSubmit(e) {
    ...
  }

  return (
    <>
      <main className="age-container">
        <form onSubmit={handleSubmit}>
          <div className="form-container">
            <InputGroup
              label="Day"
              placeholder="DD"
              name="day"
              value={formValues.day}
              error={formErrors.day}
              OnInputChange={handleFormValues}
            />

            <InputGroup
              label="Month"
              placeholder="MM"
              name="month"
              value={formValues.month}
              error={formErrors.month}
              OnInputChange={handleFormValues}
            />

            <InputGroup
              label="Year"
              placeholder="YYYY"
              name="year"
              value={formValues.year}
              error={formErrors.year}
              OnInputChange={handleFormValues}
            />
          </div>

          <div className="button-container">
            <button type="submit" className="submit-btn">
              <img src="/icon-arrow.svg" alt="" />
            </button>
          </div>
        </form>

        ...
      </main>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

As shown in the code, now that I have an <InputGroup /> component, I can call it as many times as I want, and pass in the data I need via props. Since this is a simple application, it is not really an issue passing data via props, but there is a better way of sharing this data with each <InputGroup /> component without doing a lot of repetition. This is a good use case for the Context API, as I earlier explained, you would use the Context API when you need to share state that is used globally in your application.

Here's how we can rewrite this using the Context API. Let's first create our context:

import { createContext } from "react";
export const AgeContext = createContext(null);
Enter fullscreen mode Exit fullscreen mode

Now that we have our context, let's wrap the entire application inside a Context Provider, and pass in our data:

import InputGroup from "./components/InputGroup";
import { AgeContext } from "./context/AgeContext";

...

function App() {
  return (
    <>
      <AgeContext.Provider
        value={{
          formValues,
          formErrors,
          handleFormValues,
        }}
      >
        <main className="age-container">
          <form onSubmit={handleSubmit}>
            <div className="form-container">
              <InputGroup label="Day" placeholder="DD" name="day" />
              <InputGroup label="Month" placeholder="MM" name="month" />
              <InputGroup label="Year" placeholder="YYYY" name="year" />
            </div>

            <div className="button-container">
              <button type="submit" className="submit-btn">
                <img src="/icon-arrow.svg" alt="" />
              </button>
            </div>
          </form>

          ...
        </main>
      </AgeContext.Provider>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Let's update the InputGroup component to reflect this changes by using our context

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

function InputGroup({ label, placeholder, name }) {
  const { formValues, formErrors, handleFormValues } = useContext(AgeContext);

  return (
    <div className="input-group">
      <label
        htmlFor={label}
        className={formErrors[`${label}Err`] ? "text-error" : ""}
      >
        {label}
      </label>

      <input
        type="text"
        id={label}
        value={formValues[label]}
        onChange={handleFormValues}
        name={name}
        className={formErrors[`${label}Err`] ? "input-error" : ""}
        placeholder={placeholder}
      />

      <span>{formErrors[`${label}Err`]}</span>
    </div>
  );
}

export default InputGroup;
Enter fullscreen mode Exit fullscreen mode

You can see that it's easier to pass data around now that formValues and formErrors is being shared globally. This is basically how the context API works.

Conclusion

The Context API is very useful when building React applications, but it's important to know when to use it. The best time to use the Context API is when you see your application passes data that is being used across several components, using the Context API in this scenario will help a lot, and will avoid unnecessary re-renders. One thing to note though is that the Context API on it's own is not enough to avoid your components for re-rendering, you might need to use React hooks such as useMemo() and useCallback() to ensure your components re-render correctly. So how do you use the Context API?.

Top comments (1)

Collapse
 
shieldxbot profile image
shieldx •

Việc lạm dụng Context API để thay thế hoàn toàn props drilling thường dẫn đến vấn đề về hiệu năng vì mọi component tiêu thụ context đều sẽ re-render khi bất kỳ giá trị nào trong object context thay đổi. Một kinh nghiệm xương máu của mình là luôn chia nhỏ các context ra theo từng mục đích sử dụng thay vì gom tất cả vào một Provider khổng lồ. Nếu bạn chỉ cần truyền dữ liệu tĩnh hoặc dữ liệu ít thay đổi thì Context rất ổn, nhưng với những state thay đổi liên tục như dữ liệu form hay vị trí chuột, mình thường ưu tiên dùng các thư viện quản lý state chuyên dụng hoặc kết hợp với useMemo để tối ưu hóa việc render.