Problems with the change event on a color input in React
Way back in the day, HTML offered a very limited set of components for capturing input from a user. Over the years this has changed, with new controls being introduced and finding widespread support across the various browsers. One of these new-ish input types (new-ish if you've been writing webapps for 20 years or so, anyway) is the color input:
You can click that color bar and you'll get a popup from which you can select a new color. When you change the color, the span next to it will update to tell you what the current color is.
Available events
According to MDN there are a couple of events you can hook into:
- input input is fired on the input element every time the color changes
- change The change event is fired when the user dismisses the color picker
In React, you'd use onInput or onChange to bind to these events.
onInput
For some usage scenarios, like if we want to immediately respond to the change in color as the user is using the color picker, it seems the input event would be a good choice. We can bind to that via React's onInput attribute:
export function ColorExample1() {
const [color, setColor] = useState("#442288")
return <div>
<input type="color" value={color} onInput={(e) => setColor(e.target.value)}/>
<span style={{color:color}}>{color}</span>
</div>
}
This is the code we're using in the example above.
onChange
But what if we only want to take action when the user has made their final choice, and dismissed the picker? Then it seems the change event would be a good choice. We can bind to that via React's onChange attribute:
export function ColorExample2() {
const [color, setColor] = useState("#442288")
return <div>
<input type="color" value={color} onChange={(e) => setColor(e.target.value)}/>
<span style={{color:color}}>{color}</span>
</div>
}
What we're expecting to see here is the the label to the right of the color picker will change after the user has dismissed the color picker. Try it:
But what do we actually see? The color changes immediately - the same behaviour as if we had used onInput. This is not the behaviour we wanted or expected.
Binding to the change event
In order to get the behaviour we want, we have to bind to the change event using the bare DOM approach:
export function ColorExample3() {
const input = useRef(null)
const init = useRef(false)
const [color, setColor] = useState("#442288")
useEffect(() => {
if(!init.current) {
init.current = true
input.current.addEventListener("change", (e) => {
setColor(e.target.value)
})
}
})
return <div>
<input type="color" defaultValue={color} ref={input}/>
<span style={{color: color}}>{color}</span>
</div>
}
Try opening the color picker here and selecting a few colors - you'll see the color picker update, but the label to the right of the picker will not be updated until you close the picker:
Summary
React has a very powerful event binding mechanism, but in this particular case it doesn't behave correctly. Fortunately you can always fall back to the DOM. The DOM will (almost) never let you down.