本文最后更新于:2023年8月24日 晚上
Redux介绍 redux是一个专门用来做状态管理的JS库(不是react插件库),前端三大框架都可以使用,但基本和react配合使用,点击进入官方文档
使用条件:
某个组件的状态,需要让其他组件可以随时拿到(共享)
一个组件需要改变另一个组件的状态(通信)
总体原则:能不用就不用,如果不用比较吃力才考虑使用
三大核心属性 action 一个js对象,包含两个属性:
type:标识属性,值是字符串。多个type用action分开
payload:数值属性,可选。表示本次动作携带的数据
必须带有type属性,只是说明有事情发生了这件事,并没有说明应该如何跟更新state
reducer 一个纯函数,作用:
根据传入的旧状态和action,返回新状态
(previousState, action) => newState
store 仓库,redux的核心,整合action和reducer
特点:
一个应用只有一个store
获取状态:store.getState()
创建store时接收reducer作为参数:const store = createStore(reducer)
发起状态更新时,需要分发action:store.dispatch(action)
监听状态变化:const unSubscribe = store.subscribe(() => {})
取消监听:unSubscribe()
下载 1 2 3 yarn add redux npm i redux -S
使用Redux 创建store(主要仓库) 新建 redux / store.ts
1 2 3 4 5 6 import { createStore } from 'redux' import countReducer from './reducers/countReducer' const store = createStore (countReducer)export default store
在react18版本,createStore已被标记为弃用,新用法需要用到redux-thunk这个中间件(见下面的异步action)
下载 1 2 3 npm i @reduxjs/toolkit yarn add @reduxjs/toolkit
创建store对象 1 2 3 4 5 6 7 8 import { configureStore } from '@reduxjs/toolkit' import countReducer from './reducers/countReducer' const store = configureStore ({ reducer : countReducer })export default store
创建reducers(执行函数) 新建 redux / reducers / countReducer.ts
1 2 3 4 5 6 7 8 9 10 11 12 export default (state : number = 0 , action : {type : string , data : any }) => { const { type , data } = action console .log (state, action) switch (type ) { case '+' : return state + data case '-' : return state - data default : return state } }
state 当前的state树
action 要处理的行为
返回新的state树
获取state的值 1 2 import store from '../../redux/store' store.getState ()
使用reducers函数更新state 1 2 import store from '../../redux/store' store.dispatch ({ type : '+' , data : 1 })
但是注意:就算更新state中的数据,dom也不会刷新,需要手动监听后刷新
手动监听渲染 直接在main.tsx中建立redux的监听即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import React from 'react' import { createRoot } from 'react-dom/client' import App from './App' import { BrowserRouter } from 'react-router-dom' import store from './redux/store' const rootDom = createRoot (document .getElementById ('root' ) as HTMLElement )const render = ( ) => { rootDom.render ( <React.StrictMode > <BrowserRouter > <App /> </BrowserRouter > </React.StrictMode > ) }render () store.subscribe (render)
小案例 计数器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import { Button } from 'antd' import store from '../../redux/store' const Count = ( ) => { let selectRef : HTMLSelectElement const handler : { [k : string ]: () => void } = { '+' () { changeState ('+' ) }, '-' () { changeState ('-' ) }, 'odd+' () { const state = store.getState () console .log ('当前为:' , state) if (state % 2 === 1 ) changeState ('+' ) }, async 'async+' () { changeState ('+' ) } } const changeState = (type : string ) => { store.dispatch ({ type , data : Number (selectRef.value ) }) } return ( <div > <h1 > 当前求和为:{store.getState()}</h1 > <select ref ={(e: HTMLSelectElement ) => { selectRef = e }} > <option value ="1" > 1</option > <option value ="2" > 2</option > <option value ="3" > 3</option > </select > <Button onClick ={handler[ '+']}> +</Button > <Button onClick ={handler[ '- ']}> -</Button > <Button onClick ={handler[ 'odd +']}> 当前求和为奇数再加</Button > <Button onClick ={handler[ 'async +']}> 异步加</Button > </div > ) }export default Count
1 2 3 4 5 6 7 8 9 10 11 12 13 export default (state : number = 0 , action : {type : string , data : any }) => { const { type , data } = action console .log (state, action) switch (type ) { case '+' : return state + data case '-' : return state - data default : return state } }
1 2 3 4 5 6 7 import { createStore } from 'redux' import countReducer from './reducers/countReducer' const store = createStore (countReducer)export default store
完善结构 封装类型ts文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { Dispatch } from 'react' export interface IAction <T> { type : string , data : T }type IAsyncActionFn <T> = (dispatch: Dispatch<IAction<T>> ) => void export interface ICreateAction <T> { [k : string ]: (data: T ) => (IAction <T> | IAsyncActionFn <T>) }export interface IReducerHandle <T> { [k : string ]: (state: T, data: T ) => T }
封装type常量文件 1 2 3 export const INCREASE = 'increase' export const DECREASE = 'decrease'
用于创建action的文件 1 2 3 4 5 6 7 8 9 import { Dispatch } from 'react' import { IAction , ICreateAction } from '../../typeFile' import { INCREASE , DECREASE } from '../typeConstant' export default { increase : data => ({ type : INCREASE , data }), decrease : data => ({ type : DECREASE , data }) } as ICreateAction <number >
reducer执行文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import { INCREASE , DECREASE } from '../typeConstant' import { Reducer } from 'redux' import { IAction , IReducerHandle } from '../../typeFile' const handle : IReducerHandle <number > = { [INCREASE ]: (state, data ) => state + data, [DECREASE ]: (state, data ) => state - data }export default ((state = 0 , action ) => { const { type , data } = action if (handle[type ]) return handle[type ](state, data) return state }) as Reducer <number , IAction <number >>
异步action 同步action就只是封装返回一个有type和data属性的对象
异步action就是返回一个函数,但是需要引入中间件,因为action只能为一个简单的object对象,需要使用中间件配置store
配置中间件 下载中间件 1 2 3 npm install redux-thunk yarn add redux-thunk
在store.ts中配置 1 2 3 4 5 6 7 8 9 10 import { configureStore, applyMiddleware } from '@reduxjs/toolkit' import countReducer from './reducers/countReducer' import thunk from 'redux-thunk' const store = configureStore ({ reducer : countReducer, enhancers : [applyMiddleware (thunk)] })export default store
这里用的是RTK(redux toolkit),不知道的点击此处查看
但不知道为什么,我不配置发现也可以正常使用
创建异步action 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { Dispatch } from 'react' import { IAction , ICreateAction } from '../../typeFile' import { INCREASE , DECREASE } from '../typeConstant' const creatorAction : ICreateAction <number > = { increase : data => ({ type : INCREASE , data }), decrease : data => ({ type : DECREASE , data }), asyncIncrease : data => (dispatch: Dispatch<IAction<number >> ) => { setTimeout (() => { dispatch (creatorAction.increase (data) as IAction <number >) }, 1000 ) } }export default creatorAction
返回的回调函数中第一个参数为dispatch
这样调用:
1 2 3 4 5 6 changeState (countAction.asyncIncrease )const changeState = (actionFn: Function ) => { store.dispatch (actionFn (Number (selectRef.value ))) }
使用React-Redux react看在项目都使用redux状态管理,所以自己出了一个库,就叫react-redux ,配合原生redux使用
介绍
所有的UI组件都应该包裹一个容器组件,他们是父子关系
容器组件是真正和redux打交道的,里面可以随意的使用redux的api
UI组件中不能使用任何redux的api
容器组件可以传给UI组件的东西
redux中所保存的状态
用于操作状态的方法
备注:容器给UI传递:状态,操作状态的方法,均通过props传递
下载 1 2 3 npm install react-redux yarn add react-redux
创建容器组件 创建 src/containers/Count/index.ts
1 2 3 import Count from '../../components/Count' import { connect } from 'react-redux' export default connect ()(Count )
就相当于使用react-redux对UI组件进行了封装,在外面套了层容器
注意点:
UI组件中不可出现redux的API,如store.getState()
容器要有store属性,绑定你的store对象,如下
将渲染的组件从UI组件改为容器组件并为容器组件传递store对象(没有会报错)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React , { Component } from 'react' import Count from './containers/Count' import store from './redux/store' class App extends Component { render () { return ( <div > <Count store ={store} /> </div > ) } }export default App
使用了connect连接之后就相当于UI组件变成了容器组件的子组件
使用容器组件向UI组件传递状态和函数 在connect方法第一次调用时传递参数即可,在标签处只能使用store
1 2 3 4 5 6 7 8 9 10 11 12 import Count from '../../components/Count' import { connect } from 'react-redux' const stateObject = ( ) => ({ myName : 'F_SAN' })const fnObject = ( ) => ({ getName : () => '名字为F_SAN' })export default connect (stateObject, fnObject)(Count )
connect两个参数返回的都是一个简单对象,第一个传递状态,第二个传递函数
UI组件接收参数和函数对象 对于class组件来说,直接使用props就可以接收到,对于函数式组件来说,写在第一个参数接收
1 2 3 4 5 6 7 8 9 interface IProps { myName : string , getName (): string }const Count = (props: IProps ) => { const { myName, getName } = props console .log (myName, getName ()) ...
修改计数器案例 封装类型接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export interface IStateObject { count : number }export interface IFnObject { increase (insertValue : number ): void decrease (insertValue : number ): void asyncIncrease (insertValue : number ): void }export interface IProps extends IStateObject , IFnObject { }
在容器组件中添加获取当前计数器和插入删除api
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import Count from '../../components/Count' import { connect } from 'react-redux' import countActionCreator from '../../redux/action/countActionCreator' import { IStateObject , IFnObject } from '../../typeFile' import { Dispatch } from 'react' const stateObject = (state: any ) => ({ count : state }) as IStateObject const fnObject = (dispatch: Dispatch<any > ) => ({ increase : insertValue => { dispatch (countActionCreator.increase (insertValue)) }, decrease : insertValue => { dispatch (countActionCreator.decrease (insertValue)) }, asyncIncrease : insertValue => { dispatch (countActionCreator.asyncIncrease (insertValue)) } }) as IFnObject export default connect (stateObject, fnObject)(Count )
所以这里是不需要引用store的,在connect中都封装好了,这里对于fnObject有一个最简的写法(默认分发),直接写成一个对象,而不是返回一个对象
1 2 3 4 5 6 7 8 9 10 11 const { increase, decrease, asyncIncrease } = countActionCreatorconst fnObject = { increase, decrease, asyncIncrease } as any export default connect (stateObject, fnObject)(Count )
修改后的UI组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import { Button } from 'antd' import { IProps } from '../../typeFile' const Count = (props: IProps ) => { const { count, increase, decrease, asyncIncrease } = props let selectRef : HTMLSelectElement const handler : { [k : string ]: () => void } = { increase () { increase (Number (selectRef.value )) }, decrease () { decrease (Number (selectRef.value )) }, increaseByOdd () { console .log ('当前为:' , count) if (count % 2 === 1 ) increase (Number (selectRef.value )) }, asyncIncrease () { asyncIncrease (Number (selectRef.value )) } } return ( <div > <h1 > 当前求和为:{count}</h1 > <select ref ={(e: HTMLSelectElement ) => { selectRef = e }} > <option value ="1" > 一</option > <option value ="2" > 二</option > <option value ="3" > 三</option > </select > <Button onClick ={handler.increase} > +</Button > <Button onClick ={handler.decrease} > -</Button > <Button onClick ={handler.increaseByOdd} > 当前求和为奇数再加</Button > <Button onClick ={handler.asyncIncrease} > 异步加</Button > </div > ) }export default Count
之前使用redux 的时候,状态发生改变后,页面并不会重新渲染,手动监听当时的代码是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const rootDom = createRoot (document .getElementById ('root' ) as HTMLElement )const render = ( ) => { rootDom.render ( <React.StrictMode > <BrowserRouter > <App /> </BrowserRouter > </React.StrictMode > ) }render () store.subscribe (render)
但是,用了react-redux之后,因为是react自己出的,当容器组件的状态发生改变,会自动的渲染一次到UI组件上,就可以省略监听的代码了
自动传递store属性(Provider) 对于很多容器组件都需要传递store时,如下写法会太麻烦:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React , { Component } from 'react' import Count from './containers/Count' import store from './redux/store' class App extends Component { render () { return ( <div > <Count store ={store} /> <Count store ={store} /> <Count store ={store} /> <Count store ={store} /> <Count store ={store} /> <Count store ={store} /> </div > ) } }export default App
所以就有了Provider,我们只需要在main.tsx中将App整个套起来,它就可以自动为每个容器组件加上store属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from 'react' import { createRoot } from 'react-dom/client' import App from './App' import { BrowserRouter } from 'react-router-dom' import { Provider } from 'react-redux' import store from './redux/store' const rootDom = createRoot (document .getElementById ('root' ) as HTMLElement ) rootDom.render ( <React.StrictMode > <Provider store ={store} > <BrowserRouter > <App /> </BrowserRouter > </Provider > </React.StrictMode > )
容器组件和UI组件结构优化 写成多个文件,会使以后的文件成倍增长,所以可以直接将容器组件和UI组件成为一个文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import { connect } from 'react-redux' import countActionCreator from '../../redux/action/countActionCreator' import { IProps } from '../../typeFile' import { Button } from 'antd' const Count = (props: IProps ) => { const { count, increase, decrease, asyncIncrease } = props let selectRef : HTMLSelectElement const handler : { [k : string ]: () => void } = { increase () { increase (Number (selectRef.value )) }, decrease () { decrease (Number (selectRef.value )) }, increaseByOdd () { console .log ('当前为:' , count) if (count % 2 === 1 ) increase (Number (selectRef.value )) }, asyncIncrease () { asyncIncrease (Number (selectRef.value )) } } return ( <div > <h1 > 当前求和为:{count}</h1 > <select ref ={(e: HTMLSelectElement ) => { selectRef = e }} > <option value ="1" > 一</option > <option value ="2" > 二</option > <option value ="3" > 三</option > </select > <Button onClick ={handler.increase} > +</Button > <Button onClick ={handler.decrease} > -</Button > <Button onClick ={handler.increaseByOdd} > 当前求和为奇数再加</Button > <Button onClick ={handler.asyncIncrease} > 异步加</Button > </div > ) }const { increase, decrease, asyncIncrease } = countActionCreatorexport default connect ( (state: any ) => ({ count : state }), { increase, decrease, asyncIncrease })(Count ) as any
多组件共享状态(combineReducers) 在多个组件下,state就不能是个单一的值,就应该是一个对象,多组件步骤如下
store处使用combineReducers结合多个reducers
修改容器组件内的connect传递正常的对象(可以直接在connect中获取到所有数据)
创建Person组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { connect } from 'react-redux' import { Button } from 'antd' import { addPerson } from '../../redux/action/personAction' import { useRef, MutableRefObject } from 'react' const Index = (props: any ) => { const { addPerson, person, allAttributes } = props console .log (allAttributes) const name : MutableRefObject <any > = useRef (null ) const age : MutableRefObject <any > = useRef (null ) const addBtn = ( ) => { addPerson ({ name : name.current .value , age : age.current .value }) } return ( <> <hr /> <h1 > 这是人 </h1 > <input type ="text" ref ={name} placeholder ="姓名" /> <input type ="text" ref ={age} placeholder ="年龄" /> <Button onClick ={addBtn} > 添加对象</Button > <ul > { person.map((v: any) => ( <li key ={v.id} > {v.name} --- {v.age}</li > )) } </ul > </> ) }export default connect ((state: any ) => ({ person : state.person , allAttributes : state }), { addPerson })(Index )
将store组合成一个对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { configureStore, applyMiddleware, combineReducers } from '@reduxjs/toolkit' import count from './reducers/count' import person from './reducers/person' import thunk from 'redux-thunk' const allRedux = combineReducers ({ count, person })const store = configureStore ({ reducer : allRedux, enhancers : [applyMiddleware (thunk)] })export default store
Person 的 reducer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { IAction } from '../../typeFile' import { nanoid } from '@reduxjs/toolkit' const handler : { [k : string ]: (data: any , state: any ) => any } = { addPerson : (state, data ) => [{ id : nanoid (), ...data }, ...state] }export default (state : any = [], action : IAction <any >) => { const { type , data } = action if (handler[type ]) { return handler[type ](state, data) } return state }
在reducer中的state属性是单个组件的
总结:
使用@reduxjs/toolkit创建store文件(下载redux-thunk实现中间件异步,多组件需要使用combineReducers组合,将最后的组合对象交给store)
创建reducer处理文件,接收state和action({type: xxx, data: xxx}),暴露一个函数,返回内容就是该组件使用的state,负责具体任务的执行(调用使用dispatch(action))
创建构建action的封装文件(actionCreator),使用常量封装type类型,以防调用错误,至此redux构建结束,开始使用react-redux分割
将普通的组件看作UI组件,不能出现关于redux的api,然后使用connect对UI组件进行包装(connect为高阶函数,第一个方法第一个为参数为暴露的state,第二个为action内容对象或方法对象,第二个方法传入UI组件)
从外面传入store对象,可以使用Provider标签自动传递
在UI组件中使用props接收(函数式组件直接写第一个参数接收)
reducer返回对象注意点: 在reducer中,返回的对象地址不可相同,如向数组中添加一个元素后返回这个数组,不可直接使用push或者unshift添加,必须要返回一个深拷贝的数组
错误 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { IAction } from '../../typeFile' import { nanoid } from '@reduxjs/toolkit' const handler : { [k : string ]: (data: any , state: any ) => any } = { addPerson : (state, data ) => { state.unshift ({ id : nanoid (), name : data.name , age : data.age }) return state } }export default (state : any = [], action : IAction <any >) => { const { type , data } = action if (handler[type ]) { return handler[type ](state, data) } return state }
正确 1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { IAction } from '../../typeFile' import { nanoid } from '@reduxjs/toolkit' const handler : { [k : string ]: (data: any , state: any ) => any } = { addPerson : (state, data ) => [{ id : nanoid (), ...data }, ...state] }export default (state : any = [], action : IAction <any >) => { const { type , data } = action if (handler[type ]) { return handler[type ](state, data) } return state }