by unconed on 5/30/2025, 9:03:18 PM
by SebastianKra on 5/30/2025, 6:10:59 PM
> Call it contrived, but maybe we want to add drawings to our tasks. With different brush colours
That's when you lift state up - either to the parent component, or put the value in local storage. Both as a user and as a developer, my mental model is that each item in the list has it's own form, rather than sharing one.
I haven't seen enough instances of this problem that I think it would warrant a change to the core api's. Maybe you could demonstrate more real-world examples?
A pattern that I sometimes like, is an internal state that is set only if the input is dirty. This has the advantage of preventing external updates from interrupting the user while they're in the middle of typing.
function NumberInput({ value, setValue }) {
const [tempValue, setTempValue] = useState(null)
function submit() {
const parsedValue = parseInt(tempValue)
setTempName(null)
if (parsedValue !== NaN)
setItemName(tempName)
}
return <Input value={tempValue ?? value} onChange={setTempValue} onBlur={submit} />
}
I've written a similar `useDerivedState` hook before, which is basically formalizing the lastValue !== value / setLastValue pattern that the docs teach you.
But there is a major blind spot. The reason you want the dependencies is to reset the state when the outside demands it. But only way you can reset such a state is if the dependency _changes_. So it's not possible to reset the state back to the _same_ value as before.
To do that, you either need to manually manage the component lifecycle with a `key={..}` on the outside, or, you need to add e.g. a `version={N}` counter as an extra prop, to handle the edge case. Except, at that point, it makes more sense to rely on `version` entirely.
The 'proper' solution I've found is to actually write code to do what the policy and etiquette demands. E.g. for a number input, you can't aggressively reformat the contents because that's unusable, but you can check if the current text parses to the same value as before (if it is a valid number). There is no way to hide this sort of nuance with a generic state hook, it's too context-specific.
What is most useful is to treat the bridge between controlled and uncontrolled state as its own wrapper component, e.g. called <BufferedInput>, which has a render prop to actually render the <input> itself. It accepts a `value / onChange` on the outside, but passes on a different `text / onChange` to the inside. Give it a `parse`, `format` and `validate` prop that take functions, and you can clean up a lot of messy input scenarios.