我有一个谜。考虑下面的自定义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
。
鉴于updateData
是useCallback
版,我不知所措,不明白为什么它应该反复触发渲染。有人可以照亮吗?
答案 0 :(得分:1)
问题出在结合使用的useCallback/useEffect
上。必须注意useCallback
和useEffect
中的依赖项数组,因为useCallback
依赖项数组中的更改将触发useEffect
运行。
“data”
变量在useCallback
依赖关系数组中使用,当调用setData时,react将使用data
变量的新值重新运行函数组件,并触发一系列调用。
调用堆栈看起来像这样:
data
的updateData
已更改useEffect
要解决该问题,您需要从“data”
依赖项数组中删除useCallback
变量。我发现在可能的情况下不将组件状态包括在依赖项数组中是一种好习惯。
如果您需要从useEffect
或useCallback
更改组件状态,并且新状态是先前状态的函数,则可以传递将当前状态作为参数并返回一个状态的函数。新状态。
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;
}