简体   繁体   中英

How to prevent child re-render on every parent state update done though input field in React?

I am building a basic chat application. I have a parent component ( Console.js ) that contains three child components

  • QuestionCard.js
  • AnswerCard.js
  • InputCard.js

This is how the basic layout looks like

const Console = () => {
    // initial state for input field
    const [userInput, setUserInput] = useState("");

    // fetch previous conversation history
    const conversationHistory = useSelector(state=> state.conversationHistory.data)

    conversationHistory.map((info, idx) => (
        
    info.type == "statement" ? 
        <span key={idx}>
            <QuestionCard    
                data={info}
                
            />
         
        </span>

    : info.type == "answer" ?
        <span key={idx}>
            <AnswerCard 
                userInput={userInput}
                setUserInput={setUserInput}
                data={info}
            />
        </span>
    :
        <span></span>

    ))

    <InputCard
        userInput={userInput}
        setUserInput={setUserInput}
    />
}

Specifically in the InputCard.js child component, there resides the input field where the user types

const InputCard = ({userInput, setUserInput}) => {
    const handleTextBoxInput = e => {   
        setUserInput(e.target.value)
    
    }

    return (
        <input
             type="text" 
             value={userInput || ""}
             onChange={handleInput}
             id="userQuery"        
         />           
    )
}

The problem here is that every time I press a key, all the child components ( QuestionCard.js , AnswerCard.js , InputCard.js ) re-renders.

I read about memo and it is one way to ensure components don't re-render but needs something to compare against. So I understand I need to compare the userInput state before and after and check if indeed something changed. But I just don't know where do I do this comparison or whether to even use memo

Can anybody help me with this?

Note: I understand I can put the setState inside the InputCard component and re-rendering will stop but as you can see, I need the setState variables inside the AnswerCard too for some processing.

That's how React works AFAIK. A change in props, triggers a render of the component.

That said,

Here are some suggestions for your problem:

Debounce

One of the common patterns around handling user input is to debounce it. This prevents the component re-render on every key press. You can tailor the debounce timer to suit your use-case:

 const Border = { RED: { border: '1px solid red', margin: '12px 0', padding: '4px' }, GREEN: { border: '1px solid green', margin: '12px 0', padding: '4px' }, BLUE: { border: '1px solid blue', margin: '12px 0', padding: '4px' }, MAGENTA: { border: '1px solid magenta', margin: '12px 0', padding: '4px' } }; const MARGIN = { margin: '12px' }; function useDebounce(value, delay) { // State and setters for debounced value const [debouncedValue, setDebouncedValue] = React.useState(value); React.useEffect( () => { // Update debounced value after delay const handler = setTimeout(() => { setDebouncedValue(value); }, delay); // Cancel the timeout if value changes (also on delay change or unmount) // This is how we prevent debounced value from updating if value is changed ... // .. within the delay period. Timeout gets cleared and restarted. return () => { clearTimeout(handler); }; }, [value, delay] // Only re-call effect if value or delay changes ); return debouncedValue; } const useRenderCounter = (thing) => { const renderCount = React.useRef(1); React.useEffect(() => { renderCount.current += 1; }); return `Render count for ${thing} ${renderCount.current}`; }; const InputCard = ({ userInput, setUserInput }) => { const renderCount = useRenderCounter('InputCard'); const [input, setInput] = React.useState(userInput); const debouncedValue = useDebounce(input, 750); React.useEffect(() => { setUserInput(debouncedValue); }, [debouncedValue]); return ( <div style={Border.MAGENTA}> <span>{renderCount}</span> <div style={MARGIN}> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} id="userQuery" /> </div> </div> ); }; const QuestionCard = ({ userInput, setUserInput }) => { const renderCount = useRenderCounter('QuestionCard'); return ( <div style={Border.RED}> <span>{renderCount}</span> <div style={MARGIN}>User Input: {userInput}</div> </div> ); }; const AnswerCard = ({ userInput, setUserInput }) => { const renderCount = useRenderCounter('AnswerCard'); return ( <div style={Border.GREEN}> <span>{renderCount}</span> <div style={MARGIN}>User Input: {userInput}</div> </div> ); }; function App() { const renderCount = useRenderCounter('App'); const [userInput, setUserInput] = React.useState(''); return ( <div style={Border.BLUE}> <span>{renderCount}</span> <QuestionCard userInput={userInput} setUserInput={setUserInput} /> <AnswerCard userInput={userInput} setUserInput={setUserInput} /> <InputCard userInput={userInput} setUserInput={setUserInput} /> </div> ); } ReactDOM.render(<App />, document.getElementById("react"));
 span { padding: 4px; font-style: italic; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script> <div id="react"></div>

useRef

However, if you need the other components to not re-render on entering text , you can leverage useRef hook and keep track of the user input without having to re-render the component tree.

But bear in mind, with this approach, in order for other components to show the updated value on the DOM , there has to be a trigger to cause a re-render. In the example, the state is updated on button click which then triggers the re-render. Since you mentioned you are building a chat app, maybe you'd find this pattern useful.

 const Border = { RED: { border: '1px solid red', margin: '12px 0', padding: '4px' }, GREEN: { border: '1px solid green', margin: '12px 0', padding: '4px' }, BLUE: { border: '1px solid blue', margin: '12px 0', padding: '4px' }, MAGENTA: { border: '1px solid magenta', margin: '12px 0', padding: '4px' } }; const MARGIN = { margin: '12px' }; const useRenderCounter = (thing) => { const renderCount = React.useRef(1); React.useEffect(() => { renderCount.current += 1; }); return `Render count for ${thing} ${renderCount.current}`; }; const InputCard = ({ userInput, setUserInput }) => { const renderCount = useRenderCounter('InputCard'); const [input, setInput] = React.useState(userInput); React.useEffect(() => { setUserInput(input); }, [input]); return ( <div style={Border.MAGENTA}> <span>{renderCount}</span> <div style={MARGIN}> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} id="userQuery" /> </div> </div> ); }; const QuestionCard = ({ userInput, setUserInput }) => { const renderCount = useRenderCounter('QuestionCard'); return ( <div style={Border.RED}> <span>{renderCount}</span> <div style={MARGIN}>User Input: {userInput}</div> </div> ); }; const AnswerCard = ({ userInput, setUserInput }) => { const renderCount = useRenderCounter('AnswerCard'); return ( <div style={Border.GREEN}> <span>{renderCount}</span> <div style={MARGIN}>User Input: {userInput}</div> </div> ); }; function App() { const renderCount = useRenderCounter('App'); const inputRef = React.useRef(''); const setUserInput = (input) => { inputRef.current = input; }; const [submit, onSubmit] = React.useState(''); return ( <div style={Border.BLUE}> <span>{renderCount}</span> <QuestionCard userInput={inputRef.current} setUserInput={setUserInput} /> <AnswerCard userInput={inputRef.current} setUserInput={setUserInput} /> <InputCard userInput={inputRef.current} setUserInput={setUserInput} /> <button type="submit" onClick={() => onSubmit(inputRef.current)}> Trigger Render </button> </div> ); } ReactDOM.render(<App />, document.getElementById("react"));
 span { padding: 4px; font-style: italic; }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script> <div id="react"></div>

React.memo()

React.memo() is used to memoize a component, based on the dependencies passed to it. It takes a component as a prop and returns a component that prevents a component from re-rendering if the props (or values within it) have not changed.

It is important to keep in mind that your code must not depend on React.memo() just to avoid re-renders. You should be able to replace React.memo() with direct component calls and it must not affect anything else, except performance.

This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs. - Docs

I honestly do not see any benefits by using a memo for your case. Anyways, this is ideally how you can memoize a component:

  const QuestionCardMemoized = React.memo(
    () => QuestionCard({ userInput, setUserInput }),
    [userInput, setUserInput]
  );

  const AnswerCardMemoized = React.memo(
    () => AnswerCard({ userInput, setUserInput }),
    [userInput, setUserInput]
  );

  return (
    <div>
      <QuestionCardMemoized />
      <AnswerCardMemoized />
      <InputCard userInput={userInput} setUserInput={setUserInput} />
    </div>
  );

Do keep in mind that React is VERY fast. Unless you see any performance issues by measuring using profiler tools, I'd say spend your time on something more useful than unnecessary/pre-mature optimization.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM