Search code examples
reactjsreact-hooksreact-memo

React child components out of sync when using memoization


I have a component that consists of several "bigger" subcomponents, one of them being a custom form CustomForm, which in turn consists of several custom input fields CustomField.

To improve the performance of the page I want to memoize that form so that it only rerenders, whenever at least one of the data values changes (data being a state of the main component).
At the same time, I also want to memoize the individual input fields, so they only rerender whenever their respective value changes.

Following is a minimal working example - I ommited the other parts of the app and simplified the form and input fields:

import React, { useState} from 'react';


function CustomFieldComponent(props) {
    const { labelText, data, handleChange } = props;

    console.log("renders", labelText);

    return (
        <input id={labelText} key={labelText} value={data[labelText]} onChange={handleChange}/>
      )
}

function compareTextfield(prevProps, nextProps) {
    return prevProps.data[prevProps.labelText] === nextProps.data[nextProps.labelText];
}

const CustomField = React.memo(CustomFieldComponent, compareTextfield);


function CustomFormComponent(props) {
    const { data, handleChange } = props;

    return (
        <div>
            <CustomField labelText="field1" data={data} handleChange={handleChange}/>
            <CustomField labelText="field2" data={data} handleChange={handleChange}/>
            <CustomField labelText="field3" data={data} handleChange={handleChange}/>
        </div>
    )
}

function compareForm(prevProps, nextProps) {
    return prevProps.data === nextProps.data;
}

const CustomForm = React.memo(CustomFormComponent, compareForm);


export default function Test(props) {
    const [data, setData] = useState({"field1": "value1", "field2": "value2", "field3": "value3"});


    const handleChange = (e) => {
        var local_data = {...data};
        local_data[e.target.id] = e.target.value;
        setData(local_data);
    }


    // other parts of the component

    return (
        <div>
            <CustomForm data={data} handleChange={handleChange}/>
            <br/>
            This is data:
            {
                Object.keys(data).map((key, index) => ( 
                    <div key={index}>{data[key]}</div> 
                ))
            }
        </div>
    )
}

This code works as intended regarding unnecessary rerenders of the different components, however the individual input fields are out of sync. E.g., when I enter some text in field1 and then in field2, the previous input of field1 gets reset (i.e., the previously changed value in data gets overwritten in subsequent inputs).

It seems that the problem lies in memoization of the CustomFieldComponent (usage of CustomField), because if I would use the non-memoized CustomFieldComponent itself within CustomFormComponent, everything works as intended and the user inputs are kept across the different fields correctly.

My question is, how can I achieve the desired memoization while keeping the individual input fields synced with data?


Solution

  • Found a fix. The problem was the handleChange function in the main component.
    It needs to be:

    const handleChange = (e) => {
        setData(prev => ({...prev, [e.target.id]: e.target.value}));
    }