@givemoneybiatch
Немного веб, немного гейм

Почему state не обновляется корректно с использованием React.memo + useCallback?

Есть задача уменьшить количество ре-рендерингов дочерних компонентов.
Иерархия следующая Parent->Child1->Child2. При этом в Child1 генерируется список из стейта, где каждый элемент списка- это Child2.

Готовый пример здесь https://plnkr.co/edit/6nKaKrgNIF7LSetN?preview

Parent

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

                />
            </>
        }


Child1

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

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 />

                </>

            )
        }

        )

Для решения проблемы пытаюсь оборачивать дочерние элементы Child1 и Child2 в React.memo().
А функции которые передаю в onChange - оборачиваю в useCallback().
Проблема все равно остается и при изменении одного инпута в Child2, весь список рендерится заново.
А также, вылазит баг с состоянием, которые заключается в том, что при изменении инпута в Child2 все остальные значение списка переустанавливаются в дефолтное значение.
  • Вопрос задан
  • 200 просмотров
Решения вопроса 1
0xD34F
@0xD34F Куратор тега React
Всё обновляется корректно. Что написали, то и получили.

Почему всегда рендерятся все экземпляры Child2 - потому что вместо обновления одного элемента data.list вы всегда обновляете все.

Почему значения сбрасываются на дефолтные - потому что из-за useCallback data у вас всегда имеет начальное значение (ну, то, что было при первом рендеринге, то, которое initialData).

Переделываем:

Компонент Parent, обработчик onChange передаваемый в Child1, пусть вместо значения обновляемого свойства получает функцию, которая будет принимать предыдущее значение и возвращать новое:

React.useCallback((key, val) => {
  setData(data => ({
    ...data,
    [key]: val(data[key]),
  }));
}, [])

Ну и переписываем его вызовы внутри Child1:

onChange('list', list => list.map((n, i) => i === index
  ? { ...n, [prop]: value }
  : n
))

onChange('description', () => e.target.value)
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы