[react]react-router-dom 与 redux 版本升级
- 环境
- 脚手架的升级
- react-router-dom 升级
- 关于路由相关文件的写法--react-router-dom 5.0.1
- 入口渲染文件App.js
- 路由框架src/views/root/index.js
- 路由守卫 src/views/routerguide/index.jsx
- 路由文件src/views/page.js
- 关于路由相关文件的写法--react-router-dom 6.20.1方式1
- 入口渲染文件App.js
- 路由框架src/views/root/index.js
- 报错信息
- [RouterGuide] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>
- 路由守卫 src/views/routerguide/index.jsx
- 路由文件src/views/page.js
- 关于路由相关文件的写法--react-router-dom 6.20.1方式2
- 入口渲染文件App.js
- 路由框架src/views/root/index.js
- 路由文件 src/views/page.js
- 路由的跳转
- 代码跳转
- 标签跳转
- 状态管理
- React 状态管理--Redux4.0.4版本
- action.js定义改变状态类型
- connect.js定义组件需要修改的全局变量
- redux.js 定义改变状态类型
- store.js 定义所有的全局变量
- App.js 全局变量的注册:
- 组件使用全局变量
- React 状态管理--Redux5.0.0版本
- store.js 定义所有全局状态
- store.js 定义所有全局状态--持久化版本
- 组件使用全局变量:
- 报错信息
- store.js:60 A non-serializable value was detected in an action, in the path: `register`. Value: ƒ register2(key) { _pStore.dispatch({ type: REGISTER,key});}
- 生命周期
- 问题汇总
- Module not found: Can't resolve 'web-vitals'
- Cannot access '__WEBPACK_DEFAULT_EXPORT__' before initialization
- State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect()
- Line 296:7: React Hook "useEffect" is called in function "next" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
本文主要是数据状态管理的升级以及路由升级的相关设置
环境
- node -v.18.15.0
- react - 16.9.0
脚手架的升级
升级过程中遇到的各种各样的问题记录一下
官方提供了一部分[升级指南]–如何升级到 React 18,所以仅记录一些官网没有的问题
包之间是存在依赖性的,因此并不是同时升级所有的包是最优选择,首先选定必须要升级的包,先升级之后在运行查看
关于脚手架的升级可查看react脚手架的升级,我们最好是先升级脚手架
react-router-dom 升级
根据查看包的更新命令发现react-router-dom也已经由5.0.1升级到6.20.1版本了
发现之前的路由的写法已经不支持了
而官网上关于路由的介绍感觉有点混乱。。。
查了很多资料,最后整理一下
发现针对路由的配置简化了很多
关于路由相关文件的写法–react-router-dom 5.0.1
入口渲染文件App.js
Root 是所有路由的外框架,老版本是在./view/root/index中初始化所有的路由,新版本的可以直接配置在路由文件src/views/page.js中了
import React from 'react';
import Root from './view/root/index';
import {Router, Switch, Route} from "react-router-dom";
import {Provider} from 'react-redux';
import {store} from './reducer/store';
import {createBrowserHistory} from "history";const history = createBrowserHistory();class App extends React.Component {render(){return (<Provider store={store}><Router history={history}><Switch><Route component={Root}/></Switch></Router></Provider>);}
}
export default App;
路由框架src/views/root/index.js
//老版
import React from 'react';
import {Switch, Route, Redirect} from "react-router-dom";
import {routerMap} from "../../view/pages";
import RouterGuide from '../routerguide/index'
const pagesRoute = () => {return routerMap.map((item, index) => {if("/"==item.path){return <Route path="/" exact key={index} render={(props) => {return <Redirect to={{pathname: item.redirectPath, search: `${props.location.search}`}}/>}}/>}else{return <RouterGuide key={index} path={item.path} component={item.component} auth={item.auth}/>} })
}class Root extends React.Component {render() {return (<div className="main-content"><Switch>{pagesRoute()}<Redirect to="/"/></Switch></div>)}
}
export default Root;
路由守卫 src/views/routerguide/index.jsx
import React from 'react';
import {Route} from "react-router-dom";
import {connect} from 'react-redux';
import {userMap} from '../../reducer/connect';
import {device} from 'device.js/dist/device'class RouterGuide extends React.Component {componentWillMount(){let {path, auth, userId} = this.propsif (window.WEIXIN && path !== "/mobile") {window.location.href = "/mobile";} else if (!device.mobile && path !== "/mobile") {window.location.href = "/mobile";} else if (device.mobile && !window.WEIXIN && ('' === userId.userId || null === userId.userId)) {if (auth) {window.location.href = "/index";}}}render(){let {path, component} = this.propsreturn (<Route path={path} component={component}/>)}
}export default connect(userMap.mapStateToProps, userMap.mapDispatchToProps)(RouterGuide);
路由文件src/views/page.js
//老版
import IndexPage from "../view/index/index";
import UserPage from "../view/user/index";
import DetailPage from "../view/detail/index";
import ResultPage from "../view/result/index";
export const routerMap=[{path:'/',redirectPath:'/index',errorPath:'/mobile',redirect:true,auth:false},{path:'/index',component:IndexPage,auth:false},{path:'/detail',component:DetailPage,auth:true},{path:'/user',component:UserPage,auth:true},{path:'/result',component:ResultPage,auth:false}
]
关于路由相关文件的写法–react-router-dom 6.20.1方式1
入口渲染文件App.js
import React from "react";
import { createBrowserRouter } from "react-router-dom";
import Root from './view/root/index';
function App() {return (<BrowserRouter><Root></Root></BrowserRouter>);
}
export default App;
路由框架src/views/root/index.js
import React,{Suspense} from "react";
import { Routes,Route, Outlet } from "react-router-dom";
import {routerMap} from "../../view/pages";
import RouterGuide from '../routerguide/index'// React.LazyExoticComponent<ComponentType<any>>
const withGuard = (item) => {return (<RouterGuideelement={<Suspense><item.element /></Suspense>}auth={item.auth}/>);
};
const pagesRoute = () => {return routerMap.map((item, index) => {return (<Routepath={item.path}key={index}element={withGuard(item)}/>);});
};
function Root() {return (<div className="main-content"><Routes>{pagesRoute()}</Routes></div>);
}
export default Root;
报错信息
[RouterGuide] is not a component. All component children of must be a or <React.Fragment>
该问题是想要像5.0.1版本一样,在pagesRoute设置路由守卫的时候报错了,<Routes></Routes>
的内部只能是<Route/>
,哪怕是定义路由守卫都不可以
直接在element设置为函数也报错,会报错Functions are not valid as a React child
,但是调用函数,并使用Suspense
标签即可
路由守卫 src/views/routerguide/index.jsx
新版的路由守卫可以直接在定义路由文件的时候直接设置,也即在方式2中通过src/views/page.js文件中设置,也可以单独设置
import React, { useEffect, Suspense } from "react";
import { Route, useNavigate, useLocation, Navigate } from "react-router-dom";
import { store } from "../../reducer/store";
// 具体实现根据项目需求来进行处理,返回是路由地址。
const onRouterBefore = (one, auth) => {if (!device.mobile) {return "/mobile";} else if (auth) {const state = store.getState();return state.userId ? one.pathname : "/index";} else {return one.pathname;}
};
function RouterGuide({ element, auth }) {const location = useLocation();const navigate = useNavigate();const { pathname } = location;useEffect(() => {// onRouterBefore 是对路由地址进行处理的函数const nextPath = onRouterBefore(location, auth);if (nextPath && nextPath !== pathname) {//路由重定向navigate(nextPath, { replace: true });}}, [pathname]);return element;
}
export default RouterGuide;
路由文件src/views/page.js
路由守卫在定义时直接设置,并且相当于在配置文件中直接设定了Root是外框架,不像老版本是在Root编写的,写法更加方便简洁
import Root from "../view/root/index"
import IndexPage from "../view/index/index";
import UserPage from "../view/user/index";
import DetailPage from "../view/detail/index";
import ResultPage from "../view/result/index";const routerMap=[{path:'/',element:(IndexPage),errorElement: (ErrorPage),auth:false,},{path:'/index',element:(IndexPage),auth:false},{path:'/detail',element:(DetailPage),auth:true},{path:'/user',element:(UserPage),auth:true},{path:'/result',element:(ResultPage),auth:false}
]
export {routerMap}
关于路由相关文件的写法–react-router-dom 6.20.1方式2
入口渲染文件App.js
//App.js 升级版
import React from "react";
import { RouterProvider,createBrowserRouter } from "react-router-dom";
import { routerMap } from "./view/pages";const router = createBrowserRouter(routerMap);
function App() {return <RouterProvider router={router} />;
}
export default App;
路由框架src/views/root/index.js
import React from "react";
import { Route, Outlet } from "react-router-dom";
function Root() {return (<div className="main-content"><Outlet /></div>);
}
export default Root;
路由文件 src/views/page.js
路由守卫在定义路由时直接设置,并且相当于在配置文件中直接设定了Root是外框架,不像老版本是手动引入Root,再在Root中引入路由拦截编写的,该方式写法更加方便简洁
import Root from "../view/root/index"
import IndexPage from "../view/index/index";
import UserPage from "../view/user/index";
import DetailPage from "../view/detail/index";
import ResultPage from "../view/result/index";// 具体实现根据项目需求来进行处理,返回是路由地址。
const onRouterBefore = (one, auth) => {if (!device.mobile) {return "/mobile";} else if (one.pathname == "/") {return "/index";} else if (auth) {const state = store.getState();return state.userId ? one.pathname : "/index";} else {return one.pathname;}
};
function Guard({ element, auth }) {const location = useLocation();const navigate = useNavigate();const { pathname } = location;useEffect(() => {// onRouterBefore 是对路由地址进行处理的函数const nextPath = onRouterBefore(location, auth);if (nextPath && nextPath !== pathname) {//路由重定向navigate(nextPath, { replace: true });}}, [pathname]);return element;
}
// React.LazyExoticComponent<ComponentType<any>>
const withGuard = (Comp, auth) => {return (<Guardelement={<Suspense><Comp /></Suspense>}auth={auth}/>);
};
const routerMap=[{path:'/',element:(Root),errorElement: (ErrorPage),auth:false,children:[{path:'/index',element:(IndexPage),auth:false},{path:'/detail',element:(DetailPage),auth:true},{path:'/user',element:(UserPage),auth:true},{path:'/result',element:(ResultPage),auth:false}]}
]
const pagesRoute = (list)=>{list.map(item=>{item.element=withGuard(item.element,item.auth)if(item.children){pagesRoute(item.children)}})
}
pagesRoute(routerMap)
export {routerMap}
路由的跳转
代码跳转
使用老版的history路由的化话,如上面代码介绍 使用 createBrowserHistory,实现跳转的方法:
let {history} = this.props;
history.push('/detail')
新版本跳转方法:
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
navigate("/detail")
注意,不是
usenavigation
,而是useNavigate
跳转
标签跳转
<Route path="/" element={<Navigate to="/index" replace />}/>
状态管理
React 状态管理–Redux4.0.4版本
- “redux”: “^4.0.4”
- “react-redux”: “^7.1.0”
action.js定义改变状态类型
const USERID = 'userId';
const USERNAME = 'userName';
const PHONE = 'phone';
const ADDRESS = 'address';
const action_userId = {type: USERID, text: 'update the user id'}
const action_userName = {type: USERNAME, text: 'update user name'}
const action_phone = {type: PHONE, text: 'update the phone'}
const action_address = {type: ADDRESS, text: 'update address'}
export {USERID,USERNAME,PHONE,ADDRESS,action_userId,action_userName,action_phone,action_address}
connect.js定义组件需要修改的全局变量
import {action_userId, action_userName,action_phone,action_address
} from './action.js'export const userMap = {mapStateToProps(state) {return state;},mapDispatchToProps(dispatch) {return {getUserId: () => {dispatch(action_userId)},getUserName: () => {dispatch(action_userName)}}}
};
//对使用到的状态更新
export const userInfoMap = {mapStateToProps(state) {return {userId: state.userId,phone: state.phone,address:state.address}},mapDispatchToProps(dispatch) {return {getUserId: () => {dispatch(action_userId)},getPhone: () => {dispatch(action_phone)},getAddress: () => {dispatch(action_address)}}}
};
redux.js 定义改变状态类型
import {combineReducers} from 'redux';
import {USERID,USERNAME,PHONE,ADDRESS} from './action.js'/*** 触发的处理函数* @param state 设置一个初始值,若没有显示该初始值* @param action * @returns {({} & {userId: string})|{userId: string}}*/
const getUserId = (state = {userId: ''}, action) => {switch (action.type) {case USERID:return Object.assign({}, state, {userId: state.userId})default:return state}
}
/*** 身份证号* @param state* @param action* @returns {{userName: string}|({} & {userName: string})}*/
const getUserName = (state = {userName: ''}, action) => {switch (action.type) {case USERNAME:return Object.assign({}, state, {userName: state.userName})default:return state}
}
/**** @param state* @param action* @returns {({} & {phone: string} & {phone: *})|{phone: string}}*/
const getPhone = (state = {phone: ''}, action) => {switch (action.type) {case PHONELABEL:return Object.assign({}, state, {phoneLabel: state.phone})default:return state}
}/**** @param state* @param action* @returns {({} & {address: string} & {address: *})}*/
const getAddress = (state = {address: ''}, action) => {switch (action.type) {case ADDRESS:return Object.assign({}, state, {address: state.address})default:return state}
}
/*** 多个reducer方法 * @type {Reducer<any>}*/
export const allReducer = combineReducers({userId: getPersonId,userName: getUserName,address: getAddress,phone:getPhone,
})
store.js 定义所有的全局变量
import {createStore} from 'redux'
import {allReducer} from './redux'
export const store = createStore(allReducer)
App.js 全局变量的注册:
import React from 'react';
import {Router, Switch, Route} from "react-router-dom";
import {Provider} from 'react-redux';
import {store} from './reducer/store';import {createBrowserHistory} from "history";
import Template from './components/template/template';const history = createBrowserHistory();
class App extends React.Component {render(){return (<Provider store={store}><Router history={history}><Switch><Route component={Template}/></Switch></Router></Provider>);}
}
export default App;
组件使用全局变量
import React from 'react'
import {connect} from "react-redux";
import {userInfoMap} from "../../reducer/connect";
import {setTitle} from '../../global';
import axios from 'axios';
import './index.css'class index extends React.Component {constructor(props){super(props);this.next = this.next.bind(this);this.changeUserName = this.changeUserName.bind(this);this.changeAddress = this.changeAddress.bind(this);this.changePhone = this.changePhone.bind(this);this.clearValue = this.clearValue.bind(this);this.state = {userId:"",userName: '',phone: '',address: '',errorMes: ''}}clearValue(){this.setState({userName: '',phone: '',address: '',errorMes: ''})}changeUserName(event){let val = event.target.value;this.setState({userName: val,}, () => {if (val.length>10) {this.setState({errorMes: "最大输入10个字符"})}})}changeAddress(event){let val = event.target.value;this.setState({address: val,}, () => {if (val.length>10) {this.setState({errorMes: "最大输入10个字符"})}})}changePhone(event){let val = event.target.value;this.setState({phone: val,}, () => {if (val.length>11) {this.setState({errorMes: "最大输入11个字符"})}})}login(){let {userId, address, phone,userName} = this.props;axios({method:'post',url:"/login", data: {userName: this.state.userName,address: this.state.address,phone: this.state.phone}}).then((response) => {if (response.responseCode === "200") {//更新当前组件,局部状态变更this.setState({userId: response.data.userId}, () => {let {history} = this.props;history.push('/home')})//更新全局数据userId.userId = response.data.userId;address.address = response.data.address;phone.phone = response.data.phone;userName.userName = response.data.userName;} else {//局部状态变更this.setState({errorMes: '请求超时请稍后再试',}, () => {//状态变更后的回调函数// this.changeInput();})}})}next(){this.login();}//初次挂载完成componentDidMount(){setTitle("客户信息");}//组件卸载componentWillUnmount(){del();}render(){return (<div className="page"><div className="margin"><div className="form-group top10"><input type="text" value={this.state.userName}onChange={(e) => this.changeUserName(e)} placeholder="请输入您的姓名"/><i onClick={() => this.clearValue()}></i></div><div className="form-group top10"><input type="text" value={this.state.address}onChange={(e) => this.changeAddress(e)} placeholder="请输入您的地址"/><i onClick={() => this.clearValue()}></i></div><div className="form-group top10"><input type="text" value={this.state.phone}onChange={(e) => this.changePhone(e)} placeholder="请输入您的手机号"/><i onClick={() => this.clearValue()}></i></div><div className="form-group"><label onClick={() => this.next()}>下一步</label></div></div></div>)}
}export default connect(userInfoMap.mapStateToProps, userInfoMap.mapDispatchToProps)(index)
React 状态管理–Redux5.0.0版本
- “redux”: “^5.0.0”
- “@reduxjs/toolkit”: “^2.0.1”
- “react”: “^18.2.0”
store.js 定义所有全局状态
import { createSlice, configureStore } from "@reduxjs/toolkit";const initialState = {userId: "",userName: "",phone: "",address: "",
};
const statusSlice = createSlice({name: "stateGlobal",initialState: initialState,reducers: {appReducer: (state, action) => {return Object.assign({}, state, {...action.payload});}}
});
export const store = configureStore({reducer: statusSlice.reducer
});
export const { appReducer } = statusSlice.actions;
store.js 定义所有全局状态–持久化版本
首先,需要添加持久化的包
npm i redux-persist
其次,修改相关代码
import { createSlice, configureStore } from "@reduxjs/toolkit";
import { persistStore, persistReducer, PERSIST} from "redux-persist";
import storage from "redux-persist/lib/storage";const initialState = {userId: "",userName: "",phone: "",address: "",
};
const statusSlice = createSlice({name: "stateGlobal",initialState: initialState,// devTools : 开启redux-devtools,默认开启,开发环境开启,生产环境关闭devTools: process.env.NODE_ENV === "development",reducers: {appReducer: (state, action) => {return Object.assign({}, state, {...action.payload});},otherReducer:(state, action) => {return Object.assign({}, state, {userId:action.payload});},}
});
//在localStorge中生成key为root的值
const persistConfig = {key: "root",version: 1,storage,blacklist: [] //设置某个reducer数据不持久化,
};
const reducers = persistReducer(persistConfig, statusSlice.reducer);
const store = configureStore({reducer: reducers,middleware: (getDefaultMiddleware) =>getDefaultMiddleware({serializableCheck: {//设置序列化检测ignoredActions: [PERSIST],//忽略的action类型ignoredActionPaths: [],//忽略action中的路径ignoredPaths: []//忽略state中的路径}// serializableCheck: false //关闭redux序列化检测})
});
const persistor = persistStore(store);
export const { appReducer } = statusSlice.actions;
export { store, persistor };
组件使用全局变量:
import React from 'react'
import { useNavigate } from "react-router-dom";
import { store, appReducer } from "../../reducer/store";
import {setTitle} from '../../global';
import axios from 'axios';
import './index.css'export default function Index() {const navigate = useNavigate();//获取全局数据const init=store.getState(); const [state,setState] = useState({userId:"",userName: '',phone: '',address: '',errorMes: ''})const clearValue=()=>{setState({...state,userName: '',phone: '',address: '',errorMes: ''})}const changeUserName=(event)=>{let val = event.target.value;setState({...state,userName: val,errorMes: val.length>10?"最大输入10个字符":state.errorMes})}const changeAddress=(event)=>{let val = event.target.value;setState({...state,address: val,errorMes: val.length>10?"最大输入10个字符":state.errorMes})}const changePhone=(event)=>{let val = event.target.value;setState({...state,phone: val,errorMes: val.length>11?"最大输入11个字符":state.errorMes})}const login=()=>{ axios({method:'post',url:"/login", data: {userName: state.userName,address: state.address,phone: state.phone}}).then((response) => {if (response.responseCode === "200") {//更新当前组件,局部状态变更setState({...state,userId: response.data.userId})navigate('/home')//更新全局数据store.dispatch(appReducer({userId: response.data.userId,address: response.data.address,phone: response.data.phone,userName: response.data.userName,}))} else {//局部状态变更setState({...state,errorMes: "请求超时请稍后再试"})//状态变更后的回调函数,没法设置,setState不支持回调// changeInput();}})}const next=()=>{login();}//初次挂载完成useEffect(()=>{setTitle("客户信息");},[])//组件卸载useEffect(()=>{return ()=>{del();}})return (<div className="page"><div className="margin"><div className="form-group top10"><input type="text" value={state.userName}onChange={(e) => changeUserName(e)} placeholder="请输入您的姓名"/><i onClick={() => clearValue()}></i></div><div className="form-group top10"><input type="text" value={state.address}onChange={(e) => changeAddress(e)} placeholder="请输入您的地址"/><i onClick={() => clearValue()}></i></div><div className="form-group top10"><input type="text" value={state.phone}onChange={(e) => changePhone(e)} placeholder="请输入您的手机号"/><i onClick={() => clearValue()}></i></div><div className="form-group"><label onClick={() =>next()}>下一步</label></div></div></div>)
}
通过以上方式发现现有的状态管理比以前简单很多
报错信息
store.js:60 A non-serializable value was detected in an action, in the path: register
. Value: ƒ register2(key) { _pStore.dispatch({ type: REGISTER,key});}
import { persistStore, persistReducer, PERSIST} from "redux-persist";
const store = configureStore({reducer: reducers,middleware: (getDefaultMiddleware) =>getDefaultMiddleware({serializableCheck: {// Ignore these action typesignoredActions: [PERSIST],// Ignore these field paths in all actionsignoredActionPaths: [],// Ignore these paths in the stateignoredPaths: []}// serializableCheck: false //关闭redux序列化检测})
});
一开始 middleware
写错了位置,写在了createSlice中,后来仔细检查代码发现是位置写错了!!!!
生命周期
老版本的生命周期非常清晰,新版版的因为不推荐组件式的组件,推荐的是函数式组件,然后生命周期就非常混乱
老版本比较关注:
- 首次挂载
- 卸载前
- 状态更新的时候
发现 React18.2.0 采用的是函数式组件,并且状态更新的回调已经没有了,所以不能行云流水的在设置卸载前的功能了,挂载后需要执行的操作,并且因为每次状态更新都要重新渲染 DOM ,因此如果在之后的回调中需要设置操作,但是没有状态后的回调了,导致整体逻辑去设置的时候非常混乱,会存在很多坑!!!并且存在什么渲染两次!!!!!!!这些都需要自己考虑,感觉是对用户的维护成本很高
新版本的虽说是可以使用useEffect
,但是逻辑非常不清晰,并且官网的手册也根本没有对这些变更给与非常明确的解决方案,感觉?#$%^&*,我们需要花大量的时间去考虑各个变量之间的关系,可以不可以在某个位置更新,反正目前为止在升级的过程中的维护感觉很痛苦,我在考略要不要放弃这个框架了。。。。。。
问题汇总
这里是升级过程中遇到的问题汇总
Module not found: Can’t resolve ‘web-vitals’
如果启动后发现以上报错信息,说明之前米有初始化该工具包,初始化一下即可
npm i web-vitals@2.1.4 -D
Cannot access ‘WEBPACK_DEFAULT_EXPORT’ before initialization
es6模块循环依赖问题导致,查找代码把循环依赖注释掉即可
State updates from the useState() and useReducer() Hooks don’t support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect()
在更新前,使用this.setState()
修改状态,并且this.setState()
是异步函数,有回调函数的;
包升级后,使用useState()
修改状态,并且useState()
是异步函数,但是没有回调函数
Line 296:7: React Hook “useEffect” is called in function “next” that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word “use”
这是本人在开发工程中,从网上查找相关资料,说是组件名称没有大写开头,可是本人组件名称是大写!!!!但是提示next不是React组件方法名 ,感觉有点莫名其妙,想到next确实小写,本人将 useEffect 方式写在了函数"next"中,移到组件根块下报错消失!!!!所以useEffect是必须直接使用在组件结构下