I have a fairly simple React component which renders two components within it: one is a big list and another is a modal. When a button within the list is clicked, the modal appears. This process is causing the list component to re-render, thus inducing the lag. There is a gap in my React knowledge as to why this is happening.
Here's the code:
const ExistingMargins = ({ margins }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [marginBeingEdited, setMarginBeingEdited] = useState();
return (
<>
<TableRows
margins={margins}
onEditButtonClicked={(marginToEdit) => {
setMarginBeingEdited(marginToEdit);
setIsModalOpen(true);
}}
/>
<EditModal
isModalOpen={isModalOpen}
closeModal={() => setIsModalOpen(false)}
margin={marginBeingEdited}
/>
</>
);
};
const TableRows = ({ margins, onEditButtonClicked }) => {
return (
<table>
{margins.map(margin => (
<tr key={margin.Id}>
<td>{margin.Name}</td>
<td>
<button onClick={() => onEditButtonClicked(margin)}>Edit</button>
</td>
</tr>
))}
</table>
);
};
My understanding: is that React applies a unique key to the two components being rendered here (TableRows
and EditModal
) and that, whenever any state variable changes, the component re-renders and updates the shadow-DOM. React then compares the new and previous shadow-DOM to detect changes. Any changes get rendered in the actual-DOM. Since nothing in TableRows
is changing, no laggy updates to the actual-DOM happens.
This isn't the case. Here's what is actually happening:
- The user clicks the Edit button in the
TableRows
component - The
isModalOpen
andmarginBeingEdited
state variables in the parent component get updated - The entire
ExistingMargins
component re-renders - ...including the
TableRows
component, which causes lag
My understanding is that TableRows
shouldn't re-render on the actual-DOM because no state variables being passed to it have changed.
This is not happening. I can see in Chrome's React Profiler that there's a big, slow re-render of the table rows.
Why is this happening? Memoisation might fix it, but where is my understanding incorrect?
CodePudding user response:
There are a couple of things going on:
TableRows
isn't memo-ized (viaReact.memo
for instance, since it's a function component; for a class component you'd inherit fromReact.PureComponent
). Non-memoized components re-render if their parent re-renders, even if their props don't change.But also:
Its props are changing, because of this code:
onEditButtonClicked={(marginToEdit) => { setMarginBeingEdited(marginToEdit); setIsModalOpen(true); }}
That creates a new function every time the parent is rendered, so
TableRows
sees a changed prop.
To solve that:
- Memoize the component
- Create the function once and reuse it (via
useCallback
,useMemo
, oruseRef
)
See my answer to Should we use useCallback in every function handler in React Functional Components? for more details.
Side note re:
My understanding is that TableRows shouldn't re-render on the actual-DOM because no state variables being passed to it have changed.
I think the term you're looking for there is "commit," not render. When React renders your component, your component function is called and returns an element tree. React then commits that tree to the DOM (doing a diff and only making the necessary changes).