React:在useEffect中使用自定义Notistack封装hook触发无限循环如何解决?
问题触发原因
你封装的useSnackbar自定义Hook,虽然内部的showSnackbarVariant已经用useCallback做了引用缓存,但最终返回的对象中,showSuccessSnackbar/showErrorSnackbar等方法都是每次Hook调用时新创建的箭头函数,引用地址每次渲染都会变化。
当你把这类引用不稳定的方法加入useEffect依赖数组时,每次组件重渲染都会判定依赖发生变化,触发useEffect回调执行;而回调里的activateConnectedAccount调用又会触发组件重渲染,生成新的showSuccessSnackbar引用,最终形成无限循环。
可行修复方案
方案1:给useSnackbar返回的所有方法加缓存(推荐)
直接从Hook层面保证返回方法的引用稳定性,是最彻底的修复方式,两种实现可选:
第一种是给每个方法单独加useCallback:
import { useCallback } from 'react'; import { useSnackbar as useNotistackSnackbar } from 'notistack'; import { SNACKBAR_TYPES } from '../constants/properties'; const useSnackbar = () => { const { enqueueSnackbar } = useNotistackSnackbar(); const showSnackbarVariant = useCallback( ({ text, action_text, onActionClick, variant }) => enqueueSnackbar( { variant, text, action_text, onActionClick, }, { autoHideDuration: action_text && onActionClick ? 9000 : 4000 } ), [enqueueSnackbar] ); // 给每个返回方法加useCallback缓存,依赖只有稳定的showSnackbarVariant const showSuccessSnackbar = useCallback(({ text, action_text, onActionClick }) => showSnackbarVariant({ variant: SNACKBAR_TYPES.SUCCESS, text, action_text, onActionClick, }), [showSnackbarVariant]) const showErrorSnackbar = useCallback(({ text, action_text, onActionClick }) => showSnackbarVariant({ variant: SNACKBAR_TYPES.ERROR, text, action_text, onActionClick, }), [showSnackbarVariant]) const showWarningSnackbar = useCallback(({ text, action_text, onActionClick }) => showSnackbarVariant({ variant: SNACKBAR_TYPES.WARNING, text, action_text, onActionClick, }), [showSnackbarVariant]) const showDownloadSnackbar = useCallback(({ text, action_text, onActionClick }) => showSnackbarVariant({ variant: SNACKBAR_TYPES.DOWNLOAD, text, action_text, onActionClick, }), [showSnackbarVariant]) const showPlainSnackbar = useCallback(({ text, action_text, onActionClick }) => showSnackbarVariant({ variant: SNACKBAR_TYPES.PLAIN, text, action_text, onActionClick, }), [showSnackbarVariant]) return { showSuccessSnackbar, showErrorSnackbar, showWarningSnackbar, showDownloadSnackbar, showPlainSnackbar }; }; export default useSnackbar;
第二种是用useMemo包裹整个返回对象,效果完全一致:
return useMemo(() => ({ showSuccessSnackbar: ({ text, action_text, onActionClick }) => showSnackbarVariant({ variant: SNACKBAR_TYPES.SUCCESS, text, action_text, onActionClick }), showErrorSnackbar: ({ text, action_text, onActionClick }) => showSnackbarVariant({ variant: SNACKBAR_TYPES.ERROR, text, action_text, onActionClick }), // 剩余方法省略 }), [showSnackbarVariant])
方案2:组件层用useRef持有方法引用(不用改Hook的临时方案)
如果不方便修改useSnackbar的源码,可以在调用侧用useRef存储方法,避免把不稳定的引用加入依赖:
export default function MyComponent() { const { showSuccessSnackbar, showErrorSnackbar } = useSnackbar(); // 用ref存储方法引用,每次渲染更新ref的值 const successSnackbarRef = useRef(showSuccessSnackbar); const errorSnackbarRef = useRef(showErrorSnackbar); useEffect(() => { successSnackbarRef.current = showSuccessSnackbar; errorSnackbarRef.current = showErrorSnackbar; }) const { mutate: activateConnectedAccount } = useCustomHook({ onSuccess: async () => { successSnackbarRef.current({ text: 'Direct deposit has been enabled.', }); }, onError: () => { errorSnackbarRef.current({ text: 'An error occurred. Please double check your bank information.', }); }, }); useEffect( () => { activateConnectedAccount() console.log("yooo"); }, [activateConnectedAccount] // 不需要再加snackbar相关方法 ); return <div>Foobar</div>; }
方案3:忽略依赖检查(不推荐)
如果确认showSuccessSnackbar的功能永远不会变化,可以手动加注释忽略依赖检查,但这种方式有潜在bug风险,只适合临时调试用:
useEffect( () => { activateConnectedAccount() console.log("yooo"); }, // eslint-disable-next-line react-hooks/exhaustive-deps [] );
内容的提问来源于stack exchange,提问作者bigpotato
相关产品推荐
相关产品推荐

