useCallback 的 React useEffect 依赖总是触发渲染
React useEffect dependency of useCallback always triggers render
我有一个谜。考虑以下按时间段获取数据并将结果存储在 Map
:
中的自定义 React 挂钩
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 我不知道为什么它应该重复触发渲染。任何人都可以解释一下吗?
这是我现在根据@jure 上面的评论得到的:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to 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;
}
问题出在useCallback/useEffect
的组合使用上。必须小心 useCallback
和 useEffect
中的依赖数组,因为 useCallback
依赖数组的变化会触发 useEffect
到 运行。
“data”
变量在 useCallback
依赖数组中使用,当调用 setData 时,react 将重新 运行 函数组件,并为 data
变量和触发一连串的电话。
调用堆栈看起来像这样:
- 使用效果运行
- 已调用更新数据
- 调用了setState
- 组件使用新的状态数据重新渲染
data
的新值触发 useCallback
updateData
已更改
- 再次触发
useEffect
要解决此问题,您需要从 useCallback
依赖项数组中删除 “data”
变量。我发现尽可能不在依赖项数组中包含组件状态是一个好习惯。
如果您需要从 useEffect
或 useCallback
更改组件状态,并且新状态是先前状态的函数,您可以传递接收当前状态的函数作为参数, returns 一个新状态。
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]
);
在您的示例中,您只需要当前状态来计算下一个状态,这样就可以了。
我有一个谜。考虑以下按时间段获取数据并将结果存储在 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 我不知道为什么它应该重复触发渲染。任何人都可以解释一下吗?
这是我现在根据@jure 上面的评论得到的:
I think the problem is that the "data" variable is included in the dependency array of useCallback. Every time you setData, the data variable is changed that triggers useCallback to provide new updateData and that triggers useEffect. Try to implement updateData without a dependecy on the data variable. you can do something like setData(d=>new Map(d.set(period, asyncState)) to avoid passing "data" variable to 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;
}
问题出在useCallback/useEffect
的组合使用上。必须小心 useCallback
和 useEffect
中的依赖数组,因为 useCallback
依赖数组的变化会触发 useEffect
到 运行。
“data”
变量在 useCallback
依赖数组中使用,当调用 setData 时,react 将重新 运行 函数组件,并为 data
变量和触发一连串的电话。
调用堆栈看起来像这样:
- 使用效果运行
- 已调用更新数据
- 调用了setState
- 组件使用新的状态数据重新渲染
data
的新值触发 useCallbackupdateData
已更改- 再次触发
useEffect
要解决此问题,您需要从 useCallback
依赖项数组中删除 “data”
变量。我发现尽可能不在依赖项数组中包含组件状态是一个好习惯。
如果您需要从 useEffect
或 useCallback
更改组件状态,并且新状态是先前状态的函数,您可以传递接收当前状态的函数作为参数, returns 一个新状态。
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]
);
在您的示例中,您只需要当前状态来计算下一个状态,这样就可以了。