大家好,我是前端西瓜哥。今天我们看看在React中使用Redux的4种方法。Redux是一个状态容器JS库,提供可预测的状态管理,经常与React配合管理应用程序的全局状态并执行响应式组件更新。Redux一般不需要,只有在项目比较复杂的时候,比如多个组件分散在不同的地方使用相同的状态。在这种情况下,如果将代码逐层通过props传递,代码将变得不可维护。这时候我们可以考虑使用Redux这样的状态管理库。我们不使用Redux,而是创建一个显示用户名并支持设置用户名的User组件。下面我们来看看不使用Redux是如何写的。import{Component,createRef}from'react';classUserextendsComponent{state={username:'FrontendWatermelon'};inputRef=createRef();setUsername=()=>{this.setState({username:this.inputRef.current.value});};render(){return(
用户名:{this.state.username}
设置用户名);}}导出默认用户;让我们转换这个组件并将状态迁移到Redux。写的最底层,Redux,跟框架无关。我们先看看只使用Redux库的写法。演示:https://codesandbox.io/s/redux-plain-demo-bc4kv0。首先我们创建一个reducer。//user_reducer.jsimport{SET_USERNAME}from'./constants';//初始值constdefaultState={name:'FrontWatermelon',age:88};//reducerexport用于修改用户状态constuserReducer=(preState=defaultState,action)=>{switch(action.type){caseSET_USERNAME://所有类型值都放在常量中return{...preState,name:action.payload};//也可以在这里根据需要添加setUserAgadefault:returnpreState;之类的逻辑}};//constants.jsexportconstSET_USERNAME='SET_USERNAME';reducer是一个更新状态的函数,接收原始状态preState和一个updateaction对象action。动作对象有描述类型和代表动作的其他数据属性(通常是负载)。有效负载将以某种方式计算出一个新状态,替换redux中的原始状态。{type:'SET_USERNAME',payload:'Newusername'}type通常是一个字符串,例如,我们将使用'COUNT_INCREMENT'来为计数器加一,或使用'SET_USERNAME'来更新用户名。reducer会根据不同的类型执行不同的更新状态行为。我们通常使用一个函数来帮助构造动作,这个函数叫做ActionCreator://user_action.jsimport{SET_USERNAME}from'./constants';exportconstsetUsernameAction=(data)=>{return{type:SET_USERNAME,payload:data};};使用减速器,我们可以使用它们来构建我们的商店。store可以访问保存在redux中的所有状态:import{combineReducers,createStore}from'redux';从'./user_reducer'导入{userReducer};conststore=createStore(combineReducers({user:userReducer}));exportdefaultstore;combineReducers可以将多个reducer组合在一起,并带有相应的属性名称。比如在上面的代码中,我们可以通过store.getState().user来获取到user对象。如果你添加一个新的计数器状态对象,只需要添加counter:counterReducer,你可以使用store.getState().counter来获取这个对象。createStore用于创建应用程序中的所有状态,然后将这些状态存储在返回的存储中。现在我们的用户组件看起来像这样:import{Component,createRef}from'react';从'../store/store'导入商店;从'../store/user_action'导入{setUsernameAction};类用户扩展组件{inputRef=createRef();componentDidMount(){store.subscribe(()=>{this.setState({});});}setUsername=()=>{store.dispatch(setUsernameAction(this.inputRef.current.value));};render(){return(
用户名:{store.getState().user.name}
设置用户名);}}exportdefaultUser;store.getState()可以获取state对象,通过它,我们可以获取到我们需要的对象,比如user对象。store.dispatch(action)调度动作对象以触发状态更新。store.subscribe(fn)订阅状态??变化并执行回调函数。在这里,我们一发现状态已经改变就重新渲染组件。Redux的本质是发布订阅模式,状态集中在一起,可以通过store.getState()获取状态,通过store.dispatch(action)改变状态,通过store.dispatch(action)订阅状态改变store.subscribe(fn)(React组件监听到变化后,重新渲染组件)。这种写法是最原始的写法,可以用在任何框架中。缺点很明显:使用redux的组件需要订阅状态变化,并在组件发生变化时立即重新渲染。有时其他组件的状态发生变化,当前组件会不必要地重新渲染。自己判断吧,太繁琐,容易出错,还容易忘记订阅。对于忘记订阅的问题,我们也可以直接让根组件监听并重新渲染,但是这样性能很差。接下来西瓜要讲的React-Redux库可以解决这个问题。它只能在当前组件使用的特定状态发生变化时重新渲染组件。React-Redux发现大家喜欢在React中使用Redux,于是Facebook发布了一个React-Redux库,让大家可以更好更正确的在React中使用Redux。React-Redux与连接高级组件一起工作。我们先来看看如何使用connect。演示:https://codesandbox.io/s/react-redux-with-connect-demo-kfvxt6。React-Redux引入了容器组件的概念,负责处理redux。容器组件实际上是一个高级组件,它封装了真正的UI组件,并在其上进行以下工作:映射状态并分派到props并将其注入到UI组件中。侦听状态更改并在必要时重新呈现UI组件。高阶组件:接受组件参数并返回新组件的函数。高层组件的作用是为真正的UI组件封装一些可重用的逻辑,通常用于增强功能。随着ReactHooks越来越流行,现在人们更喜欢使用ReactHooks而不是写起来更优雅的高阶函数。constContainerComponent=connect(mapStateToProps,mapDispatchToProps,)(UIComponent);现在开始改造项目。我们创建一个container文件夹,把User.jsx文件放在里面,在里面写入如下内容://containers/User.jsximport{connect}from'react-redux';importUserUIfrom'../components/User';从'../store/user_action'导入{setUsernameAction};exportdefaultconnect(//mapStateToProps(state)=>({user:state.user}),//mapDispatchToProps(dispatch)=>({setUsername:(newName)=>dispatch(setUsernameAction(newName))}))(用户界面);然后记得在使用容器的地方传入我们的store对象,如下:importUserContainerfrom'./containers/User';importstorefrom'./store/store';import'./styles.css';exportdefaultfunctionApp(){return
;}当然,如果每个容器组件都传入store会比较麻烦,我们通常采用另一种方式:使用redux提供的ContextProvider-react对根组件进行包裹,如下:import{Provider}from'react-redux';ReactDOM.render(
,document.getElementById('root'));然后是UI组件的改造:import{Component,createRef}from'react';类用户扩展组件{我nputRef=createRef();render(){return(用户名:{this.props.user.name}
this.props.setUsername(this.inputRef.current.value)}>设置用户名 );}}导出默认用户;UI组件props会取到用户对象,setUsername方法,以及我们注入的store对象(如果使用Context方法是获取不到的),使用connect后,只有当组件使用的状态发生变化时,才会触发组件的更新。这里有一点需要特别注意,就是你要保证新状态对象不等于旧状态,从而触发组件重新渲染,处理对象时容易出错方法。需要复制一个新的对象作为新的状态,推荐使用展开运算符。//user_reducer.js//写法不对,新的状态还是指向原来的对象preState.name=action.payload;returnpreState;//正确的写法return{...preState,name:action.payload};或者考虑使用immer,一个不可变的数据结构库。React-RudexwithReactHooks我们使用了一个高级组件,比如connect来增强UI组件的功能。说到增强,react-redux也提供了现在很流行的ReactHooks写法,比较优雅,也是我公司目前的做法。这里我们不需要连接高阶组件,也就是说我们不需要容器组件。演示:https://codesandbox.io/s/react-redux-with-hooks-demo-8ixl0h。//User.jsimport{useEffect,useRef}from'react';从'react-redux'导入{useDispatch,useSelector};从'../store/user_action'导入{setUsernameAction};constUser=()=>{//获取状态constuser=useSelector((state)=>state.user);//获取调度方法constdipatch=useDispatch();constinputRef=useRef(null);返回(用户名:{user.name}
{constnewName=inputRef.current.value;dipatch(setUsernameAction(newName));}}>设置用户名 );};exportdefaultUser;通过useSelector,我们可以获取上下文绑定的状态,然后从中获取我们需要使用的状态。constuser=useSelector((state)=>state.user);如果不止一个,我们可以写成一个对象:const{user,counter}=useSelector((state)=>({user:state.user,counter:state.counter}));是不是有点像connect的mapStateToProps。然后获取dispatch方法:constdispatch=useDispatch();hook很优雅,但是我也发现相比于connect方法,我们的redux状态逻辑和组件是耦合在一起的。但是一般我们的组件都是业务组件,这还是可以接受的。我们从ReduxToolkit可以看出,我们需要维护一个状态,我们需要编写reducer方法,actioncreator方法,并使用一个content.js文件来集中管理所有的actionType字符串。你发现自己写了很多模板代码,每增加一个状态,就需要创建上面的东西,在各种文件中跑来跑去,看得人都麻木了。于是redux又出来了一个工具集库ReduxToolkit来解决这个问题。demo:https://codesandbox.io/s/redux-toolkit-demo-8x0391ReduxToolkit提供了createSlice方法,可以帮助你用更少的代码生成配套的reducer和action,可维护性好。//userSlice.jsimport{createSlice}from'@reduxjs/toolkit';constuserSlice=createSlice({name:'user',initialState:{name:'前端西瓜哥',age:88},reducers:{setUsername:(state,action)=>{//由于ReduxToolkit内置了immer,所以可以直接更改。state.name=action.payload;}}});//actionsexportconst{setUsername}=userSlice.actions;//获取你需要的状态,并在组件的userSelector钩子中使用它。exportconstselectUser=(state)=>state.user;//reducerexportdefaultuserSlice.reducer;createSlice传入name(标识符,用于生成action)、initialState(初始值)、reducers(对象形式)参数,返回一个东西。返回的slice对象有一个actions对象属性。比如上面代码中,在actions下有一个setUsername方法,执行后会返回{type:"user/setUsername",payload:"newname"}。可以看到action的类型是根据name和reducers的属性产生的,保证唯一性。Slice还有一个reducer对象,其实就是将之前传递的带有自动生成action的reducer转换成函数形式。createSlice是做什么的?createSlice将原本管理状态但分离代码的action和reducer集合在一起,因此您无需自己命名actionType。然后生成store,改成configureStore://store.jsimport{configureStore}from'@reduxjs/toolkit';从'./userSlice'导入userReducer;conststore=configureStore({reducer:{user:userReducer}});exportdefaultstore;Summary简单总结一下:原来的写法太简单了,需要通过store.subscribe判断一个组件是否需要重新渲染(fn),写起来麻烦,性能堪忧。结合ReduxReact库,通过connect注入redux状态。需要额外写一个connect高层组件生成的容器组件,但是降低了耦合度。ReduxReact只会在所需状态发生变化时重新渲染组件。这里需要注意的是,在改变的时候,新旧状态不能相同,尤其是对象的情况,否则重新渲染是不行的。如果你的项目主要使用函数式组件,可以不用connect直接使用useSelector获取状态,使用userDispatch改变状态。十分优雅。Redux还推出了ReduxToolkit,解决了配置复杂、模板编写过多、需要手动安装大量相关包等问题。