we have two options when we are dealing with inputs in react realm:
controlled components :
we update the value of the input by usingvalue
prop andonChange
eventuncontrolled component :
DOM takes care of updating the input value. we can access the value by setting aref
on the input
There is a chance that you have encountered a situation that whenever you type something into an input or textarea there is a delay (lagging) and the input update is very slow. It is rather annoying and a bad user experience.
This behavior is a side effect of using controlled components. let's see why and how we can mitigate the issue
underlying cause
In controlled components, there is a cycle an input goes through.on every keystroke, we change some state(it could be in a global state like Redux or by useState
hook), and React re-renders and set the input's value prop with the new state. This cycle could be expensive. That's why we face a delay while updating the input. another situation would be having a huge component that every keystroke causes the component to re-render.
examples:
there is a complex component (e.g., a big form with lots of inputs), and whenever the input changes, the whole component re-renders
a big web app with state management (e.g., redux, context) that on every keystroke changes something in the store that triggers a re-render of the whole app
bounce, debounce might work?
if we bounce updating the global state and getting back the same value would add a delay making the input much worse. although it would be great to use it with isolated component.bounceing and debouncing is effective whenever we want to call an API and we don't want to fetch loads of information on every keystroke.
solutions
there are a couple of ways that we could address this issue.
Change to uncontrolled component
let's assume we have a component with a couple of inputs :
function ComponentA() {
const [value1, setState1] = useState();
const [value2, setState2] = useState();
const [value3, setState3] = useState();
const handleSubmit = () => {
//do something
};
<form onSubmit={handleSumbit}>
<input value={value1} onChange={e => setState1(e.target.value)} />;
<input value={value2} onChange={e => setState2(e.target.value)} />
<input value={value3} onChange={e => setState2(e.target.value)} />
</form>;
}
let's assume we have a component with a couple of inputs. we can change the code to use the uncontrolled component then input doesn't need to go through the re-rendering phase to get the value back.
function ComponentB() {
const input1 = useRef();
const input2 = useRef();
const input3 = useRef();
const handleSubmit = () => {
// let value1=input1.current.value
// let value2=input2.current.value
// let value3=input3.current.value
// do something with them or update a store
};
return (
<form onSubmit={handleSubmit}>
<input ref={input1} />;
<input ref={input2} />
<input ref={input3} />
</form>
);
}
onBlur
we can update our state (or global state) with the onBlur event. although it is not ideal in terms of user experience
onInputBlur = (e) => {
//setting the parent component state
setPageValue(e.target.value);
}
onInputChange = (e) => {
/*setting the current component state separately so that it will
not lag anyway*/
setState({inputValue: e.target.value});
}
return (
<input
value = {this.state.inputValue}
onBlur = {this.onInputBlur}
onChange={this.onInputChange}
>
)
Isolated component
the optimal solution is to use an isolated input component and manage the input state locally
import { debounce } from 'lodash';
function ControlledInput({ onUpdate }) {
const [value, setState] = useState();
const handleChange = e => {
setState(e.target.value);
onUpdate(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}
function ComponentB() {
const input1 = useRef();
const input2 = useRef();
const input3 = useRef();
const handleSubmit = () => {
//do something with the values
};
return (
<form onSubmit={handleSubmit}>
<ControlledInput
onUpdate={val => {
input1.current = val;
// update global state by debounce ,...
}}
/>
;
<ControlledInput
onUpdate={val => {
input1.current = val;
// update global state by debounce ,...
}}
/>
;
<ControlledInput
onUpdate={val => {
input1.current = val;
//update global state by debounce ,...
}}
/>
;
</form>
);
}
we have the benefit of having a controlled component and not causing any unnecessary re-renders or going through an expensive one. we can make custom components that check for certain criteria and show success or error messages. now we can implement a bouncing, debouncing mechanism and update the global state or fetch an API. our input speed is natural and we wouldn't cause any unnecessary update or API calling on every keystroke.
I'd be happy to hear from you, let's connect on Twitter
Top comments (9)
I had client-side validation in mind while writing the example. I don't know remix does server-side or client-side but yeah that's much cleaner. Using just one ref and putting all those values in an object would be cleaner. I wanted to address all use cases, especially the one where an input changes the behavior of the app. so I think that example isn't super great I would edit the example
How is the last example suppose to work?
You want to assign a string (
val
) to a (potential) HTMLInputElement?Those input refs are just like box to store data.then you can use them to validate the inputs when the user submits.we could have used 'useState' in that case the componenet would re-renders.the whole idea is to use a controlled input those refs are just an example on how one can consume the 'val' data
Nicely put, I think that the refresh boundaries of components in React are often not considered or perhaps fully understood.
thank you, yeah I think there is not enough discussion about the subject
also thanks for the feedback
Much like @miketalbot is noting, one issue here is that the lifecycle is very intransparent. Isolated components are indeed a good solution for this (as is, in this particular example, just relying on good old browser interfaces with uncontrolled components as @lukeshiru is pointing out), but the crucial question this post doesn't answer is why that is the case.
The benefit to smaller components is that there is a lot more in the component tree that remains unchanged - and thus, a lot more that react can skip evaluating and rerendering.
thank you for your feedback, I 'll mention the reason in the article
Pretty old article but I tried having this isolated component approach in my project. I used a global zustand store to store input data and whenever input triggered change it ran this whole cycle of setValueInStore->storeGetsUpdated->whoeverListensToStoreMustUpdate->inputMustUpdate->inputValueUpdated. And in was especially noticable on slider inputs. And spearating input and isolating it helped, but a new issue occured: whenever you reset your store to default value your input UI doesn't get updated because you never listen to the data that's stored outside, you have isolation, one-way binding. If you start listening to it you introduce two-way binding again and the issue starts occurion again.
We ended up adding debounce, although I agree it's not the best soultion. Do you have any suggestions for it?