升级到 React 18 后 React App 无法渲染
我的 React App 运行正常,但在升级到 React 18、MUI v5 和 Redux v5 后,它无法渲染任何内容。
在终端中,我收到:“webpack 编译成功”
但在 Chrome 控制台中,我收到:
Warning: 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.
printWarning @ react.development.js:209
error @ react.development.js:183
resolveDispatcher @ react.development.js:1592
useContext @ react.development.js:1602
useReduxContext2 @ useReduxContext.ts:14
useSelector2 @ useSelector.ts:175
NoLogin @ auth.js:23
./src/routes/index.js @ index.js:9
options.factory @ react refresh:6
__webpack_require__ @ bootstrap:22
fn @ hot module replacement:61
./src/routes/Routes.js @ index.js:13
options.factory @ react refresh:6
__webpack_require__ @ bootstrap:22
fn @ hot module replacement:61
./src/App.js @ bundle.js:25
options.factory @ react refresh:6
__webpack_require__ @ bootstrap:22
fn @ hot module replacement:61
./src/index.js @ Sidebar.js:420
options.factory @ react refresh:6
__webpack_require__ @ bootstrap:22
(anonymous) @ startup:7
(anonymous) @ startup:7
Show 6 more frames
Show less
react.development.js:1618
Uncaught TypeError: Cannot read properties of null (reading 'useContext')
at Object.useContext (react.development.js:1618:1)
at useReduxContext2 (useReduxContext.ts:14:1)
at useSelector2 (useSelector.ts:175:1)
at NoLogin (auth.js:23:1)
at ./src/routes/index.js (index.js:9:1)
at options.factory (react refresh:6:1)
at __webpack_require__ (bootstrap:22:1)
at fn (hot module replacement:61:1)
at ./src/routes/Routes.js (index.js:13:1)
at options.factory (react refresh:6:1)`
这是我的
package.json
:
{
"name": ********************,
"version": "0.0.1",
"private": true,
"homepage": ********************
"scripts": {
"start": ********************
"start:local": ********************
"start:sg": ********************
"build:dev": ********************
"build": *********************
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 10",
"not op_mini all"
],
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"dependencies": {
"@date-io/date-fns": "^3.0.0",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@iconify/react": "^4.1.1",
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.15",
"@mui/styled-engine": "^5.15.14",
"@mui/x-date-pickers": "^7.2.0",
"@reduxjs/toolkit": "^2.2.3",
"axios": "^1.6.8",
"chart.js": "^4.4.2",
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"env-cmd": "^10.1.0",
"i18n-iso-countries": "^7.10.0",
"immer": "^10.0.3",
"material-ui-confirm": "^3.0.12",
"moment": "^2.30.1",
"mui-datatables": "^4.3.0",
"notistack": "^3.0.1",
"numeral": "^2.0.6",
"polished": "^4.1.3",
"prop-types": "^15.7.2",
"react": "^17.0.0 || ^18.0.0",
"react-app-polyfill": "^3.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-feather": "^2.0.10",
"react-perfect-scrollbar": "^1.5.8",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
"react-scripts": "^5.0.1",
"react-select": "^5.2.0",
"redux": "^5.0.1",
"redux-auth-wrapper": "^3.0.0",
"redux-saga": "^1.1.3",
"reselect": "^5.1.0",
"styled-components": "^6.1.8",
"url": "^0.11.3"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
}
}
这是我的
App.js
:
import React from "react";
import { connect } from "react-redux";
import { BrowserRouter as Router } from "react-router-dom";
import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
import AdapterDateFns from '@mui/lab/AdapterDateFns';
import LocalizationProvider from '@mui/lab/LocalizationProvider';
import { SnackbarProvider } from "notistack";
import maTheme from "./theme";
import Routes from "./routes/Routes";
import { ConfirmProvider } from 'material-ui-confirm';
const App = ({ theme }) => {
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={maTheme[theme.currentTheme]}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<ConfirmProvider>
<SnackbarProvider maxSnack={1}>
<Routes />
</SnackbarProvider>
</ConfirmProvider>
</LocalizationProvider>
</ThemeProvider>
</StyledEngineProvider>
);
};
const ConnectedApp = connect(store => ({ theme: store.themeReducer }))(App);
function AppWithRouter() {
return (
<Router>
<ConnectedApp/>
</Router>
);
}
export default AppWithRouter;`
这是我的
index.js
:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store/index';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App tab="home"/>
</Provider>
</React.StrictMode>
);
这是我的
Routes/index.js
:
import React from "react";
import async from "../components/Async";
import { Truck } from "react-feather";
import {NeedsLogin, NoLogin} from "./auth";
// Auth components
const SignIn = NoLogin(() => import("../pages/auth/SignIn"));
const SignUp = async(() => import("../pages/auth/SignUp"));
const ForgotPassword = async(() => import("../pages/auth/ForgotPassword"));
const ResetPassword = async(() => import("../pages/auth/ResetPassword"));
const Page404 = async(() => import("../pages/auth/Page404"));
const Page500 = async(() => import("../pages/auth/Page500"));
// Pages components
const OnBoard = NeedsLogin(() => import("../pages/pages/OnBoard"));
const Institutions = NeedsLogin(() => import("../pages/pages/Institutions/Institutions"));
const LoginInstitutions = NeedsLogin(() => import("../pages/pages/Institutions/LoginInstitution"));
const Instances = NeedsLogin(() => import("../pages/pages/Instances/Instances"));
const EditInstance = NeedsLogin(() => import("../pages/pages/Instances/editInstance"));
//Account Routes
const MyAccount = NeedsLogin(() => import("../pages/pages/Account/MyAccount"));
const homeRoutes = {
id: "Home",
path: "/",
component: OnBoard,
children: null,
hidden: true
};
const accountRoutes = {
id: "My Account",
path: "/my_account",
component: MyAccount,
children: null,
hidden: true
};
const institutionsRoutes = {
id: "Institutions",
path: "/institutions",
component: Institutions,
children: null,
hidden: true
};
const loginInstitutionsRoutes = {
id: "Distribution Institutions",
path: "/login_institutions",
component: LoginInstitutions,
icon: <Truck />,
children: null,
};
const instancesRoutes = {
id: "Instances",
path: "/instances",
component: Instances,
children: null,
hidden: true
};
const editInstanceRoutes = {
id: "Edit Instance",
path: "/editInstance",
component: EditInstance,
children: null,
hidden: true
};
const authRoutes = {
id: "Auth",
path: "/auth",
hidden: true,
children: [
{
path: "/auth/sign-in",
name: "Sign In",
component: SignIn
},
{
path: "/auth/sign-up",
name: "Sign Up",
component: SignUp
},
{
path: "/auth/forgot-password",
name: "Forgot Password",
component: ForgotPassword
},
{
path: "/reset_your_password/:id",
name: "Reset Password",
component: ResetPassword
},
{
path: "/auth/404",
name: "404 Page",
component: Page404
},
{
path: "/auth/500",
name: "500 Page",
component: Page500
}
]
};
export const dashboard = [
homeRoutes,
accountRoutes,
institutionsRoutes,
instancesRoutes,
editInstanceRoutes,
loginInstitutionsRoutes
];
export const auth = [authRoutes];
export default [
homeRoutes,
accountRoutes,
authRoutes,
institutionsRoutes,
instancesRoutes,
editInstanceRoutes,
loginInstitutionsRoutes
];
这是我的
auth.js
:
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import Loader from '../components/Loader';
const NeedsLogin = ({ children }) => {
const signedIn = useSelector(state => state.auth.signedIn);
const loading = useSelector(state => state.auth.loading);
const history = useNavigate();
if (loading) {
return <Loader />;
}
if (!signedIn) {
history('/auth/sign-in');
return null;
}
return children;
};
const NoLogin = ({ children }) => {
const signedIn = useSelector(state => state.auth.signedIn);
const loading = useSelector(state => state.auth.loading);
const history = useNavigate();
if (loading) {
return <Loader />;
}
if (signedIn) {
history('/');
return null;
}
return children;
};
export { NeedsLogin, NoLogin };
我尝试删除
React.StrictMode
并得到相同的结果。
我使用以下命令检查了项目中的 react 和 react-dom 版本:
npm ls react-dom
├─┬ @mui/[email protected]
│ ├─┬ @mui/[email protected]
│ │ ├─┬ @floating-ui/[email protected]
│ │ │ └── [email protected] deduped
│ │ └── [email protected] deduped
│ └── [email protected] deduped
├─┬ @mui/[email protected]
│ ├── [email protected] deduped
│ └─┬ [email protected]
│ └── [email protected] deduped
├─┬ @mui/[email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├── [email protected] deduped
│ ├─┬ [email protected]
│ │ ├─┬ [email protected]
│ │ │ └── [email protected] deduped
│ │ ├── [email protected]
│ │ └─┬ [email protected]
│ │ └── [email protected] deduped
│ └─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├── [email protected]
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
└─┬ [email protected]
└── [email protected] deduped
npm ls react
├─┬ @emotion/[email protected]
│ ├─┬ @emotion/[email protected]
│ │ └── [email protected] deduped
│ └── [email protected] deduped
├─┬ @emotion/[email protected]
│ └── [email protected] deduped
├─┬ @iconify/[email protected]
│ └── [email protected] deduped
├─┬ @mui/[email protected]
│ ├─┬ @mui/[email protected]
│ │ ├─┬ @floating-ui/[email protected]
│ │ │ └── [email protected] deduped
│ │ └── [email protected] deduped
│ ├─┬ @mui/[email protected]
│ │ ├─┬ @mui/[email protected]
│ │ │ └── [email protected] deduped
│ │ └── [email protected] deduped
│ ├─┬ @mui/[email protected]
│ │ └── [email protected] deduped
│ └── [email protected] deduped
├─┬ @mui/[email protected]
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ └── [email protected] deduped
├─┬ @mui/[email protected]
│ └── [email protected] deduped
├─┬ @mui/[email protected]
│ └── [email protected] deduped
├─┬ @reduxjs/[email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ ├─┬ @mui/[email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ ├─┬ [email protected]
│ │ │ └── [email protected] deduped
│ │ ├─┬ [email protected]
│ │ │ └── [email protected] deduped
│ │ ├─┬ [email protected]
│ │ │ └── [email protected] deduped
│ │ └── [email protected]
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├── [email protected] deduped
│ └─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ ├── [email protected] deduped
│ └─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ ├── [email protected] deduped
│ └─┬ [email protected]
│ └── [email protected] deduped
├── [email protected]
└─┬ [email protected]
└── [email protected] deduped
我尝试卸载“mui-datatables”,以确保 React 和 React-DOM 均为 18.2.0 版本,但问题仍然存在
尝试在您的 package.json 文件中使用单个版本的 react 和 react-dom(如下面提到的代码片段所示),卸载您的 node 模块文件夹并执行全新的 npm 安装。
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
这里的问题可能是
history
的调用方式,在
react-router 文档
中,他们从效果钩子内部调用该函数。请参阅下面的潜在解决方案:
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import Loader from '../components/Loader';
const NeedsLogin = ({ children }) => {
const signedIn = useSelector(state => state.auth.signedIn);
const loading = useSelector(state => state.auth.loading);
const history = useNavigate();
useEffect(() = {
if (!loading && !signedIn) {
history('/auth/sign-in');
}
}, [loading, signedIn]);
if (loading) {
return <Loader />;
}
if (!signedIn) {
return null;
}
return children;
};
const NoLogin = ({ children }) => {
const signedIn = useSelector(state => state.auth.signedIn);
const loading = useSelector(state => state.auth.loading);
const history = useNavigate();
useEffect(() = {
if (!loading && signedIn) {
history('/');
}
}, [loading, signedIn]);
if (loading) {
return <Loader />;
}
if (signedIn) {
return null;
}
return children;
};
export { NeedsLogin, NoLogin };
另一个问题可能与依赖项有关:
react-sortable-tree-patch-react-17
,它正在引入
react
和
react-dom
的 v17。
...
├─┬ [email protected]
│ ├─┬ @mui/[email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ ├─┬ [email protected]
│ │ │ └── [email protected] deduped
│ │ ├─┬ [email protected]
│ │ │ └── [email protected] deduped
...
查看依赖包
mui-datatables
可能
仍然是一个问题
。
如果您使用的是相对较新的
npm
版本,则可以添加
overrides
强制将
react
和
react-dom
的版本设置为
18.2.0
。或者,如果
yarn
是一个选项,您可以添加类似的配置
resolutions
。
在您的
package.json
中添加以下内容,然后在运行
npm i
之前删除
./node_modules
和
package-lock.json
,干净地安装您的依赖项。
{
...
"overrides": {
"react": "18.2.0",
"react-dom": "18.2.0"
}
}
但是,这可能会对仍然期望 react v17 的库产生副作用。
今天也遇到了同样的问题,追溯到 react-router-dom,当我不使用 router 时,项目运行正常,没有报错。你可以尝试使用官方的示例,用
useRouters
初始化
routerObj
,会返回一个可以直接放在 dom 中的 react 组件。
例如:
import React from 'react';
import { useRoutes } from 'react-router-dom';
import { router } from './router';
const App: React.FC = () => {
const routerElement = useRoutes(router)
return routerElement;
};
export default App;