26

I'm trying to a large React form, but each input in the form is complex and has a complex state. I keep the state of all of the inputs in a parent component, and each input is wrapped in a child component. I have a state array in parent that contains the current value of the state for all of the input elements. Currently, every time there's an onChange in the input, I try to reset the entire state array in the parent via setState(). This was okay with up to 5 inputs, but once I got above that (like a hundred inputs), I started to notice some serious lag in the program. Please note: the program will also allow you to rearrange, delete, and add inputs, so the state needs to accommodate those changes Ie. the first input could swap positions with the 2nd input or be inserted right after the 10th input.

My objective is to find a way to optimize the performance of this onChange. Ultimately, I don't really need the data to be in the parent component, I just need to collect the values for the inputs when I click save at the bottom of the page.

Just to reiterate, I have two components.

  1. A Parent Component

  2. A Child Component

The Child component is basically an input where users can write hundreds of lines of text.

The Parent component contains 100s of the children and essentially looks like this:

export default function Parent(props) {
  const [state, setState] = useState(createDummyData());
  useEffect(() => {});



  const onInputChange = (value, index) => {
    var tempValue = [...state];
    tempValue[index] = value;
    setState(tempValue);
  };

  return (
    <>
      <div style={{ display: "flex", flexDirection: "column" }}>
        {state.map((item, index) => (
          <Child
            value={state[index].content}
            index={index}
            onChange={onInputChange}
          />
        ))}
        <button style={{backgroundColor: "red"}}>save input data</button>
      </div>
    </>
  );
}

The child component looks like this

export default function Child(props) {
  useEffect(() => {});

  const onChange = event => {
    props.onChange(event.target.value, props.index);
  };

  return (
    <>
      <input value={props.value} onChange={onChange} />
    </>
  );
}

I haven't found a simple way around this. Some seem to suggest using Redux, others seem to say use a combination of useMemo and useEffect to prevent rerenders. Your help would be much appreciated.

One thing I noticed is that if I try to keep individual state within the Child components, they render onChange much faster. This is probably because it doesn't have to setState for the parent state array each time. If possible, I'd like to be able to simply go through and grab the state of the child nodes when I click save. Would I use a ref in this case? Is it possible to do it without a ref?

I'd also like to AVOID using onBlur() just for the purpose of this project

The codesandbox is copied down below for reference: https://codesandbox.io/s/cocky-knuth-jm8n6?fontsize=14

andul007
  • 263
  • 1
  • 4
  • 5

4 Answers4

16

Created example with similar functional components and dummy data:

https://codesandbox.io/s/recursing-cdn-q4vx0

Have to create Child component with local item state and sync with main array. Added Delete and Add.

Work with object arrays as your sample.

Oleg
  • 3,580
  • 1
  • 7
  • 12
  • I have a slight question, I have to make the state an array, not a map because inputs can be added or deleted. How would you modify this so that it could accomplish this? – andul007 Oct 30 '19 at 23:56
  • Did you try to delete prop in existing map like: : delete state[keyfield], you also can work with array but you need some field for key unique value for each row. – Oleg Oct 31 '19 at 03:17
  • I udpdate the answer with your code and add delete and update – Oleg Oct 31 '19 at 06:55
  • Thank you!!!! This is amazing! Just a quick question. I'm a little confused as to how you were able to just do: const onInputChange = (index, item) => { state[index] = item; }; Isn't state supposed to be immutable? Aren't you supposed to use setState()? – andul007 Nov 07 '19 at 05:34
  • Yes you are right but in this case I just want to sync new instance from child item to parent array without rerender whole array, only for sync. – Oleg Nov 07 '19 at 05:47
  • the thing is that here you are not actually updating the state using set state, so you don't really solve the performance problem, the app starts to lag only when you are setting new state – Alexander Cherednichenko Aug 22 '20 at 08:09
  • How about using a useRef() for state in the parent when we are only using it for syncing with a new instance? – bhanu prakash May 24 '21 at 19:17
4

Copying large arrays shouldn't be the culprit, it only copies object references which is usually very fast, not an actual deep copy. In fact, Redux itself does it this way.

The reason you're experiencing this lag is you didn't set keys to the children. You should set unique keys and don't use the index as the key.

Mordechai
  • 15,437
  • 2
  • 41
  • 82
1

I'd minimize the re-renders, so I'd do following:

  1. Keep all functions in parent and share memoized callback to avoid redundant function constructions
  2. Do not use indexes to figure out specific children
  3. Memoize the children

You can check out the code with implemented removal by the link https://codesandbox.io/s/loving-rgb-cejoy?fontsize=14&hidenavigation=1&theme=dark&file=/src/Parent.jsx

That way, you reduce the re-renders almost to zero and you're good to go until you face the penalties of copying and rendering really large arrays which I guess you are not likely to run into

dkuznietsov
  • 298
  • 1
  • 9
1

You have 2 options:

  1. Add key to the Child, e.g., email, firstName, lastName, allowing React to only re-render changed input(s). Prefer not to use index for the key, otherwise inserting/deleting item at the being of the state array will cause the entire list to re-render

    {state.map((item, index) => (
      <Child
        // e.g., email, firstName, lastName, prefer not to use `index`
        key={item.name}
        value={item.content}
        index={index}
        onChange={onInputChange}
      />
    ))}
    
  2. As you mention the state array is huge, you should not use .map() anymore, instead you should utilise libraries such as react-virtualized, which will only render visible elements to the DOM and greatly improve your performance.

engineforce
  • 2,840
  • 1
  • 23
  • 17