You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Jest测试React-Redux应用时出现Invalid Hooks错误

Jest测试中Redux useSelector触发「Invalid hook call」错误排查与解决

我开发了一个基于TypeScript、React和Redux的应用,浏览器构建运行完全正常,但在编写Jest测试用例时,只要组件中使用Redux的useSelector Hook,就会抛出「Invalid hook call」错误。我已确认:

  • React(18.2.0)与react-dom(18.2.0)版本匹配
  • redux(v4.2.1)和react-redux(v8.1.2)版本兼容
  • 严格遵循Hooks规则,仅在React组件顶层调用useSelector

错误信息

console.error
Error: Uncaught [Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app
    See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.]
    at reportException (...\node_modules\jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:66:24)
    at ...

错误定位代码

测试文件报错行:

19 | describe("Entry Point", () => {
20 |     it("should render successfully", () => {
21 |         const {baseElement} = render(<EntryPoint />);
    |                                     ^
22 |         expect(baseElement).toBeTruthy();
23 |     });
24 | });

组件报错行:

3 |
4 | const App = () => {
5 |   const name = useSelector((state: RootState) => state.ui.name);
   |                           ^
6 |   return <div>{name}</div>;
7 | };
8 |

相关代码

App.tsx

import { useSelector } from "react-redux";
import type { RootState } from "./store";

const App = () => {
  const name = useSelector((state: RootState) => state.logger.name);
  return <div>{name}</div>;
};

export default App;

注:useSelector在React组件顶层调用

App.test.tsx

import React from "react";
import App from "../src/App";
import { setupStore } from "../src/store";
import { Provider } from "react-redux";
import { render } from "@testing-library/react";

const EntryPoint = () => {
    const store = setupStore();

    return <Provider store={store}>
        <App />
    </Provider>;
};

describe("Entry Point", () => {
    it("should render successfully", () => {
        const {baseElement} = render(<EntryPoint />);
        expect(baseElement).toBeTruthy();
    });
});

store.ts

import { PreloadedState, combineReducers } from "redux";
import reducer from "./slice";
import { configureStore } from "@reduxjs/toolkit";

const rootReducer = combineReducers({
    ui: reducer
});
export type RootState = ReturnType<typeof rootReducer>;

export const setupStore = (preloadedState?: PreloadedState<RootState>) => {
    return configureStore({
        reducer: rootReducer,
        preloadedState
    });
};

export type AppStore = ReturnType<typeof setupStore>;

slice.ts

import {createSlice} from "@reduxjs/toolkit";
import type {PayloadAction} from "@reduxjs/toolkit";

interface State {
    name: string;
}

const initialState: State = {
    name: "test",
};

export const slice = createSlice({
    name: "logger",
    initialState,
    reducers: {
        setName: (state, action: PayloadAction<State["name"]>) => {
            state.name = action.payload;
        },
    }
});

export const {
    setName
} = slice.actions;

export default slice.reducer;

package.json(含Jest配置)

{
  "name": "...",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "start": "vite --port 3457",
    "test": "jest",
  },
  "dependencies": {
    "@reduxjs/toolkit": "1.9.5",
    "jest-dom": "^4.0.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-redux": "8.1.2",
    "redux": "4.2.1"
  },
  "devDependencies": {
    "@jest/globals": "^29.6.1",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "@testing-library/user-event": "14.5.1",
    "@types/react": "^18.2.15",
    "@types/react-dom": "^18.2.7",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@vitejs/plugin-react": "^4.0.3",
    "eslint": "^8.45.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.3",
    "jest": "^29.6.1",
    "jest-environment-jsdom": "^29.6.1",
    "ts-jest": "^29.1.1",
    "typescript": "^5.0.2",
    "vite": "^4.4.5"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "jsx",
      "tsx"
    ],
    "moduleNameMapper": {
      "\\.css$": "<rootDir>/tests/_styleMock.js"
    },
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "testEnvironment": "jsdom"
  }
}

解决方案

1. 适配ES模块的Jest配置

项目设置了"type": "module",但ts-jest默认使用CommonJS编译模式,会导致模块解析冲突。修改package.json中的jest配置:

"jest": {
  "moduleFileExtensions": ["js", "ts", "jsx", "tsx"],
  "moduleNameMapper": {
    "\\.css$": "<rootDir>/tests/_styleMock.js"
  },
  "transform": {
    "^.+\\.tsx?$": "ts-jest"
  },
  "testEnvironment": "jsdom",
  // 新增以下配置
  "extensionsToTreatAsEsm": [".ts", ".tsx"],
  "globals": {
    "ts-jest": {
      "useESM": true
    }
  }
}

2. 强制React单实例加载

添加模块映射,确保测试时只加载项目根目录下的React相关依赖,避免多实例问题:

"moduleNameMapper": {
  "\\.css$": "<rootDir>/tests/_styleMock.js",
  "^react$": "<rootDir>/node_modules/react",
  "^react-dom$": "<rootDir>/node_modules/react-dom",
  "^react-redux$": "<rootDir>/node_modules/react-redux"
}

3. 优化测试组件写法(推荐)

不要在组件内部创建store,改为在测试逻辑中初始化后传入,减少不必要的组件渲染开销:

import React from "react";
import App from "../src/App";
import { setupStore } from "../src/store";
import { Provider } from "react-redux";
import { render } from "@testing-library/react";

describe("Entry Point", () => {
    it("should render successfully", () => {
        const store = setupStore();
        const {baseElement} = render(
            <Provider store={store}>
                <App />
            </Provider>
        );
        expect(baseElement).toBeTruthy();
    });
});

4. 清理依赖缓存

执行以下命令清理缓存并重新安装依赖,确保依赖版本一致:

npm cache clean --force
rm -rf node_modules package-lock.json
npm install

内容的提问来源于stack exchange,提问作者Julianna

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 19:02:33