Same logic with different libraries works different.
React - ✅
https://codesandbox.io/s/react-controlled-input-egxgu
Preact - ❌
https://codesandbox.io/s/preact-controlled-input-kib3p
This is an inherent flaw to the inner workings of vdom and html for now, I'll see if I can find a solution to this problem but for now I can't see one yet.
Let me draw the situation:
This can be prevented by doing the following, this is a workaround for now and I'll take a look if we can fix this without patching the event system:
return <input value={value} onInput={onChange} maxLength={3} />;
@JoviDeCroock what does React do in this case? Binds keydown event and prevents it, whenever an input is controlled (there's value={value} on it)? Can't Preact do the same?
React ships big code bundles of event patching for these scenario's. I don't think adding a maxLength is that bad of a solution though. HTML traditionally offers all solutions but interpreting them from JS makes you overwrite all native events code.
Right. Preventing keydown wouldn't actually help because e.target.value couldn't be used anymore and you'd need to set up workarounds around e.which and that opens a whole new can of worms and you end up with what React does. So no easy fix I can see either :/.
One possibility is to re-render even on same-state change when that state controls input value (not sure how easy would be to determine that).
That's not possible without a flag or static code analysis (which is not possible with a runtime library)
@JoviDeCroock this is simple example... what if i want to format received value?
event.target.value.replace(/[^0-9]g/, '')
@Crysp nothing is stopping you from using maxLength for that too if I understand the problem you're trying to show well enough
@JoviDeCroock he's right --- in his example it should prevent writing numbers which it fails to do so due to this bug. maxLength doesn't have a say in this.
Similarly, if you wanted to allow only letters, you wouldn't be able to do so:
@JoviDeCroock as far as i understand, set doesn't use deep comparison for prev and next value.
i want to write only numbers into state. if i input abc and then input 1 state, value of field resets to 1. for first three characters set received empty string, compare default state and they are equal. but value of field still uncontrolled.
i think this not logical behaviour
@Crysp it seems the only workaround is to leverage the behavior that useState does shallow comparison, and use a non-primitive to force re-render:
const ControlledInput = () => {
const [data, setData] = useState({ value: "" });
const onChange = event =>
setData({ value: event.target.value.replace(/[^a-z]/gi, "") });
return <input value={data.value} onChange={onChange} />;
};
@dwelle thx, i'll use your variant until bug will be fixed
I am having a similar issue with value simply being ignored. Setting it still allows any input.
React: https://codesandbox.io/s/gallant-frog-eeul9
Preact: https://codesandbox.io/s/nifty-brahmagupta-0z15g
I am having issues with this as well, in the meantime I made this hook for creating controlled inputs:
import * as React from 'react'
/** preact/compat controlled input fix */
/**
* Preact/compat hook for fixing controlled inputs
* @param value string value of the state to bind to input
* @param onChange callback method invoked when changing input
*/
export const useControlledInput = (value:string, onChange: (newValue: string) => void) => {
const inputRef = React.useRef<HTMLInputElement>()
React.useEffect(()=>{
if(inputRef.current && value !== inputRef.current.value){
inputRef.current.value = value || ''
}
},[value])
React.useEffect(()=>{
if(inputRef.current){
// @ts-ignore - ts types are incorrect for onchange
inputRef.current.oninput = (ev) => onChange(ev.target.value)
}
}, [onChange])
return [
/** Bind to ref for input */
(el: HTMLInputElement|null) => {
if(!inputRef.current && el){
// @ts-ignore
el.oninput = (ev) => onChange(ev.target.value)
inputRef.current = el
if(el.value !== value) { el.value = value || '' }
}
}
]
}
example comp:
import React, { useState } from 'react'
import { useControlledInput } from '../hooks'
const TestInput = () => {
const [ controlledText, setControlledText ] = useState('')
const [ controlledTextRef ] = useControlledInput( controlledText, (changes) => {
// Do some validation
setControlledText(changes)
} )
return <input ref={controlledTextRef} />
}
I solved this issue by forcibly synchronizing the value of the input dom to the state.
<input value={x} onChange={evt => {
const slicedValue = evt.target.value.slice(0, 3);
setX(slicedValue);
evt.target.value = slicedValue;
}} />
I also encountered the same problem and I solved it like this
interface IInput {
value: number | string;
onInput: (e: any) => void;
id: string;
type: "text";
}
export default function Input(props: IInput) {
const handleInput = useCallback(
(event) => {
const newValue = event.target.value;
event.target.value = props.value;
const newEvent = {
...event,
target: { ...event.target, value: newValue },
};
props.onInput && props.onInput(newEvent);
},
[props.value]
);
return <input {...props} onInput={handleInput} />;
}
Most helpful comment
@Crysp it seems the only workaround is to leverage the behavior that
useStatedoes shallow comparison, and use a non-primitive to force re-render:https://codesandbox.io/s/preact-controlled-input-2-rvghz