【问题标题】:React useEffect dependency of useCallback always triggers render反应useCallback的useEffect依赖总是触发渲染
【发布时间】:2020-04-23 03:03:20
【问题描述】:

我有一个谜。考虑以下自定义 React 钩子,它按时间段获取数据并将结果存储在 Map 中:

export function useDataByPeriod(dateRanges: PeriodFilter[]) {
    const isMounted = useMountedState();

    const [data, setData] = useState(
        new Map(
            dateRanges.map(dateRange => [
                dateRange,
                makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
            ])
        )
    );

    const updateData = useCallback(
        (period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
            const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
            if (isSafeToSetData) {
                setData(new Map(data.set(period, asyncState)));
            }
        },
        [setData, data, isMounted]
    );

    useEffect(() => {
        if (dateRanges.length === 0) {
            return;
        }

        const loadData = () => {
            const client = makeClient();
            dateRanges.map(dateRange => {
                updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));

                return client
                    .getData(dateRange.dateFrom, dateRange.dateTo)
                    .then(periodData => {
                        updateData(dateRange, makeAsyncData(periodData));
                    })
                    .catch(error => {
                        const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
                        console.error(errorString, error);
                        updateData(dateRange, makeAsyncError(errorString));
                    });
            });
        };

        loadData();
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [dateRanges /*, updateData - for some reason when included this triggers infinite renders */]);

    return data;
}

updateData 作为依赖项添加时,useEffect 被重复触发。如果我将它作为依赖项排除,那么一切都会按预期工作/行为,但eslint 抱怨我违反了react-hooks/exhaustive-deps

鉴于updateData 一直是useCallback-ed,我不明白为什么它应该反复触发渲染。有没有人能解释一下?

【问题讨论】:

  • 你是否尝试从 useCallback 中的依赖数组中移除 setData?
  • 鉴于 setData 是一个依赖项,那不就是在不同的地方创建 react-hooks/exhaustive-deps 问题吗?
  • 我认为问题在于“data”变量包含在useCallback的依赖数组中。每次设置数据时,都会更改触发 useCallback 以提供新的 updateData 并触发 useEffect 的数据变量。尝试在不依赖数据变量的情况下实现 updateData。您可以执行类似 setData(d=>new Map(d.set(period, asyncState)) 之类的操作,以避免将“数据”变量传递给 useCallback
  • 您还确定 useMountedState 返回相同的 ref 吗?除此之外,我认为@jure 是对的
  • @JohnReilly 很高兴它成功了。我试图撰写一个答案。用文字来解释这种行为并不容易 :) 猜想这是 react 钩子的一个有点令人困惑的部分。

标签: reactjs use-effect usecallback


【解决方案1】:

问题在于useCallback/useEffect组合使用。必须小心 useCallbackuseEffect 中的依赖数组,因为 useCallback 依赖数组中的更改将触发 useEffect 运行。

“data” 变量在 useCallback 依赖数组中使用,当调用 setData 时,react 将使用 data 变量的新值重新运行函数组件,并触发一系列调用。

调用堆栈看起来像这样:

  1. useEffect 运行
  2. 已调用更新数据
  3. setState 调用
  4. 组件使用新的状态数据重新渲染
  5. data 的新值触发 useCallback
  6. updateData 已更改
  7. 再次触发useEffect

要解决此问题,您需要从 useCallback 依赖数组中删除 “data” 变量。我发现尽可能不在依赖数组中包含组件状态是一种很好的做法。

如果您需要从 useEffectuseCallback 更改组件状态,并且新状态是先前状态的函数,您可以传递接收当前状态作为参数并返回新状态的函数。

const updateData = useCallback(
    (period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
        const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
        if (isSafeToSetData) {
            setData(existingData => new Map(existingData.set(period, asyncState)));
        }
    },
    [setData, isMounted]
);

在您的示例中,您只需要当前状态来计算下一个状态,这样应该可以工作。

【讨论】:

  • 这应该得到更多的支持!我以前没有想过使用依赖注入来解决它(将旧数据作为参数传递)。谢谢!!!
【解决方案2】:

这是我现在根据上面@jure 的评论得到的:

我认为问题在于“data”变量包含在useCallback的依赖数组中。每次设置数据时,都会更改触发 useCallback 以提供新的 updateData 并触发 useEffect 的数据变量。尝试在不依赖数据变量的情况下实现 updateData。您可以执行 setData(d=>new Map(d.set(period, asyncState)) 之类的操作,以避免将“数据”变量传递给 useCallback

我按照建议的方式调整了我的代码,并且成功了。谢谢!

export function useDataByPeriod(dateRanges: PeriodFilter[]) {
    const isMounted = useMountedState();

    const [data, setData] = useState(
        new Map(
            dateRanges.map(dateRange => [
                dateRange,
                makeAsyncIsLoading({ isLoading: false }) as AsyncState<MyData[]>
            ])
        )
    );

    const updateData = useCallback(
        (period: PeriodFilter, asyncState: AsyncState<MyData[]>) => {
            const isSafeToSetData = isMounted === undefined || (isMounted !== undefined && isMounted());
            if (isSafeToSetData) {
                setData(existingData => new Map(existingData.set(period, asyncState)));
            }
        },
        [setData, isMounted]
    );

    useEffect(() => {
        if (dateRanges.length === 0) {
            return;
        }

        const loadData = () => {
            const client = makeClient();
            dateRanges.map(dateRange => {
                updateData(dateRange, makeAsyncIsLoading({ isLoading: true }));

                return client
                    .getData(dateRange.dateFrom, dateRange.dateTo)
                    .then(traffic => {
                        updateData(dateRange, makeAsyncData(traffic));
                    })
                    .catch(error => {
                        const errorString = `Problem fetching ${dateRange.displayPeriod} (${dateRange.dateFrom} - ${dateRange.dateTo})`;
                        console.error(errorString, error);
                        updateData(dateRange, makeAsyncError(errorString));
                    });
            });
        };

        loadData();
    }, [dateRanges , updateData]);

    return data;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-12
    • 2021-09-29
    • 2022-08-19
    • 2021-06-17
    • 2019-12-14
    • 1970-01-01
    • 2020-12-11
    • 2021-07-17
    相关资源
    最近更新 更多