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?

crevulus
  • 1,658
  • 12
  • 42
  • did you try force new array on setItems([...tempItems]); or Array.from(tempItems). What you use as key in items rendering function ? – Robert Apr 16 '21 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. – Emile Bergeron Apr 16 '21 at 15:52
  • Does this answer your question? [How do I update states onchange in an array of object in React Hooks](https://stackoverflow.com/questions/55987953/how-do-i-update-states-onchange-in-an-array-of-object-in-react-hooks) – Emile Bergeron Apr 16 '21 at 15:53
  • @EmileBergeron I agree - was racing against the clock. – crevulus Apr 17 '21 at 17:16

3 Answers3

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});
Sean Vieira
  • 155,703
  • 32
  • 311
  • 293
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!);
};
Rohan Pujari
  • 788
  • 9
  • 21
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.

Tala
  • 909
  • 10
  • 29
  • Pushing into the current state is an [anti-pattern in React](https://stackoverflow.com/q/37755997/1218980). The state should be treated as immutable. – Emile Bergeron Apr 16 '21 at 15:56
  • @EmileBergeron That's exactly what I said and did. – Tala Apr 16 '21 at 17:13
  • 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). – Emile Bergeron Apr 16 '21 at 17:18
  • @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. – Tala Apr 16 '21 at 17:19
  • 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. – Emile Bergeron Apr 16 '21 at 17:31