You need to enable JavaScript to run this app. 使用React Hook结合TypeScript封装共享SnackBar方法时出现无效Hook调用报错
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.23 02:36:04