1

State is defined like so:

const [items, setItems] = useState([] as CartItemType[]);
const [id, setId] = useState<number | undefined>();

In this case, id is totally useless. Don't need it in my app at all.

However, if I try to update items, the state variable doesn't change and the UI doesn't reload, unless I also update id:

useEffect(() => console.log("reload")); // only fires if I include setId

const clickItem = (item: CartItemType) => {
  let tempItems = data;
  // @ts-ignore
  tempItems[item.id - 1].animation =
    "item animate__animated animate__zoomOut";
  setItems(tempItems!); // "!" to get rid of ts complaint about possible undefined value 
  setId(item.id); // nothing happens if I don't include this
};

// ... inside the return, in a map fn
<Item
  item={item}
  handleAddToCart={handleAddToCart}
  clickItem={clickItem}
/>

// inside Item component
<StyledItemWrapper
  className={item.animation}
  onClick={() => {
    clickItem(item); // item = an obj containing an id and an animation property
  }}
>

Why is setId necessary here? What is it doing that setItems isn't?

4
  • did you try force new array on setItems([...tempItems]); or Array.from(tempItems). What you use as key in items rendering function ? Commented Apr 16, 2021 at 14:48
  • Using @ts-ignore and ! non-null assertion defeats the purpose of using TypeScript. You should try and fix the code instead of silencing the errors. Commented Apr 16, 2021 at 15:52
  • Does this answer your question? How do I update states onchange in an array of object in React Hooks Commented Apr 16, 2021 at 15:53
  • @EmileBergeron I agree - was racing against the clock. Commented Apr 17, 2021 at 17:16

3 Answers 3

3

The reason is because setState uses Object.is equality by default to compare the old and the new values and tempItems === items even after you mutate one of the objects inside of it.

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects.

You can solve this by only mutating a copy of the array:

let tempItems = [...data]; // You call it `data` here, but I assume it's the same as `items` above.

but you'll run into the same problem if anything depends on item changing, so then you have to copy everything, which is more expensive:

let tempItems = data.map(d => ({...d}));

The alternative is to only copy what you're going to mutate (or switch to an immutable datastructure library like Immer or Immutable.js):

let lastIndex = data.length - 1;
// Copy _only_ the value we're going to mutate
let tempItems = data.map((d, i) => i !== lastIndex ? d : {...d});
Sign up to request clarification or add additional context in comments.

1 Comment

This is the correct answer. I've made a shallow copy, and now it works without setting id.
2

Each time you call setItems you would have to pass it a new array if you want to render. If you mutate the same array then the equality checks that react does to see if things have changed will always tell that array hasn't changed so no rendering will take place.

Eg:)

let a = [{animation:  true}]
let b = a
a[0].animation = false
console.log(a === b) // This returns true

You can instead use map to loop over the array and return a new array.

const clickItem = (item: CartItemType) => {
  let tempItems = data.map((a, index) => {
    if (index !== (item.id - 1)) { return a }
    return {...a, animation: "item animate__animated animate__zoomOut"}
  })
  setItems(tempItems!);
};

Comments

1

Common mistake. I do this all the time. States are considered changed if the underlying object reference changes. So these objects don't change:

Given:

interface {
  id: number
  type_name: string
}
const [items, setItems] = useState([] as CartItemType[]);
let curItems = items;

When:

curItems.push(new CartItemType());
setItems(curItems);

Expected:

state change is triggered

Actual:

state change is not triggered

Now... When:

curItems.push(new CartItemType());
setItems([...curItems]);

Expected:

state change is triggered

Actual:

state change is triggered

Same goes for objects. The fact is if you change underlying properties of an object (and arrays, since arrays are objects with numerical property names) JavaScript does not consider the object is changed because the reference to that object is unchanged. it has nothing to do with TypeScript or React and is a general JS thing; and in fact this is what React uses as a leverage for their Ref objects. If you want them considered different, change the reference by creating a new object/array by destructing it.

5 Comments

Pushing into the current state is an anti-pattern in React. The state should be treated as immutable.
@EmileBergeron That's exactly what I said and did.
You're still pushing (mutating) before making a shallow copy, so it looks like it works, but it's still prone to failure (anti-pattern).
@EmileBergeron as you can clearly see, that is for demonstration purposes and showing why their code fails. Also note that this is a hook state. The way Sean Vieira copied/destructed state value in a temp variable and then set by the setter function is the way to do so.
I'm definitely talking about your "Now... when:" example, which looks like the solution you're suggesting in your answer, and which fails to avoid mutating the current state even though it would trigger a render.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.