Search code examples
reactjsuse-effect

In React, why are my changes in a useEffect object not reflected in browser?


I have an object containing an array that I use to generate a list of inputs. In this example, the initial value is aaa bbb ccc. Here is the initial value of the object (full code later).

{ name: "tttt", versions: [{ name: "aaa" }, { name: "bbb" }, { name: "ccc" }

here's a screenshot after I typed "cf" after bbb. As you can see, only the c appears in the UI, the "f" does not. (only showing the .versions array, trying to keep it as concise as possible)

screenshot 1

I update the value of the object using useState. I also monitor it using useEffect, the "f" char does appear there, see screenshot of the console.

enter image description here

Here's the full code

import React, { useEffect, useState } from "react";
import "./App.css";

function Test() {
  const [product, product_set] = useState({});
  const [currentVersionIndex, currentVersionIndex_set] = useState(0);
  useEffect(() => {
    console.log("this should only run once ===================");
    product_set({ name: "tttt", versions: [{ name: "aaa" }, { name: "bbb" }, { name: "ccc" }] });
  }, []);
  useEffect(() => {
    console.log("Step test uE[] product=", product);
  }, [product]);
  useEffect(() => {
    console.log("Step test uE[] currentVersionIndex=", currentVersionIndex);
  }, [currentVersionIndex]);


  const handleEditVersion = async (ev) => {
    const newValue = ev.target.value;
     currentVersionIndex_set(parseInt(ev.target.id));
     const idx = parseInt(ev.target.id);
    // I tried both with idx and with currentVersionIndex
    let v = product.versions;
    v[idx].name = ev.target.value;
    const selObj = product;
    selObj.versions = v;
    product_set(selObj);
    // versionsList_set(v)
  };

  return (
    <div>
      <ul>
        {product &&
          product.versions &&
          product.versions.map((item, index) => (
            <li>
              <input type="text" 
                value={item.name} 
                key={`AAAAA_${index}`} 
                id={index} 
                onChange={handleEditVersion} 
              />
            </li>
          ))}
      </ul>
    </div>
  );
}
export default Test;

Solution

  • Problem:

    Line 4 and Line 7 in handleEditVersion function actually mutates state because the v and selObj variable is a reference to state. React state should be treated as immutable.

    Solution:

    Spread operator creates a separate copy of the object that’s stored in state. Now it is safe to mutate the object on line 5 and line 8 — it’s a completely separate object from the object in state.

      const handleEditVersion = (ev) => {
        currentVersionIndex_set(parseInt(ev.target.id));
        const idx = parseInt(ev.target.id);
    
        // Wrong: let v = product.versions;
        let v = [...product.versions]; // You have to copy the state data by using spread operator
        v[idx].name = ev.target.value;
    
        // Wrong: const selObj = product;
        const selObj = { ...product }; // You have to copy the state data by using spread operator 
        selObj.versions = v;
        product_set(selObj)
      }