React re renders everything every time key is pressed

4.4k Views Asked by At

I have a chart and an input field below. Basically the user can ask a question and the chart will change accordingly. Here is how it looks like

enter image description here

Here is the sample of the code below (ignore CoreUI syntaxes)

<CRow>
   <CCol xs="12" sm="12" lg="12">
      <CCard id="answerScreen" style={{height: "500px"}}>
      {
      !tempResponse.loading?  tempResponse.data.map(value => (  
      <ChartRender
         key={uuidv4()}
         data={value} 
         />
      ))
      :  
      <Loader/>
      }
      </CCard>
   </CCol>
</CRow>

<CRow>
   <CCol xs="12" sm="12" lg="12">
      <CCard>
        
            <CCardBody>
               <CForm className="form-horizontal">
                  <CFormGroup>
                           <CInputGroup size="lg" className="input-prepend">
                              <CInputGroupPrepend>
                                 <CInputGroupText className="">@Ask</CInputGroupText>
                              </CInputGroupPrepend>
                              <CInput 
                              size="16" 
                              type="text" 
                              value={userInput || ""}
                              onChange={e=> handleTextBoxInput(e)}
                              onClick={e=> handleTextBoxClick(e)}   
                              onKeyPress={e => handleTextBoxEnter(e)}
                              id="userQuery"
                              />
                            
                              <CInputGroupAppend>
                                 <CButton color="primary">Send</CButton>
                              </CInputGroupAppend>
                           </CInputGroup>
                  </CFormGroup>
               </CForm>
            </CCardBody>
        
      </CCard>
   </CCol>
</CRow>

This is how I have defined my states

const [userInput, setUserInput] = React.useState("");
const [tempResponse, setTempResponse] = React.useState({
        data: []
})

I suspect the problem in this part of the code

<CInput 
   size="16" 
   type="text" 
   value={userInput || ""}
   onChange={e=> handleTextBoxInput(e)}
   onClick={e=> handleTextBoxClick(e)}   
   onKeyPress={e => handleTextBoxEnter(e)}
   id="userQuery"
/>

I even tried adding useCallback to onChange function like this

const handleTextBoxInput =  useCallback(e =>{   
        e.preventDefault();
        setUserInput(e.target.value)
}, [])

But no help. I even read about memo but not sure where or how to apply it in my situation. What am I doing wrong?

OBSERVATION

As mentioned by @Matthew ,arrow syntax creates a different callback each time which contributes to re rendering and hence must be removed.

But even after removing it, the chart is getting re rendered every time a key is pressed. I am using Chartjs which uses canvas internally. Is Chartjs a problem?

3

There are 3 best solutions below

2
On

On your input, you have two events firing on every keypress - onKeyPress and onChange - remove the onKeyPress.

I suspect that handleTextBoxEnter calls setTempResponse which updates tempResponse. Setting state that the UI depends on will trigger a re-render.

You should also handle form submissions using the onSubmit event. If an element has focus inside of a form and the enter button is pressed - it will fire onSubmit.

<form onSubmit={handleTextBoxEnter}></form>

Also, if a key changes React will re-render. You are calling a function in your key so it is updated on every update.

tempResponse.data.map((value, i) => (  
 <ChartRender key={`chart-${i}`} data={value} />
))

FYI manually creating a UUID for a key overkill.

export const YourComponent = (): JSX.Element => {
  const [userInput, setUserInput] = useState('');
  const [tempResponse, setTempResponse] = useState({ data: [], loading: true });

  useEffect(()=>{
    // handle initial data loading and set loading to false
  }, [])

  const handleSubmit = (e) => {
    e.preventDefault();
    setTempResponse(your_state_data);
  };

  // e.preventDefault shouldn't be used here and is not required
  const handleChange = ({ target }) => setUserInput(target.value);

  if (tempResponse.loading) {
    return <Loading />;
  }

  // action is set to # for iOS - an action is required to show the virtual submit button on the keyboard
  return (
    <>
      <form action="#" onSubmit={handleSubmit}>
        <input defaultValue={userInput} onChange={handleChange} type="text" />
        <button type="submit">Submit</button>
      </form>
      {!!tempResponse.length &&
        tempResponse.data.map((value, i) => (
          <ChartRender key={`chart-${i}`} data={value} />
        ))}
    </>
  );
};

4
On

You are correct about the problemetic code.

<CInput 
   size="16" 
   type="text" 
   value={userInput || ""}
   onChange={e=> handleTextBoxInput(e)} // performance issue
   onClick={e=> handleTextBoxClick(e)}  // performance issue 
   onKeyPress={e => handleTextBoxEnter(e)} // performance issue
   id="userQuery"
/>

When the above code is run multiple times, the functions will be re-created every time. So instead of doing that, the following is enough

<CInput 
   size="16" 
   type="text" 
   value={userInput || ""}
   onChange={handleTextBoxInput}
   onClick={handleTextBoxClick}
   onKeyPress={handleTextBoxEnter}
   id="userQuery"
/>

The useCallback hook returns, well, a callback function. You can simply use the return values as normal event callbacks.

0
On

<input defaultValue={userInput} onChange={handleChange} type="text" />

here userInput is a state which once a user enter a character the userInput state is updated via handleChange which causes rerender so instead remove

defaultValue={userInput} onChange={handleChange}

once the user click on the send button the use the value present in input field and do the necessary function

sorry for the late reply.