2

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>
  );
}
9
  • 1
    ArrayConsumer is the one that passes item into ArrayItem. If it does not rerender, react can't pass the new prop when you update specific item. Or when you add/delete item, it needs to rerender in order to execute the map method again. I don't think there is something that can be done to change this, this is how react works. ArrayConsumer have to rerender, but if you don't want to rerender all ArrayItems, you can try using React.memo Commented Oct 6, 2024 at 10:51
  • @OktayYuzcan I don't think you know how signals work, as your comment is quite incorrect. Signals are mutable state containers, you do not need rerenders in many cases. Commented Oct 6, 2024 at 23:42
  • I understand that not anything should rerender. But ArrayConsumer is the one that renders the list. If it does not rerender you can't add / delete. You have not provided an example how you update your array. Commented Oct 7, 2024 at 5:38
  • @OktayYuzcan You're confusing me with OP, and you absolutely can add & delete without rerendering. That's core functionality that signals provides. It bypasses rerenders. Commented Oct 7, 2024 at 6:09
  • You definitely can't. ArrayConsumer returns a fragment that has array of N items. This is what react-dom receives. If you want to add new item, react-dom has to rerender it again in order to receive fragment + M amount of items. The example below is the same. For is a component that rerenders every time the array changes. In the implementation it uses Map where it only appends, never removes, this will lead to memory leaks. The item is passes as key, I don't see how this gonna work, if the item is an object, it can't be used as key, if it is number there will be dublicate keys Commented Oct 7, 2024 at 8:40

2 Answers 2

1

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} />
        )}
      />
    </>
  );
}
Sign up to request clarification or add additional context in comments.

9 Comments

interesting, though not exactly? apart from the memory leak, it doesn't pass a signal to the ArrayItem
Not sure I follow, you want to pass a signal to <ArrayItem>? If so, you need to use nested signals, which your example doesn't show. Once you unwrap with .value, the result is no longer a signal. You'll need to provide more information.
yes that's what I wrote. I wonder if it makes sense to have nested signals, and if it is best practice. Technically I see it's very possible, practically it could be a pain
Yes, it's absolutely fine to use nested signals if that's what your case depends upon. FWIW, I am a maintainer of Preact Signals (though I usually stick to the Preact end of things).
github.com/EthanStandel/deepsignal and github.com/luisherranz/deepsignal are both great, though the latter is a bit different syntactically.
|
0

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.

Comments

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.