使用React Hook结合TypeScript封装共享SnackBar方法时出现无效Hook调用报错
报错根因
- 原封装违反了React Hooks的核心调用规则:
useSnackbar是React Hook,只能在函数组件主体顶层或者以use开头的自定义Hook内部调用,不能在普通工具函数、事件回调、异步流程、useEffect回调里直接执行。 - 原代码把
useSnackbar写在普通的SnackBar函数内部,又把这个函数直接放到useEffect回调里调用——此时根本不在React的组件渲染上下文中,自然会触发非法Hook调用的报错。 - 额外存在导入错配问题:原SnackBar文件是
export default默认导出,但业务组件用了命名导入,本身就会导致引用为undefined。
正确实现方案(两种场景按需选择)
场景1:组件内复用(常规业务场景,优先推荐)
把原来的封装改成合规的自定义Hook,保证Hook本身在组件顶层被调用,返回纯函数供业务逻辑调用,完全符合React规则。
第一步:重写snackbar.ts文件
// snackbar.ts import { useSnackbar } from 'notistack'; // 自定义Hook必须以use开头,符合React Hook命名规范,会被ESLint正确识别 export const useSnackBar = () => { const { enqueueSnackbar } = useSnackbar(); // 返回闭包持有enqueueSnackbar引用的普通函数,内部无任何Hook调用,可任意放到回调、异步流程中执行 const showSnack = (message: string, isError?: boolean) => { return enqueueSnackbar(message, { anchorOrigin: { horizontal: 'right', vertical: 'top' }, variant: isError ? 'error' : 'success', style: { whiteSpace: 'pre-line' } }); }; return showSnack; };
第二步:业务组件内合规调用
import { FC, useEffect } from 'react'; import { useSnackBar } from '../../snackbar'; const userEditForm: FC = () => { // Hook必须在组件主体顶层调用,不能放到回调逻辑里 const showSnack = useSnackBar(); useEffect(() => { if (!loading && submitForm) { if (!error) { // 直接调用普通函数,无Hook执行逻辑 showSnack('user updated'); } else { showSnack(error, true); } } }, [error, loading, submitForm, showSnack]); // 将返回的方法加入依赖数组,符合Hooks lint规则 return <MyComponent />; };
场景2:跨非组件上下文复用(比如axios拦截器、全局工具函数中调用)
如果需要脱离React组件上下文(比如请求拦截器、全局状态回调里)调用SnackBar,可以通过模块级全局变量持有enqueueSnackbar实例,完全绕开Hook调用限制。
第一步:实现全局SnackBar控制器
// snackbarController.ts import { VariantType, SnackbarKey, SnackbarMessage } from 'notistack'; // 模块级全局变量,持有enqueueSnackbar方法引用 let globalEnqueue: ( message: SnackbarMessage, options?: { variant?: VariantType; anchorOrigin?: { horizontal: 'left' | 'center' | 'right'; vertical: 'top' | 'bottom' }; style?: React.CSSProperties; } ) => SnackbarKey; // 初始化方法,在应用根节点挂载SnackbarProvider时注入实例 export const initGlobalSnackbar = (enqueue: typeof globalEnqueue) => { globalEnqueue = enqueue; }; // 全局可调用的SnackBar方法,无任何Hook逻辑,任意位置可调用 export const showGlobalSnack = (message: string, isError?: boolean) => { if (!globalEnqueue) { throw new Error('SnackBar全局实例未初始化,请先在应用根节点完成实例注入'); } return globalEnqueue(message, { anchorOrigin: { horizontal: 'right', vertical: 'top' }, variant: isError ? 'error' : 'success', style: { whiteSpace: 'pre-line' } }); };
第二步:在应用根节点做实例初始化
// App.tsx 根组件 import { SnackbarProvider } from 'notistack'; import { initGlobalSnackbar } from './snackbarController'; const App = () => { return ( <SnackbarProvider ref={(ref) => { // 挂载后将enqueueSnackbar方法注入全局控制器 if (ref) initGlobalSnackbar(ref.enqueueSnackbar); }} > {/* 所有子组件、路由逻辑 */} <RootRoutes /> </SnackbarProvider> ); };
之后不管是在业务组件、axios拦截器、普通ts工具文件里,只需要import { showGlobalSnack } from './snackbarController'就可以直接调用,完全不会触发Hook调用报错。
内容的提问来源于stack exchange,提问作者AMendis
相关产品推荐
相关产品推荐

