Controlled form input with signal value that needs to respond to defaultValue changes #4443
-
I have a use-case where I have a form that can be edited by multiple users (the state is synced between clients via websockets). For form elements, I usually go for a controlled form input that receives a function Input( props ) {
const { defaultValue } = props;
const signal = useSignal( defaultValue );
return (
<input
onInput={ event => signal.value = event.target.value }
value={ signal.value }
/>
);
} But now, because other users can edit these inputs as well, the component needs to react to changes to the function Input( props ) {
// Init the local state with defaultValue.
const { defaultValue } = props;
const signal = useSignal( defaultValue );
// Update the value whenever defaultValue changes.
useEffect(() => signal.value = defaultValue, [ defaultValue ]);
return (
<input
onInput={ event => signal.value = event.target.value }
value={ signal.value }
/>
);
} This works, but the downside is that it causes an extra render pass:
According to React docs, you should not useEffect() to adjust some state when a prop changes, but rather change the state directly during a render:
React - You Might Not Need an Effect So I came up with this reusable hook that returns a signal that will immediately (= during render) change its function useSignalWithDefaultValue( value ) {
const prevRef = useRef( value );
const signal = useSignal( value );
if ( prevRef.current !== value ) {
prevRef.current = value;
signal.value = value;
}
return signal;
} So then my component looks like this: function Input( props ) {
const { defaultValue } = props;
const signal = useSignalWithDefaultValue( defaultValue );
return (
<input
onInput={ event => signal.value = event.target.value }
value={ signal.value }
/>
);
} So my questions are:
Thanks! Note(s):I know my example component here consists of a single |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 4 replies
-
Simplest solution, if you can get away with it: make Otherwise I don't think your setup here is unreasonable. |
Beta Was this translation helpful? Give feedback.
-
I just realized that one thing that I think I could do to improve on this is to not use function Input( props ) {
const { defaultValue } = props;
const signal = useSignalWithDefaultValue( defaultValue );
return (
<input
onInput={ event => signal.value = event.target.value }
value={ signal } // <= pass `signal`, not `signal.value`
/>
);
} I keep forgetting that preact signals will skip the render and just update the DOM if you pass a signal directly. Looks like this works for properties and attributes as well! With |
Beta Was this translation helpful? Give feedback.
-
This is a decent approach. I have a variant of this that doesn't allow the argument and signal value to diverge (any render overwrites if different) that I have been calling It's slightly different than yours in that it doesn't need the ref, but I think for your case what you came up with is actually pretty clever. Re your first question:
Yes. If you mutate useState values synchronously during rendering, Preact will immediately retry rendering your function component, up to 25 times. |
Beta Was this translation helpful? Give feedback.
This is a decent approach. I have a variant of this that doesn't allow the argument and signal value to diverge (any render overwrites if different) that I have been calling
useLiveSignal
:https://gist.github.com/developit/a72311c247756f24da5b22d19c9dad48#file-signal-utils-js-L46:L55
It's slightly different than yours in that it doesn't need the ref, but I think for your case what you came up with is actually pretty clever.
Re your first question:
Yes. If you mutate useState values synchronously during rendering, Preact will immediately retry rendering your function…