preactjs / signals

Manage state with style in every framework
https://preactjs.com/blog/introducing-signals/
MIT License
3.71k stars 91 forks source link

`useSignalEffect` does not update when a signal reference changes #361

Closed fabian-hiller closed 1 year ago

fabian-hiller commented 1 year ago

There is a strange, but currently probably expected behavior when the reference of a signal passed as a prop changes. Here is a simple scenario:

A simplified real-world example: Preact RPEL

I noticed the problem when implementing Modular Forms for Preact. In order to have the ability to set a listener to the value at a specific index in a list, the signal must not change in SolidJS. Therefore, I swap the values instead of moving the signals in the list.

developit commented 1 year ago

The way to do this is to store the prop signal in a local signal - signals track value changes over time, not signal changes over time, so if you need to react to a signal reference being changed over time you need to store it in another signal.

I typically use a helper called useLiveSignal() for this, which creates a Signal whose value is updated on every render if the provided argument is different than the signal's current value.

/**
 * useSignal(), but rerendering with a different value arg updates the signal.
 * Useful for: const a = useLiveSignal(props.a)
 */
export function useLiveSignal<T = any>(value: T): Signal<T> {
  const s = useSignal(value);
  if (s.peek() !== value) s.value = value;
  return s;
}

Using that helper, here's how you'd write the code to make effects work the way you're wanting them to:

function Item(props) {
  // create a signal to hold the changing value of `props.value`: (which is itself a signal)
  const propValue = useLiveSignal(props.value);
  const value = useSignal('');
  useSignalEffect(() => (value.value = propValue.value.value));
  return <span>{value}</span>;
}
fabian-hiller commented 1 year ago

Thank you very much for your precise answer. I am now using a slightly modified version of useLiveSignal internally in Modular Forms, which ensures that the signals of a field within a list remain the same.

jaskp commented 1 year ago
export function useLiveSignal<T = any>(value: T): Signal<T> {
  const s = useSignal(value);
  if (s.peek() !== value) s.value = value;
  return s;
}

Is it okay to mutate it in the render function? In React, you can only call setState in the render function if it belongs to the same component, but changing the signal could trigger other component re-renders, right? Is it fine in Preact?

JoviDeCroock commented 1 year ago

As most things, blank statements have to be taken with a gran of salt ;) but yes you can do so in Preact as long as you don't create an infinite loop

XantreDev commented 1 year ago

I think useLiveSignal should be used on user side, because he exactly know that he will swap signals. I think in most cases signals should be static

XantreDev commented 1 year ago

Wrapping all props in useLiveSignal, mostly when some props can be optional can be tricky

leqwasd commented 10 months ago

Well, in react - this is one of the reasons you have dependencies. If you have component like this:

const Component: React.FC = () => {
    const ref = React.useRef<HTMLDivElement>(null);
    React.useEffect(() => {
        console.log(ref.current);
    }, []);  // Here you dont need ref as a dependency because both - useRef and useEffect will be alive for the same 
    return (
        <div ref={ref}>
            <ComponentWithWrapperRef parentRef={ref} />
        </div>
    );
};

const ComponentWithWrapperRef: React.FC<{ parentRef: React.RefObject<HTMLDivElement> }> = ({
    parentRef
}) => {
    React.useEffect(() => {
        console.log(parentRef.current);
    }, [parentRef]);  // Here you need parentRef as a dependency because the ref itself can change, not just the .current
    return <div />;
};

}

In my opinion, here - a dependency would be reasonable. Otherwise - how would you handle - if you would pass that signal down even further? Would you pass argument signal, and it would have to be wrapper there again in useLiveSignal ? Would you pass "liveSignal" - and then wrap it again in useLiveSignal? Then access it through .value.value.value.value.value?

XantreDev commented 10 months ago

I think useLinkedSignal is great for this type of cases. Because it unwraps signal deeply