Jovi De Croock
Software Engineer
Controlled inputs
In the HTML world we don't really have a notion of controlled components, as this control is performed by JavaScript, this is an important thing to note while employing these concepts. A controlled input implies that we completely control the state in JavaScript with our rendering library.
The following is just an <input>
rendered by Preact, no state, no effects, just an initial value. Notice
how it's already interactable and will never cause a rerender in the component.
input rerenders: 0
We are typing and no updates are being performed because this is all happening in HTML, the button underneath
has a click-handler that will look at the ref
on this input and print out the value, notice how the represented
value in our component is being updated as well? An uncontrolled components syncs this up for us.
Uncontrolled issues
You might ask yourself where this need for controlled components comes from, validation and code manipulation of the value are some of the main contenders, let's look how this becomes an issue in VDOM-land.
We use
onInput
as that performs changes in real time whileonChange
is actually debounced by default, in React they patch this by changingonChange
to behave likeonInput
.
const Input = () => {
const [value, setValue] = useState('')
const onInput = (e) => {
if (e.currentTarget.value.length > 3) return
setValue(e.currentTarget.value)
}
return <input value={value} onInput={onInput} />
}
input rerenders: 0
A diff is Preact looking at the difference between the old and new state and update the DOM accordingly
We can see that even when we type more than 3 characters the input does not reflect this, you clearly see that the nodes
stop rerendering in VDOM and that when you click the button the state and ref value are very different.
This is happening because we never enter our diff, entering our diff happens as the result of a state-update, in this case a call to setValue
.
In the above we could replace return
with a setValue(value)
but that would still suffer from the same issue as we would bail out of diffing
due to equal state, oh damn you perforance optimizations!
This means that as long as we have no state-change we transition back to an uncontrolled component, our diffing is what actually performs the control of the DOM-node.
Going controlled
The above is one of the main pain points that we see when performing control from the VDOM world over an input, we don't really control it at this point as we have no way to "know what happened", this is an issue in Preact 10 but got implemented in Preact 11.
Conceptually what we needed was a way to reset the value after an event handler so when a diff is happening we store the previous value on the DOM-node, next we wrap the event-handlers and perform the user-code and afterwards check whether the value has changed versus the previous value.
const eventHandler = (e) => {
userSuppliedOnInput(e)
if (isInput && this.prevValue !== this.value) {
this.value = this.prevValue
}
}
This means that we have stopped the automatic updating of the value happening by the DOM and replaced it back to the old value, now whether or not the VDOM triggers a diff due to a state-update the value will be the expected one, this due to only two scenario's being possible:
- the user has updated state in the
onInput
--> triggers a diff with an updatedvalue
property --> the VDOM will update the DOM in the subsequent render - the user has bailed out of an update --> the value remains the same as it was before
This is a little caveat while our deterministic VDOM sometimes gives us some issues from a library point of view, I'd love to give a shoutout to react-hook-form for doing an amazing job at using uncontrolled inputs!
For the curious here is the PR introducing controlled inputs for Preact 11
As a bonus, we could have probably asked for a user-land implementation where people leverage a component like this:
const Input = () => {
const [value, setValue] = useState('')
const inputRef = useRef()
const onInput = (e) => {
if (e.currentTarget.value.length <= 3) {
setValue(e.currentTarget.value)
} else {
const start = inputRef.current.selectionStart
const end = inputRef.current.selectionEnd
const diffLength = Math.abs(e.currentTarget.value.length - value.length)
inputRef.current.value = value
// Restore selection
inputRef.current.setSelectionRange(start - diffLength, end - diffLength)
}
}
return <input ref={inputRef} value={value} onInput={onInput} />
}
input rerenders: 0
here we manually do the resetting that is now happening in the background for Preact!
Concluding
Hope this can shed some light as to why I prefer uncontrolled inputs
for both performance reasons
as well as it being built-in for the DOM. Implementing complex validation can be a bit tricky with
uncontrolled inputs but as shown in react-hook-form
it's very much possible. With uncontrolled
components we opt-out of the front-end diffing and fully rely on the DOM logic.
Feel free to hit me up on Twitter about this!