0

Consider the following React functional component called Test:

function Test() {
  const [data, setData] = React.useState(new Set());

  function handleClick() {
    data.add(42);
    setData(data);
  }

  return (
    <>
      <button onClick={handleClick}>ADD 42</button>
      <p>{data.has(42) ? 'in set' : 'not in set'}</p>
    </>
  );
}

The code above is affected by a common defect -- despite the intention of triggering a re-render via setData(data), nothing will happen in practice as React doesn't check the contents of what data refers to, and setData(data) doesn't change any state.

The behavior can be seen live here on StackBlitz.

One possible workaround to achieve the desired behavior is to use structuredClone as follows:

function handleClick() {
  data.add(42);
  setData(structuredClone(data));
}

Performing a deep copy of the data object will force React to perform a re-render. However, this solution performs an useless deep copy and it does not clearly communicate the intent of the developer.

What is the idiomatic way to force React to render again in a situation like the above?

For example, I would like something such as:

function handleClick() {
  data.add(42);
  markAsDirty(data);
}
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • Check the idea of Spreading the Set to a new Set to change its reference. So React will know that it's changed: https://stackoverflow.com/questions/58806883/how-to-use-set-with-reacts-usestate – webgodo Mar 16 '23 at 21:17

1 Answers1

2

You should be treating React state as immutable. This means that if you want to add something to a Set, create a new one.

function Test() {
  const [data, setData] = React.useState(new Set());

  function handleClick() {
    const copy = new Set(data);
    copy.add(42);
    setData(copy);
  }

  return (
    <>
      <button onClick={handleClick}>ADD 42</button>
      <p>{data.has(42) ? 'in set' : 'not in set'}</p>
    </>
  );
}
kelsny
  • 23,009
  • 3
  • 19
  • 48
  • This is pretty much what `structuredClone` achieves, but I want to avoid the cost of an unnecessary deep copy at run-time. – Vittorio Romeo Mar 16 '23 at 18:20
  • 1
    @VittorioRomeo It's not a "deep copy". It's a shallow copy. The values in the old set are simply passed to the new set. – kelsny Mar 16 '23 at 18:21
  • I see, so it's not as expensive as `structuredClone`. – Vittorio Romeo Mar 16 '23 at 18:25
  • a simple benchmark of array.slice vs set.new shows that it is 99.5% slower. Here is the benchmark i ran - https://gist.github.com/alexanderankin/7f631ae01a075f41df404f5cf3727239 – Dave Ankin Mar 17 '23 at 07:45