简体   繁体   中英

State is not updated properly when using React.memo and useCallback

My problem is quite common but it is not easy to figure out what is wrong with my example. I have nested components Parent->Child1->Child2. State and setState are passed in props to child components. I want to avoid re-renders in child components when parent is updated and I want to avoid to re-render the whole list when only one item is changed.

Here is my sample https://plnkr.co/edit/6nKaKrgNIF7LSetN As you can see when I change title in Parent or description in Child1, Child2 is not re-rendered, but when I change state in Child2, whole list is re-rendered plus there are some side-effects with state update.

Parent component. It contains title and. For Child1 onChange I receive prop which I want to update in state and value for it. I use useCallback hook to avoid additional re-render in child components, you will see React.memo there


const Parent = ({ initialData }) => {
    const [data, setData] = React.useState(initialData);
    console.log('render Parent');
    return <>
         <h2>Parent</h2>

        <input
            placeholder="title"
            value={data.title}
            onChange={
                React.useCallback((e) => {
                     setData(prev => ({ ...prev, title: e.target.value }))
                }, [])
            }
        />
        <br />
        <Child1
            data={data}
            onChange={
                React.useCallback((prop, value) => {
                    setData(
                        prev => {
                            prev[prop] = value;
                            const newState = { ...prev };
                            return newState;
                        }
                    );
                }
                , []
                )
            }

        />
    </>
}


Then in Child1 wrapped with React.memo. It renders also Child2 in a list.

        const Child1 = React.memo(
            ({ data, onChange }) => {
                console.log('render Child1');
                return <>
                    <h3>Child1</h3>

                    <input
                        placeholder="description"
                        value={data.description}
                        onChange={(e) => { onChange('description', e.target.value) }}

                    />
                    <br />
                    <br />
                    <br />
                    {data.list.map((element, index) => {
                        return <Child2
                            key={index} // don't do this in real
                            index={index}
                            data={element}
                            onChange={
                                React.useCallback(
                                    (prop, value) => {
                                        const newList = data.list.map((e, i) => {
                                            let newItem = { ...e };
                                            if (i == index) {
                                                newItem[prop] = value;
                                            }
                                            return newItem;
                                        });
                                        onChange('list', newList);
                                    }
                                    ,
                                    []
                                )
                            }
                        />
                    })}

                </>

            }
        )

Child2 is rendered in the list


        const Child2 = React.memo(({ index, data, onChange }) => {
            console.log('render Child2', index);
            return (

                <>
                    <h4>Child2</h4>

                    Country: <br />
                    <input
                        placeholder="country"
                        value={data.country}
                        onChange={(e) => onChange('country', e.target.value)}
                    />
                    <br />
                    <br />
                    Region: <br />
                    <input
                        placeholder="region"
                        value={data.region}
                        onChange={(e) => onChange('region', e.target.value)}
                    />
                    <br />
                    <br />
                    City: <br />
                    <input
                        placeholder="city"
                        value={data.city}
                        onChange={(e) => onChange('city', e.target.value)}
                    />

                    <hr />

                </>

            )
        }

        )



onChange in Child2 is calling setData from Parent, which updates Parent's state. Updating the state of Parent will trigger a re-render. Child1 re-renders because its data prop is changing (changing props will also trigger a re-render).

Try passing title in your dependency arrays in the useCallback functions in Parent and Child1 and see if that works. This should make the onChange function appear to be "changing" which should trigger re-renders down the component tree.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM