Prerequisites

  • React > v16.8.0 (hooks are not supported below this version)
  • Yarn package manager (personal preference is applicable)

State management has always been a crucial part of a React application. The performance and experience of our application are dependent on state management. Our application might not work as expected if we cannot handle and reflect state changes.

States in React JS

Why is the state so important? Imagine you are browsing a website. You see a dark mode toggle button and click on it. But, nothing happened, or only some part of the application is dark. It feels weird; there might be various reasons. It might be possible that the state is not changed or reflected throughout the application.

State lets us store some values that reflect the current condition of a component. React re-renders the required component when the state is changed after doing the necessary changes. React has introduced various hooks that let us use the state and other lifecycle methods simply and efficiently.

Let's see how state sharing is done in a modern React application.

State Sharing in React Application

In a typical React application, we use props to share the states. This method allows us to share the state from parent to child components.

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

const CartDisplay = ({ noOfItems }) => {
  return <span>{noOfItems}</span>;
};

const CartOperator = () => {
  const [noOfItems, setNoOfItems] = useState(0);
  return (
    <div style={{ display: "grid", gridGap: "1rem" }}>
      <CartDisplay noOfItems={noOfItems} />
      <button onClick={() => setNoOfItems(noOfItems - 1)}>
        Decrement
      </button>
      <button onClick={() => setNoOfItems(noOfItems + 1)}>
        Increment
      </button>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <CartOperator />
    </div>
  );
}
Example Code

This leads to the problem of not being able to share states between the sibling components. For example, imagine the CartDisplay component inside the App component. Here we cannot share the state from CartOperator component to CartDisplay component.

To solve this issue, we can do something called state lifting. It is lifting the state from a component to its parent component. After that, we can pass the state from the parent to both the child components.

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

const CartDisplay = ({ noOfItems }) => {
  return <span>{noOfItems}</span>;
};

const CartOperator = ({ noOfItems, setNoOfItems }) => {
  return (
    <div style={{ display: "grid", gridGap: "1rem" }}>
      <button onClick={() => setNoOfItems(noOfItems - 1)}>Decrement</button>
      <button onClick={() => setNoOfItems(noOfItems + 1)}>Increment</button>
    </div>
  );
};

export default function App() {
  const [noOfItems, setNoOfItems] = useState(0);
  return (
    <div className="App">
      <CartDisplay noOfItems={noOfItems} />
      <CartOperator noOfItems={noOfItems} setNoOfItems={setNoOfItems} />
    </div>
  );
}
Code With State Lifting

This solves our issue, but what if we need to share state logic between a large number of components? Only using useState and doing prop drilling, state lifting might not sound practical.

We can use React Context API and custom hooks to share state logic globally. We also have many external libraries for state management, like recoil, redux, flux, etc. We should always examine our needs before using React context. This means making the state global, which it actually needs to be. For example:

  • Theme configuration that determines how the application will look depending on that configuration.
  • Shortcut keys set up those trigger actions that are divided throughout the application.
  • Authentication status, which might be used to render various components based on user.
⚠️
Using React Context API without its actual need might cause serious performance issues.

With that caution in mind, let's start learning about React Context API.

React Context API

React Context provides a way to share/pass data (including states) throughout the component tree. This removes the burden of needing to pass down props manually.

💡
It is always recommended to use context for only the data that needs to be globally accessible.

When to use React Context?

  1. State that needs to be globally accessible.
  2. Just follow point number 1 strictly.

Create Context (Theme Context)

Let's create a context that will share the theme state throughout our application.

export const ThemeContext = createContext();
A Segment of ThemeContextProvider.jsx

createContext takes an optional parameter for assigning a default value. It is undefined if we pass nothing.

⚠️
The default value is only used when the component that uses this context cannot find its corresponding context provider.

Context Provider (Theme Context Provider)

const ThemeContextProvider = ({
    children
}) => {
    // useState hook for storing our theme state and changing state
    const [isDark, setIsDark] = useState(false);
    const context = {
        isDark,
        setIsDark
    };
    return (
        <ThemeContext.Provider value={context}>{children}</ThemeContext.Provider>
    );
};
A segment of ThemeContextProvider.jsx

ThemeContext.Provider takes in a value prop. This prop makes the desired state accessible to its children components.

In our case, we are passing down isDark state and setIsDark method. This is our setup for context that will make our theme state (isDark) and method to change it ( setIsDark ) globally accessible.

import { createContext, useState } from "react";

export const ThemeContext = createContext();

const ThemeContextProvider = ({ children }) => {
  // useState hook for storing our theme state and changing state
  const [isDark, setIsDark] = useState(false);
  const context = {
    isDark,
    setIsDark
  };
  return (
    <ThemeContext.Provider value={context}>{children}</ThemeContext.Provider>
  );
};

export default ThemeContextProvider;
ThemeContextProvider.jsx

As mentioned before, we should wrap our components with the context provider. Not doing so will use the default value of the context (i.e., undefined for our case). So, let's wrap our App Component with ThemeContextProvider first.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import App from "./App";
// importing theme context provider
import ThemeContextProvider from "./ThemeContextProvider";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <ThemeContextProvider>
      <App />
    </ThemeContextProvider>
  </StrictMode>
);
index.js

Our context setup is completed. Let's look at how we can consume that context value using hooks.

💡
We can also use Context.Consumer for accessing the value.

Hooks

Hooks let us use state logic that can be implemented anywhere inside React functions. We can use hooks to separate component logic and make it reusable. Some examples of React built-in hooks are: useState, useEffect, useReducer, etc.

Making our custom hook

Let's implement a custom hook that gives us the theme state and method to toggle it.

import { useContext } from "react";
import { ThemeContext } from "./ThemeContextProvider";

const useTheme = () => {
  const { isDark, setIsDark } = useContext(ThemeContext);

  const toggleTheme = () => {
    setIsDark(!isDark);
  };

  return { isDark, toggleTheme };
};

export default useTheme;
useTheme.jsx

At first, we are using the useContext hook from React, which takes in a context. This hook returns its value provided by the nearest provider, i.e., ThemeContextProvider in our case. After that, we made a function to toggle the theme. Finally, we returned our theme state and the method to toggle it from our custom hook called useTheme.

We can use this custom hook anywhere inside our React functions. Let's see the implementation below:

import useTheme from "./useTheme";

const Nav = () => {
  const { isDark, toggleTheme } = useTheme();
  return (
    <nav>
      <button onClick={toggleTheme}>{isDark ? "Light" : "Dark"}</button>
    </nav>
  );
};
export default Nav;
Nav.jsx

We used our custom hook to access our current theme state via isDark variable. After that, we added a click event listener on the button. This event triggers the toggleTheme method from our custom hook.

import Nav from "./Nav";
import useTheme from "./useTheme";

export default function App() {
  const { isDark } = useTheme();
  return (
    <div
      style={{
        width: "100%",
        height: "100vh",
        backgroundColor: isDark ? "#000" : "#fff"
      }}
    >
      <Nav />
    </div>
  );
}
App.jsx

Conclusion

We can use React context API to make our state or data globally accessible. The context provider is responsible for passing down the value. All those components that subscribe to the provider will re-render when the state changes. When we have more context providers, we might re-render components that do not require the change. This is the main reason for the warning:

⚠️
Using React Context API without its actual need might cause serious performance issues.

Custom hooks can be used to separate component state logic and make them reusable. They can be used only inside the top-level React functions.

I hope you learned something new through this blog. Subscribe to learn even more!