How to mitigate useless React renders when large state is present but only one changes?

Viewed 56

I have a search query component. User types query, clicks run and executes it. However, due to how state is organized, it causes excessive re renders. Following is a simplification of it. This organization of the code feels natural to me. QueryEditor is normally a complex editor, hence it stores its own value. Similar for query results. I got the value of it on each change, updated the query in the parent component. However this causes all HTML elements in the parent component to re-render even though their state does not change.

First of all, not being very familiar with the concepts;

  • Is this expected behavior? What I've done might not be ideal, but causing every DOM elment to rerenders because parent state changed feels unncesseary to me. Or maybe I'm doing something wrong.

  • How would this be improved? Obviously thing to improve is not get every change as a callback, but I did not find a good way to call a method of child component to getValue() from a parent component.

Link if you want: https://codesandbox.io/s/purple-architecture-5kpx5?file=/src/App.tsx You can clearly see the performance impact with React DevTools and see the render time increases with the number of elements even the elements did not change but the query text changes.

function App() {
  console.log("Re-render?");
  
  const [name, setName] = useState("mustafa");
  const [query, setQuery] = useState("");
  const [data, setData] = useState<number[]>([]);

  const runQuery = () => {
    console.log("using query...", query);
    setData([...Array(data.length + 1000).keys()]);
  };

  return (
    <div>
      <h1>Hello {name}</h1>

      <p>Please enter your query</p>
      <QueryEditor
        onChange={(e) => {
          setQuery(e.target.value);
        }}
      />

      <br />
      <button onClick={runQuery}>Run Query</button>
      <hr />

      <h4>Results</h4>
      {data.length > 0 && <p>There are {data.length} results </p>}
      {data.length > 0 && <DataResults data={data} />}
    </div>
  );
}

interface EditorProps {
  onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
}

function QueryEditor({ onChange }: EditorProps) {
  return <textarea onChange={onChange} />;
}

function DataResults({ data }: { data: number[] }) {
  return (
    <div>
      <ul>
        {data.map((a) => (
          <li key={a}>Item {a}</li>
        ))}
      </ul>
    </div>
  );
}


2 Answers

useState will always cause re-rendering. When you type in the textarea, you update query which cause rerendering (it's the normal behaviour of useState). In your case, you don't use query until you click the button to run the query. You can change const [query, setQuery] = useState(""); by const query = useRef(""). Then your QueryEditor onChange function becomes

{(e) => {
  query.current = (e.target.value);
}}

This way you should not have all these re renders. PS : when you run the query, you must use query.current instead of just query

you can't change the parent's behaviour of re-rendering. That is the way react works, when the children has to re-render, then the parent has to re-render the child, thus being called again. Have a look at the docs to understand the logic behind re-rendering.

What you can do in this case, is to memoized the siblings that has huge data.

So if I do this:

const DataResultsMemoized = React.memo(DataResults);

Then use it in the parent:

      {data.length > 0 && <DataResultsMemoized data={data} />}

Then your sibling won't re-render every time you type in the textbox.

Here's code sandbox example, check that the console.log in the children doesn't go again when you type

Related