The problem with React props
Components are great — they allow you to easily organize different parts of your application into separate files. If you have a child component that needs a piece of state, simply lift that state up to the nearest parent component, and then pass it down as a prop. Here's an example:
// App.js
import IncrementButton from "./IncrementButton";
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<div className="container">
<h1>{count}</h1>
<div>
<IncrementButton setCount={setCount} />
</div>
</div>
);
};
export default App;
// IncrementButton.js
const IncrementButton = (props) => {
const incrementHandler = () => {
props.setCount((prevCount) => prevCount + 1);
};
return (
<>
<button onClick={incrementHandler}>+</button>
</>
);
};
export default IncrementButton;
...this is a simple application with two components: App
and IncrementButton
.
The parent component (App
) holds the count
state, which is used to display the current number. The setCount
function is passed down to the child component
(IncrementButton
) as a prop, where it can then control the state. This works great, but what if
the application looks something like this instead...
// App.js
import IncrementInterface from "./IncrementInterface";
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<div className="container">
<h1>{count}</h1>
<div>
<IncrementInterface setCount={setCount} />
</div>
</div>
);
};
export default App;
// IncrementInterface.js
import IncrementButton from "./IncrementButton";
const IncrementInterface = (props) => {
return (
<div>
<p>CLICK TO INCREMENT: </p>
<IncrementButton setCount={props.setCount} />
</div>
);
};
export default IncrementInterface;
// IncrementButton.js
const IncrementButton = (props) => {
const incrementHandler = () => {
props.setCount((prevCount) => prevCount + 1);
};
return (
<>
<button onClick={incrementHandler}>+</button>
</>
);
};
export default IncrementButton;
...this time, setCount
is passed down as a prop to the IncrementInterface
component, which then passes it down again to the IncrementButton
, where it can finally be used. This is a problem.
Why? Well, the IncrementInterface
component has no need for the setCount
prop — it's
simply passing it down to the IncrementButton
component. This process of sending props from a higher-level component
to a lower-level component is referred to as "prop drilling".
As you might imagine, this can quickly get messy if there's a lot of components. For example, if component "E" needs a piece of state from component "A", it needs to be sent all the way down as a prop, going through "B", "C", and "D", before finally reaching "E". Wouldn't it be great if you could just pass that state directly from component "A" to "E"? Well, you can!
Using the Context API
The React Context API is the solution to prop drilling. In a nutshell, the Context API is just a structure that allows you to share data across all levels of your application. Using the Context API is simple, and I'll show how by using the example from earlier.
Earlier I stated that I want to be able to pass down the setCount
function directly to the
IncrementButton
component, without going through the IncrementInterface
component at all. With the Context API, this can easily be achieved. First, I'll create a new folder in my application called
contexts
, which will hold all my contexts. Inside this folder, I'll create a new file called
countContext.js
, which is where I'll create the context. Here's what my file looks like after the
context is created:
// countContext.js
import { createContext } from "react";
export const countContext = createContext({});
...the createContext
function is imported from React and initialized with an empty object. Then,
it's exported as a variable called countContext
. Now, this context can be used in the parent component, which
in this case is App
...
// App.js
import IncrementInterface from "./IncrementInterface";
import { countContext } from "./contexts/countContext";
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<countContext.Provider value={{ setCount }}>
<div className="container">
<h1>{count}</h1>
<div>
<IncrementInterface />
</div>
</div>
</countContext.Provider>
);
};
export default App;
...first, the countContext
variable is imported into the component. Once imported, it's used just like any
other component, wrapping around everything. The Provider
keyword provides the ability to access
context changes. Next, the component is given a prop of value
, which will hold an object of any pieces of
data you want to access across your application. For my application, I want to be able to access setCount
from anywhere. Now, every component in between the countContext
tags (including children of those components)
will have access to setCount
. Since I want to use setCount
in
the IncrementButton
component, I need to import the context in that component, along with the useContext()
React Hook...
// IncrementButton.js
import { useContext } from "react";
import { countContext } from "./contexts/countContext";
const IncrementButton = (props) => {
const { setCount } = useContext(countContext);
const incrementHandler = () => {
setCount((prevCount) => prevCount + 1);
};
return (
<>
<button onClick={incrementHandler}>+</button>
</>
);
};
export default IncrementButton;
...the useContext()
Hook is used to create data that can be accessed throughout the component hierarchy
without prop drilling. Using it is easy: just destructure the variable(s) you got from countContext
,
which in this case is only setCount
. After that, it can be used anywhere in the
IncrementButton
component without the use of props.
Contexts aren't limited to just one component; if I wanted to, I could use setCount
across different
components. At the end of the day, the Context API should be used whenever
you have data that needs to be accessed throughout different components. It makes your codebase cleaner and removes the need for
prop drilling, making it great for state management. For a more complex application, you should probably use a library like Redux for
state management, which is much more powerful.
If you want to learn more about context, I recommend reading React's official documentation on the topic.