响应useCallback的useEffect依赖关系始终触发渲染

时间:2020-01-05 05:50:24

标签: reactjs use-effect usecallback

我有一个谜。考虑下面的自定义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;
}

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

鉴于updateDatauseCallback版,我不知所措,不明白为什么它应该反复触发渲染。有人可以照亮吗?

2 个答案:

答案 0 :(得分:1)

问题出在结合使用的useCallback/useEffect上。必须注意useCallbackuseEffect中的依赖项数组,因为useCallback依赖项数组中的更改将触发useEffect运行。

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

调用堆栈看起来像这样:

  1. useEffect运行
  2. updateData称为
  3. setState称为
  4. 组件使用新的状态数据重新呈现
  5. data
  6. 新值会触发useCallback
  7. updateData已更改
  8. 再次
  9. 触发useEffect

要解决该问题,您需要从“data”依赖项数组中删除useCallback变量。我发现在可能的情况下不将组件状态包括在依赖项数组中是一种好习惯。

如果您需要从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]
);

在您的示例中,您只需要当前状态即可计算下一个状态,从而可以正常工作。

答案 1 :(得分:0)

基于以上@jure的评论,这就是我现在拥有的:

我认为问题是useCallback的依赖项数组中包含“数据”变量。每次您设置setData时,都会更改数据变量,该变量将触发useCallback提供新的updateData并触发useEffect。尝试实现updateData而不依赖于数据变量。您可以执行类似setData(d => new Map(d.set(period,asyncState)))的操作,以避免将“ data”变量传递给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;
}