Originally published to Medium for JavaScript in Plain English on February 28, 2024
…I was like you once: learning the fundamentals of React, trying to understand what components are, along with key principles in creating them & how they communicate with each other. You have likely encountered by now the issue of prop drilling, the practice of sending data from a parent component through a series of child components, which may not even need the data, just so that some component(s) down the line can use it.
If you have not seen for yourself how this could lead to clunky code that can be hard to change or decipher, I hope you can imagine it. And, if you have any semblance of the crazy mind that I have, you may have surmised that there must be a better way to share data throughout a React app.
I am pleased to say that, yes, there are a number of ways to give all parts of a React app access to certain data, some of which you may remember hearing about, at least. There are State-management libraries, like Zustand or Redux, the concepts of which, I admit, I am only vaguely familiar with.
Fun fact: ‘Zustand’ is actually the German word for ‘state’ or ‘condition’
Knowing how to work with such libraries is useful, and it may be worthwhile to become acquainted with them, but React itself has a built-in way of sharing data throughout an app, and this is with the React Context API. In my experience so far, it is much simpler to understand from the beginning, so it may be a good idea to first understand this, then move on to learning various libraries.
Why use Context API?
Context API eliminates the need for the lifting up of State, it increases the understandability of the dependencies between components (which is great, especially if you are trying to make sense of someone else’s code), and it makes components more independent and thus, more reusable. As I will point out below, data can be accessed in a component in a single line of code by using a custom hook instead of a prop that has been passed in through an untold number of components.
How does Context API work?
Setting up Context API involves creating a Context, a Provider, which is where the data (be it State values & their setters, values derived from State, or methods) that is to be shared, is stored must then be created & implemented for that Context, then the Context is consumed in components as needed. I will take you through the steps I took to use Context API to handle information relating to a quiz. I used TypeScript, so just ignore that stuff if you’re a JavaScript purist.
1. In a file named quizContext.tsx
, create a Context & its Provider:
import { createContext, useState, ReactNode} from "react";
/* Any other necessary imports go here (useState & useEffect, which are often
used in Contexts, should be imported from 'react' above, if needed, as well
as any other things from 'react'): */
// Context is defined here:
export const QuizContext = createContext<TQuizContext | null>(null);
// Context's Provider is defined here:
export const QuizContextProvider = ({ children }: { children: ReactNode }) => {
// Define values to be shared, here:
/* These values are often State / derived-State values, setters, methods,
etc. */
const [currentQuestions, setCurrentQuestions] = useState<TQuizQuestion[] | undefined>();
const [questionIndex, setQuestionIndex] = useState<number>(0);
// Any more values to be shared...
/* Since several values are typically shared throughout the app from a
Context, it's my preference to put them all in an Object here, then pass this
Object to the 'value' property in QuizContext.Provider below. You could make
do without this Object below & directly pass all the to-be-shared values to
the 'value' property below, but it may look a bit sloppy. */
const quizContextValues: TQuizContext = {
currentQuestions,
questionIndex,
// Any further names of values to be shared, separated by commas, go here:
};
return (
// Pass Object containing to-be-shared values to 'value' property:
/* 'children' refers to the components that will have access to
the data from this context. They must be wrapped inside this Provider,
which we'll see in the next step */
// QuizContext is the name of the Context defined near the top of this file
<QuizContext.Provider value={quizContextValues}>{children}</QuizContext.Provider>
);
};
2. Implement the Context’s Provider
For the sake of simplicity, let’s pretend we’re making an app whose only function is to provide the user with a quiz to take. In main.tsx
, we need to import the Provider from above, then wrap the components that need its data, inside of it. In this case, it is the <QuizMain />
component, which, we’ll say, is the parent of some other components relating to the quiz.
These children of
<QuizMain />
(not seen here, but present in that component) will also have access to the data from<QuizContextProvider>
as long as<QuizMain />
is wrapped inside<QuizContextProvider>
. This data can be consumed in<QuizMain />
then passed to its children as props, or it can be consumed in its children by means of a custom hook, which we will see in the next step.
// Any other necessary imports...
// Import Context Provider
import { QuizContextProvider } from "./Contexts/quizContext.tsx";
// Import component that needs data from the Provider
import QuizMain from "./Components/QuizComponents/QuizMain/QuizMain.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<QuizContextProvider>
<QuizMain />
/* Add any other components requiring data from QuizContextProvider
& that are not children of QuizMain, here */
</QuizContextProvider>
</React.StrictMode>
);
<QuizMain />
, along with any other components inside <QuizContextProvider>
, corresponds to the {children}
value in the definition of QuizContextProvider
above, towards the end of the code block in Step 1.
3. Access the Context’s data!
Now comes the time to use the data from the Context. This is easily done if we create a custom hook for this purpose, so let’s see what that looks like:
// Import hook used to consume Context:
import { useContext } from "react";
// Import Context to be consumed:
import { QuizContext } from "../Contexts/quizContext";
// Define custom hook:
export const useQuizContext = () => {
// Consume Context & assign to variable:
const context = useContext(QuizContext);
/* If value of context is falsy, throw error (this happens if an attempt
is made to use the hook in a component not wrapped by the Context
Provider): */
if (!context) {
throw new Error("useQuizContext must be used inside QuizContext provider.");
}
// Else, return the consumed Context:
return context;
};
It’s a good idea to create hooks in their own, separate files. I named the file for the
useQuizContext
hookuseQuizContext.tsx
.
Now that we have a custom hook that contains the data in our Context, let’s use the hook to access some data from the Context inside of a component:
// Import hook:
import { useQuizContext } from "../../../Hooks/useQuizContext";
const QuizMain = () => {
// Destructure values from custom hook that are needed in this component:
const { currentQuestions, questionIndex } = useQuizContext();
/* These values, defined in QuizContextProvider in our first code block
above, are now available to use throughout this component */
...
}
So, as long as a component is, in this case, wrapped inside <QuizContextProvider>
, the useQuizContext
hook can be used to pull any data from QuizContext
.
When working on large projects, it is common to see multiple contexts being used. Their Providers can simply be wrapped inside of each other, like so:
// Any necessary imports...
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<MainContextProvider>
<AnotherContextProvider>
<QuizContextProvider>
<QuizMain />
</QuizContextProvider>
<AnotherContextProvider />
<MainContextProvider />
</React.StrictMode>
);
In the example above, <AnotherContextProvider>
& <QuizContextProvider>
would be able to access data from MainContext
by using its custom hook, since they are nested inside <MainContextProvider>
, and so on and so forth down the tree. As you may have gathered, Context Providers in the tree should be sorted from those that contain the most universal data to those that contain the least universal data.
I hope this article is an answered prayer for those of you who are sick of the infinite complexity of prop drilling. Whether you found this article helpful, or if you would suggest some changes to what I’ve written here, or if you would do things in a different way, I would love to hear from you, so feel free to leave a comment if you are so inclined.
Peace out & happy coding!
Top comments (0)