Uri Kutner
Uri Kutner

Reputation: 161

How to use React Signals with arrays?

I'm trying to use Signals (@preact/signals-react) to reduce re-rendering with large data objects. In my case, I'm getting object from a network request, and tends to change frequently with live updates.

for direct properties, it works well, reducing the number of rerenders:

export function Root(){
  const mySignal = useSignal({ sub: { val: 1 }, arr: [{id: 1, name: "bob"}] });

  return <div>
    <Counter signal={mySignal} /> 
    <Array signal={mySignal} /> 
  </div>
}

function Counter({ signal }) {
  const counter = useComputed(() => signal.value.counter);

  return (
    <div>
      <p>{counter}</p>
      <button
        onClick={() => {
          signal.value = { ...signal.value, counter: signal.value.counter + 1 };
        }}
      >
        Increment
      </button>
    </div>
  );
}

In this case, only Counter rerenders when incrementing 👌.
This is true also when we have useSignals().

However, with arrays the situation is different:

function ArrayConsumer({ mySignal }: { mySignal: MySignal }) {
  const arr = useComputed(() => mySignal.value.someArr);

  return (
    <>
      {arr.value.map((item) => (
        // renders and edits {item.name}
        <ArrayItem key={item.id} item={item} />
      ))}
    </>
  );
}

Because I use arr.value directly, any time the array changes, the whole list gets rerendered. I expected some sort of signalMap() method, but the docs only suggest using the signal.value there.

One solution is to wrap the individual list items in a Signal, but that seems like a bad practice? I really thought signals give similar performance and devexp to Mobx, but without lists support it's much less useful.

// ideal solution
function ArrayItem(item) {
  // assuming item is a signal too
  // although I'm missing a way to do that
  const name = useComputed(() => item.value.name);
  return (
    <div
      // pseudo code
      contentEditable
      onChange={(e) => (item.value = { ...item.value, name: e.target.value })}
    >
      {name}
    </div>
  );
}

Upvotes: 2

Views: 216

Answers (2)

math_lab3.ca
math_lab3.ca

Reputation: 156

Maybe not the nicer way of doing this, but I add an random ID key to new object in array, so when the array change and call a rerender, <Item key={obj.ID}...> block the rerender of existing Components.

Upvotes: 0

rschristian
rschristian

Reputation: 2967

The library is meant to be a core set of primitives, and therefore doesn't address utilities that can be added from userland. Such things are quite easy to build out yourself. Here's a helpful utility that Jason/developit wrote a while back:

const Item = ({ v, k, f }) => f(v, k);

/**
 * Like signal.value.map(fn), but doesn't re-render.
 */
export function For({ each, children: f, fallback }) {
    let c = useMemo(() => new Map(), []);
    return (
        each.value?.map(
            (v, k, x) => c.get(v) || (c.set(v, (x = <Item {...{ key: v, v, k, f }} />)), x)
        ) ?? fallback
    );
}

Now for your example, you can use it like so:

function ArrayConsumer({ mySignal }: { mySignal: MySignal }) {
  const arr = useComputed(() => mySignal.value.someArr);

  return (
    <>
      <For
        each={arr}
        children={(item) => (
          <ArrayItem key={item.id} item={item} />
        )}
      />
    </>
  );
}

Upvotes: 1

Related Questions