Imagine a React page that allows users to edit a bill (invoice). The bill has a fair number of editable fields, say 100. Additionally, a bill can have any number of line items. Let's imagine 100 of them, each with, say, 20 fields.
If the bill is the "parent," and is rendered by a React component, the line items are "children," each rendered by a "child" component.
I'm maintaining the state of the bill (and its line items) in the parent component, which feels right.
I want to use controlled input fields in the child (line item) component. However, this means, at least naively, that every keypress results in a state update in the parent (bill) component, and some re-renders, even if I use React.memo judiciously.
Performance has quickly gotten unacceptably slow, even with as few as 10 line items, like a 500-800ms lag on each keypress. I've done a fair bit of performance sleuthing, and have a couple of potential answers, but I'm not going to report them here, in order not to thwart any routes to the best answer.
There must be a common solution to this, no? I'd like to do it without one of the form libraries, though I'm not totally opposed to that.
The stripped-down example below, just to demonstrate the basic architecture. The example doesn't have a performance issue. :-)
Help! What's the magic that I'm missing to keep this performant?
const INITIAL_STATE = {
invoiceNumber: "ABC123",
customer: "Phil's Dills",
lineItems: [
{ index: 0, item: "Pickles", quantity: 2 },
{ index: 1, item: "Pickle Juice", quantity: 5 },
]
}
export function Bill() {
const [ bill, setBill ] = useState(INITIAL_STATE);
function updateBill(updatedLineItem) {
const newLineItems = [...bill.lineItems];
newLineItems[updatedLineItem.index] = updatedLineItem;
setBill({
...bill,
lineItems: newLineItems
})
}
return(
<div id="parent">
<h1>Bill {bill.invoiceNumber} for {bill.customer}</h1>
{bill.lineItems.map((lineItem) => (
<LineItem key={lineItem.index} line={lineItem} updateBill={updateBill} />
))}
</div>
);
}
function LineItem({ line, updateBill }) {
function updateField(e) {
updateBill({
...line,
[e.target.id]: e.target.value
});
}
return(
<div id="child">
<input id="quantity" value={line.quantity} onChange={updateField} />
<input id="item" value={line.item} onChange={updateField} />
</div>
);
}```
I think your issue actually is that you're recreating updateBill
every time this component re-renders, meaning every single child component will also re-render because they all receive updateBill
as a prop. Consider using useCallback
to memoize the updateBill
function:
const updateBill = useCallback(updatedLineItem => {
setBill(bill => {
const newLineItems = [...bill.lineItems];
newLineItems[updatedLineItem.index] = updatedLineItem;
return {
...bill,
lineItems: newLineItems
}
})
}, []);