使用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:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- 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
相关产品推荐
相关产品推荐

