DEV Community

Cover image for How to solve input delay (lagging) in react
keyvan
keyvan

Posted on

How to solve input delay (lagging) in react

we have two options when we are dealing with inputs in react realm:

  1. controlled component
  2. uncontrolled component

controlled components :
we update the value of the input by using value prop and onChange event

uncontrolled component :
DOM takes care of updating the input value. we can access the value by setting a ref 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.

slow input example

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.

cycle illustration

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>
   );
}
Enter fullscreen mode Exit fullscreen mode

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}
      >
   )
Enter fullscreen mode Exit fullscreen mode

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>
   );
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
kevinkh89 profile image
keyvan

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

Collapse
 
lisbethmarianne profile image
Katrin • Edited

How is the last example suppose to work?
You want to assign a string (val) to a (potential) HTMLInputElement?

onUpdate={val => {
  input1.current = val;
  // update global state by debounce ,...
}}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kevinkh89 profile image
keyvan

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

Collapse
 
miketalbot profile image
Mike Talbot ⭐

Nicely put, I think that the refresh boundaries of components in React are often not considered or perhaps fully understood.

Collapse
 
kevinkh89 profile image
keyvan

thank you, yeah I think there is not enough discussion about the subject

Collapse
 
kevinkh89 profile image
keyvan

also thanks for the feedback

Collapse
 
fjones profile image
FJones

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.

Collapse
 
kevinkh89 profile image
keyvan

thank you for your feedback, I 'll mention the reason in the article

Collapse
 
metammodern profile image
Buniamin Shabanov

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?