BlogReactJS

How to update state when props change in React components

Written by Codemzy on August 14th, 2023

If you need to update your state when a prop changes, the easiest way to do this is React is with the `useEffect` hook. In this blog post, we will look at three options including `useEffect, the `key` prop, and removing state.

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>
  );
};