15 Mar 2023 ยท Software Engineering

    Mastering the Context API in React for State Management and Advanced Use Cases

    7 min read
    Contents

    Any frontend application will need to manage its own state in order to display data for the user. React offers the Context API out of the box, getting rid of the need to pass specific props manually at every level in the component tree. It enables us to make certain data available globally without needing to introduce third-party libraries such as Redux.

    In addition, by using the Context API, we get rid of a nasty problem: prop drilling. This term refers to passing state from a parent component down the component tree until it reaches a deeply nested component. Not only does this practice clutter our codebase, but it also becomes difficult to maintain.

    In this article, we are first going to learn how to use Context and how to access the data that it exposes down the component tree. Afterwards, we’ll write custom hooks to avoid prop drilling by leveraging the Context API and efficiently managing state in a scalable way.

    How to use Context

    The first step in using this API is to create a context, as shown below:

    const CatFactsContext = createContext<CatFactsContextProps>({
      catFacts: [],
    });

    CatFactsContext is the object that contains our context. It offers a Provider component that we will use for wrapping our component tree. In this way, everything that the context contains will be available to all its children. In our case, cat facts will be available to the entire component tree.Now, let’s make use of the built-in hook useContext:

    const CatFactDisplay = () => {
      const { catFacts } =  useContext(CatFactsContext);
      
      return (
        <ul>
          {catFacts?.map((fact, index) => (
            <li key={index}>{fact.text}</li>
          ))}
        </ul>
      );
    };

    In the snippet above, we consume the context from the provider. We are getting the facts array, which we then display to the user.

    Here is the complete code for this example:

    import React, { createContext, useContext, useEffect, useState } from 'react';
    
    interface CatFact {
      text: string;
    }
    
    interface CatFactsContextProps {
      catFacts: CatFact[];
    }
    
    const CatFactsContext = createContext<CatFactsContextProps>({
      catFacts: [],
    });
    
    const CatFactDisplay = () => {
      const context = useContext(CatFactsContext);
      if (!context) {
        return null;
      }
      const { catFacts } = context;
    
      return (
        <ul>
          {catFacts?.map((fact, index) => (
            <li key={index}>{fact.text}</li>
          ))}
        </ul>
      );
    };
    
    const App = () => {
      const [catFacts, setCatFacts] = useState<CatFact[]>([]);
    
      useEffect(() => {
        const fetchData = async () => {
          const response = await fetch('https://cat-fact.herokuapp.com/facts');
          const data = await response.json();
          setCatFacts(data);
        };
    
        fetchData();
      }, []);
    
      return (
        <CatFactsContext.Provider value={{ catFacts }}>
          <CatFactDisplay />
        </CatFactsContext.Provider>
      );
    };
    
    export default App;
    

    Generally, we’d use Context in order to pass down some info such as the currently selected language, the user name, and the theme variant (dark/light). But what if we could also make use of useContext to reuse code?

    Code reusability with Context

    Let’s say that we need to write a confirmation dialog to use in our app. It will probably have a title, some content, and two buttons (accept and cancel, probably). This dialog will show up everywhere in our app where we need the user to confirm a certain action. An obvious solution for accomplishing this is to add this component to every component where it is needed, but this could clutter up our code base. Instead, let’s take a look at a solution that uses Context and custom hooks.

    Let’s create a file named ConfirmationDialogContext where we will start working on our context provider and our custom hook. We are going to start by defining the relevant types and then we will create the context:

    type ConfirmationDialogState = {
      isOpen: boolean;
      confirmAction: () => void;
    };
    
    type ConfirmationDialogContextValue = {
      setDialogState: (state: ConfirmationDialogState) => void;
    } & ConfirmationDialogState;
    
    const ConfirmationDialogContext = React.createContext<
      ConfirmationDialogContextValue | undefined
    >(undefined);

    As you can see, ConfirmationDialogState contains the fields that are relevant for the dialog itself (such as being open or not, as well as what to do when the user confirms the action), whereas ConfirmationDialogContextValue will allow us to put these props inside of our provider.

    The provider itself will be a component that will accept children props, which will be the component tree for our app since this provider needs to be at the top level. Inside this component, we will keep the state of our dialog (isOpen and confirmAction), and we will return the provider for the ConfirmationDialogContext object that we have created above:

    const ConfirmationDialogProvider = ({ children }: { children: ReactNode }) => {
      const [dialogState, setDialogState] = useState<ConfirmationDialogState>({
        isOpen: false,
        confirmAction: () => null,
      });
    
      const confirmAction = () => {
        dialogState.confirmAction && dialogState.confirmAction();
        setDialogState({ isOpen: false, confirmAction: () => null });
      };
    
      return (
        <ConfirmationDialogContext.Provider
          value={{ ...dialogState, setDialogState, confirmAction }}
        >
          {children}
        </ConfirmationDialogContext.Provider>
      );
    };

    The custom hook that we will use in our components for making use of the confirmation dialog will look like this:

    const useConfirmationDialog = () => {
      const context = React.useContext(ConfirmationDialogContext);
      if (!context) {
        throw new Error(
          'useConfirmationDialog must be used within a ConfirmationDialogProvider'
        );
      }
      return context;
    };
    

    Just like in our cats example, we are returning the context so that we can access it as needed using the useContext hook. We need the nullability check to ensure that we do not work with an undefined context.

    At the end of our file, we can export the provider and the custom hook:

    export { ConfirmationDialogProvider, useConfirmationDialog };

    For the sake of simplicity, let’s go to our App.tsx file (or wherever the App component is defined in your project) and wrap our children with the provider. The children, in this case, will just be a small component we will write above our App. This component will have a button that will open the confirmation dialog.

    import React from 'react';
    import {
      ConfirmationDialogProvider,
      useConfirmationDialog,
    } from './ConfirmationDialogContext';
    import ConfirmationDialog from './ConfirmationDialog';
    
    const ConfirmationExample = () => {
      const { isOpen, setDialogState, confirmAction } = useConfirmationDialog();
    
      const handleButtonClick = () => {
        setDialogState({
          isOpen: true,
          confirmAction: () => console.log('Confirmed'),
        });
      };
    
      return (
        <div>
          <button onClick={handleButtonClick}>Open Confirmation Dialog</button>
          {isOpen && (
            <ConfirmationDialog
              confirmAction={confirmAction}
              cancelAction={() =>
                setDialogState({ isOpen: false, confirmAction: () => null })
              }
            />
          )}
        </div>
      );
    };
    
    const App = () => (
      <ConfirmationDialogProvider>
        <ConfirmationExample />
      </ConfirmationDialogProvider>
    );
    
    export default App;

    The ConfirmationDialog component could look something like this:

    import React from 'react';
    
    type ConfirmationProps = {
      confirmAction: () => void;
      cancelAction: () => void;
    };
    
    const ConfirmationDialog = ({
      confirmAction,
      cancelAction,
    }: ConfirmationProps) => (
      <div>
        <p>Are you sure you want to proceed?</p>
        <button onClick={confirmAction}>Yes</button>
        <button onClick={cancelAction}>No</button>
      </div>
    );
    
    export default ConfirmationDialog;

    And this is it. Now we can invoke the useConfirmationDialog in any component where we need the confirmation of the user. This leads to more readable code and it allows us to avoid adding the ConfirmationDialog component throughout the entire code base.

    Here is the complete confirmation dialog example if you would like to try it out.

    Conclusion

    In this article, we understood what the Context API is and how it is generally used in React. We looked at a basic example with the createContext and useContext hooks in order to understand how to provide values using the Context provider, and then we went over a more advanced use case of the context provider: creating a component that enabled us to reuse code by the means of a custom hook.

    Now you should be able to use Context to avoid prop drilling in your project, as well as finding more advanced use cases where this API could simplify your code.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I am a software developer specialized in React and React Native. I enjoy challenging tasks and I write about them to help others.
    Avatar
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.