React: how to maintain performance when updating a large parent record from a controlled child input field?

scrozier :

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>
    );
}```
Nick :

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
    }
  })
}, []);

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=7102&siteId=1