I recently wrote a post about using the key
prop to remount your component on a prop change. But with a warning - ⚠️ only re-render when you need to!
Re-rendering with the key
prop should be a last resort. It has its use cases, but there's an often better way to update the state in your component when props change in ReactJS.
Here are the options we will look at:
useEffect
key
prop- removing state
Let's start with an imaginary problem, so I can show you how each of these options will work.
See the Pen Broken State by Codemzy (@codemzy) on CodePen.
So what's going on here? We have a list of food, and we can like any food items that we, well, like 🤤.
Here are our components:
// items of food
let foodList = [
{ food: "🍕 Pizza", likes: 0 },
{ food: "🍟 Fries", likes: 0 },
{ food: "🥞 Pancakes", likes: 0 },
{ food: "🥑 Avacado", likes: 0 },
{ food: "🌭 Hot Dog", likes: 0 },
{ food: "🍔 Burger", likes: 0 },
{ food: "🥪 Sandwich", likes: 0 },
{ food: "🌮 Taco", likes: 0 },
{ food: "🥗 Salad", likes: 0 },
{ food: "🧇 Waffles", likes: 0 },
];
function Foods() {
let [position, setPosition] = React.useState(0);
function getNext() {
setPosition(foodList[position+1] ? position+1 : 0);
};
return (
<>
<FoodItem food={foodList[position].food} likes={foodList[position].likes} />
<div className="text-right">
<button onClick={getNext} className="my-2 px-4 py-2 border hover:bg-gray-200 rounded">Next ➡️</button>
</div>
</>
);
};
function FoodItem({ food, likes }) {
const [ count, setCount ] = React.useState(likes);
return (
<div className="rounded-lg border p-5 text-center my-2">
<h2 className="my-2 text-3xl font-bold">{food}</h2>
<button onClick={() => setCount(count+1)} className="my-2 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded">💖 x{count}</button>
</div>
);
};
We can switch foods in our Foods
component, which returns the FoodItem
with the food as a prop. But if you like an item, and then switch to another item, do you see how the likes get messed up?
That's because the count
state doesn't get reset when our food
prop changes. We still have the same likes count from the previous food!
Now let's fix it!
useEffect
to update state when props change
useEffect
is the most common and easiest way to update the state when your props change. This is what I use in most of my components.
So what do we need to do here? Well, when the FoodItem
component gets a new food, it needs to update the count
state to however many likes that food has.
In this example, it will always start at 0, but in the real world, this data might be saved in a database, for example, and could be any number.
function FoodItem({ food, likes }) {
const [ count, setCount ] = React.useState(likes);
React.useEffect(() => {
setCount(likes)
}, [food, likes]);
//...
);
};
Whenever the props food
or likes
change, we setCount
to the new likes
number.
We pass food as a dependency to useEffect
because we want to run this code whenever the food prop changes.
And we pass likes
as a dependency because, since we need the latest likes
number, useEffect
depends on it.
key
to update state when props change
And we can also use the key
prop to update the state. If your child component state changes based on one unique prop (like an ID), then the key prop will work.
You'll give the FoodItem
a key
prop <FoodItem key={foodList[position].food} ... />
and whenever that key
changes the component will re-render, keeping the state up to date!
In this case, the key
could be the food name, or even the position (since that is unique too). But beware of using position with the key prop if your list gets reordered at any time - things can get a little funky.
We will stick with the food name for this example!
// pass the key prop to FoodItem
function Foods() {
let [position, setPosition] = React.useState(0);
function getNext() {
setPosition(foodList[position+1] ? position+1 : 0);
};
return (
<>
<FoodItem key={foodList[position].food} food={foodList[position].food} likes={foodList[position].likes} />
<div className="text-right">
<button onClick={getNext} className="my-2 px-4 py-2 border hover:bg-gray-200 rounded">Next ➡️</button>
</div>
</>
);
};
// no changes here
function FoodItem({ food, likes }) {
const [ count, setCount ] = React.useState(likes);
return (
<div className="rounded-lg border p-5 text-center my-2">
<h2 className="my-2 text-3xl font-bold">{food}</h2>
<button onClick={() => setCount(count+1)} className="my-2 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded">💖 x{count}</button>
</div>
);
};
By giving the key
prop to FoodItem
, React knows to re-render the component whenever the key
prop changes.
Remove state or lift it up
Although this blog is about updating state when props change, which is a common problem - sometimes you might find you are keeping state in sync unnecessarily.
Of course, this is just a dummy example. But if this was real, you would be saving this data. And then our count
state needs to keep in sync with our likes
. But if we already have the likes
props, we don't need that data duplicated in the count
state.
And if that FoodItem
component didn't have any state in the first place, there wouldn't be any state to update!
If we were fetching data, from a server or database for example, then we would probably have a useEffect
to fetch that data (and store it in state). Like this:
React.useEffect(() => {
// get the data on position change
setData(foodList[position]);
}, [position])
So each time the position changes, we fetch the new data.
And each time we like a food item, we would save that data (to the server) and update our state to match...
function handleLike() {
foodList[position].likes++;
setData({ ...data, likes: foodList[position].likes });
};
Now we can just pass the data
, and the handleLike
function to FoodItem
and get rid of the count
state.
function Foods() {
let [position, setPosition] = React.useState(0);
let [data, setData] = React.useState(foodList[position]);
React.useEffect(() => {
// get the data on position change
setData(foodList[position]);
}, [position])
function getNext() {
setPosition(foodList[position+1] ? position+1 : 0);
};
function handleLike() {
foodList[position].likes++;
setData({ ...data, likes: foodList[position].likes });
};
return (
<>
<FoodItem food={data.food} likes={data.likes} handleLike={handleLike} />
<div className="text-right">
<button onClick={getNext} className="my-2 px-4 py-2 border hover:bg-gray-200 rounded">Next ➡️</button>
</div>
</>
);
};
function FoodItem({ food, likes, handleLike }) {
return (
<div className="rounded-lg border p-5 text-center my-2">
<h2 className="my-2 text-3xl font-bold">{food}</h2>
<button onClick={handleLike} className="my-2 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded">💖 x{likes}</button>
</div>
);
};