React项目中使用Redux,Redux-Toolkit
前言
React项目中使用Redux(传统写法)
安装核心包
我们在React项目中使用redux需要安装下面几个核心包:
redux
:redux的核心包react-redux
:React项目中使用redux的核心包,可以使redux的状态在组件中使用,修改等。redux-thunk
:redux常用中间件,可以使action creator返回一个函数
npm i redux react-redux redux-thunk
创建文件夹结构
我们在项目中使用redux,首先应该创建一个redux的文件夹,这里存放我们所有redux相关的文件,下面是我个人总结的redux文件目录。注:每个人都有自己的编程习惯,每个公司也都有各自的编程要求,因此这里的目录结构仅供参开,如何封装可根据自己和公司的习惯修改,合理即可。
文件目录介绍:
states文件夹:
用于存放我们所有的全局状态,每个文件就是一个状态,里面包含该状态的所有内容rootReducer.ts:
用于结合所有状态的reducer函数,并生成一个总的reducerstateType:
用于定义整个状态集合的类型,这里引入每个状态的类型并整合成一个对象store.ts:
redux的核心文件,用于生成store
编写state文件内容
我们这里用两个状态作为案例,一个是counter,一个是userInfo,下面是两个文件完整代码,代码后有各个代码模块的解释。counter.ts
// 定义state类型
type CounterType = number
// 定义action类型
type CounterActionType = {
type: string,
payload: number
}
// 定义action type常量
const incrementCount = 'INCREMENT_COUNT'
const decrementCount = 'DECREMENT_COUNT'
// 定义action creator
const incrementCountAction = (payload: number) => {
return { type: incrementCount, payload }
}
const decrementCountAction = (payload: number) => {
return { type: decrementCount, payload }
}
// 定义reducer
const countReducer = (state = 0, action: CounterActionType) => {
const { type, payload } = action
switch (type) {
case incrementCount:
return state + payload
case decrementCount:
return state - payload
default:
return state
}
}
export {
incrementCountAction,
decrementCountAction,
countReducer,
CounterType
}
userInfo.ts
// 定义state类型
type UserInfoType = {
name: string,
age: number
}
// 定义action类型
type UserInfoActionType = {
type: string,
payload: string | number
}
// 定义action type常量
const changeUserName = 'CHANGE_USER_NAME'
const changeUserAge = 'CHANGE_USER_AGE'
// 定义action creator
const changeUserNameAction = (payload: string | number) => {
return { type: changeUserName, payload }
}
const changeUserAgeAction = (payload: string | number) => {
return { type: changeUserAge, payload }
}
// 定义reducer
const userInfoReducer = (state = { name: 'jack', age: 18 }, action: UserInfoActionType) => {
const { type, payload } = action
switch (type) {
case changeUserName:
return { ...state, name: payload }
case changeUserAge:
return { ...state, age: payload }
default:
return state
}
}
export {
changeUserNameAction,
changeUserAgeAction,
userInfoReducer,
UserInfoType
}
文件解析:
定义state类型:
定义单个state类型,用来导出最后合成一个完整的状态集合的类型定义action类型:
定义action类型,用于定义reducer函数时给参数声明类型定义action type常量:
定义action常量的写法是一种redux的标准写法。定义常量可以防止拼写错误和重复定义,有利于代码维护,当我们想要修改某个action type的字符串是,只需要修改常量一处就可以,不需要多处修改字符串。定义action creator:
定义action creator函数也是redux的标准写法,这样可以避免使用dispatch派发某个action时,重复书写action对象。并且用action creator函数还可以处理一些异步action的场景,后续会介绍。定义reducer:
定义reducer函数,用于根据不同action计算生成最新的状态值,每个state核心的函数导出函数和类型:
导出其他文件需要用的函数和类型,供其他文件使用
生成rootReducer
当我们有多个状态时,我们需要将所有状态的reducer函数整合成一个根reducer,整合方法用的是redux提供的combineReducers
。我们创建一个单独的文件用于整合所有reducer函数,该文件中需要引入所有state的reducer函数和redux的combineReducers方法
,最后导出rootReducer,在创建store的时候需要用到。具体内容如下rootReducer.ts
import { combineReducers } from 'redux'
import { userInfoReducer } from './states/userInfo'
import { countReducer } from './states/counter'
const rootReducer = combineReducers({
userInfo: userInfoReducer,
counter: countReducer
})
export default rootReducer
创建store
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from './rootReducer'
const store = createStore(rootReducer, applyMiddleware(thunkMiddleware))
export default store
文件解析:
- 引入redux核心api
createStore, applyMiddleware
- 引入常用中间件
redux-thunk
,该中间件可以让你的action creator函数可以返回一个函数,而不是直接返回一个action对象。在返回的函数中可以进行异步操作,从而实现异步修改状态,例如我们模拟异步修改counter值,就可以这样编写action creator函数。
const decrementCountAction = (payload: number): any => {
return (dispatch: Dispatch) => {
setTimeout(() => {
dispatch({ type: decrementCount, payload })
}, 1000)
}
}
定义整体state类型
因为我们是用ts开发项目,在我们创建出store后,store中的getState
方法将返回一个state对象,该对象包含了所有的state,因此我们需要给这个整体的state对象定义类型,实际就是综合每个state的类型,具体代码如下:我们从每个状态文件获取单个状态类型,整合后导出。
import { CounterType } from './states/counter'
import { UserInfoType } from './states/userInfo'
export type StateType = {
counter: CounterType,
userInfo: UserInfoType
}
根组件APP文件中注入store
想要项目中每个组件都能拿到全局状态,我们需要在跟组件处注入我们的store。这里就需要用到react-redux
提供的核心组件Provider
,我们使用Provider
组件包裹APP.tsx文件中的元素,实现store注入。其中的Demo01,Demo02,Demo03
是我们的测试组件,后续会用
import React from 'react'
import { Provider } from 'react-redux'
import store from '@redux/store'
import Demo01 from '@pages/reduxDemo/demo01'
import Demo02 from '@pages/reduxDemo/demo02'
import Demo03 from '@pages/reduxDemo/demo03'
function ReduxApp() {
return (
<Provider store={store}>
<div>
<h1>React Redux App</h1>
<Demo01 />
<Demo02 />
<Demo03 />
</div>
</Provider>
)
}
export default ReduxApp
组件中使用状态、派发action
下面我们使用三个测试组件介绍在组件中如何使用状态和派发action。其中Demo01
用于派发action修改状态。Demo02,Demo03
分别用来使用counter,和userInfo两个状态。Demo01.tsx
import React from 'react'
import { useDispatch } from 'react-redux'
import { incrementCountAction, decrementCountAction } from '@src/redux/states/counter'
import { changeUserNameAction, changeUserAgeAction } from '@src/redux/states/userInfo'
function Demo01() {
const dispatch = useDispatch()
return (
<div style={{ backgroundColor: 'pink' }}>
<h1>This is Demo01</h1>
<button onClick={() => dispatch(incrementCountAction(3))}>AddCount</button>
<button onClick={() => dispatch(decrementCountAction(2))}>SubCount</button>
<button onClick={() => dispatch(changeUserNameAction('Tom'))}>ChangeUserName</button>
<button onClick={() => dispatch(changeUserAgeAction(20))}>ChangeUserAge</button>
</div>
)
}
export default Demo01
代码解析:
- 我们派发action时,需要从
react-redux
导入核心方法useDispatch
,该hook会返回一个方法,就是我们创建store中的核心方法dispatch。 - 导入需要派发的
action creator
方法,action creator
方法接收一个参数,该参数会结合方法中定义的action type返回一个具体的action对象,用于传入dispatch函数中,进行派发。 - 创建四个测试按钮,当点击按钮时,派发对应的action,修改对应的状态
Demo02.tsx,Demo03.tsx
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import { StateType } from '@redux/stateType'
type StoreSelector = {
counter: number
}
function Demo02() {
const storeSelector = (state: StateType) => ({
counter: state.counter
}) as StoreSelector
const { counter } = useSelector(storeSelector, shallowEqual) as StoreSelector
return (
<div style={{ backgroundColor: 'yellow' }}>
<h1>This is Demo02 Counter: {counter}</h1>
</div>
)
}
export default Demo02
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'
import { StateType } from '@redux/stateType'
type StoreSelector = {
userInfo: {
name: string,
age: number
}
}
function Demo03() {
const storeSelector = (state: StateType) => ({
userInfo: state.userInfo
}) as StoreSelector
const { userInfo } = useSelector(storeSelector, shallowEqual) as StoreSelector
return (
<div style={{ backgroundColor: 'green' }}>
<h1>This is Demo03 UserName: {userInfo.name} UserAge: {userInfo.age}</h1>
</div>
)
}
export default Demo03
文件解析:
- 想要使用redux中的状态,首先我们需要从
react-redux
中引入两个核心方法useSelector, shallowEqual
useSelector, shallowEqual详解:``useSelector
方法用来返回过滤后的对象,该方法接收两个参数,第一个参数是必传参数,是一个selector
方法。第二个参数是shallowEqual
,这个函数用于对象浅比较,其作用就是当我们组件引用的state没有发生改变时,不渲染当前组件。因为我们知道,我们每次派发action时,所有的state的reducer函数都会执行,当没有匹配的action时,那些对象类型的state就会返回一个和之前值相等的新的对象引用。若不使用shallowEqual
,就会导致所有引用redux对象状态的组件都渲染,造成不必要的浪费。所以这里使用shallowEqual
只进行浅比较,不比较对象引用,只有当对象值发生改变时才会重新渲染组件。(可以在组件中打印console.log('Demo03 render')和console.log('Demo02 render')进行测试,测试使用shallowEqual和不使用的打印情况,这里我就不做测试了。
)- 定义
StoreSelector
类型,用于断言storeSelector
方法和useSelector
返回值的类型,这里这样断言是因为我们能确定返回值类型,但是ts没有推断出来 - 定义
storeSelector
方法是一个过滤state的方法,我们在一个组件中不可能使用所有的redux状态,因此我们用storeSelector
方法取出我们所需要的部分就可以了。该方法接收一个参数是整体state的对象集合,然后返回一个对象,对象中包含我们需要的state。我们需要将该方法传入useSelector
中获取我们所需要的状态。 - 在dom中使用状态
代码测试
上面代码不是伪代码,可以正常执行,在项目中运行后会得到如下界面,为了区分不同组件我加了不同背景色。当我们点击每个按钮时,就会发现引用对应状态的组件进行会响应式的变化。
React项目中使用Redux(Redux-Toolkit写法)
安装核心包
我们使用Redux-Toolkit写法时,需要安装一个核心包@reduxjs/toolkit
,这个包里集成了redux
核心包和redux-thunk
包,因此我们可以安装@reduxjs/toolkit
包,并卸载redux,redux-thunk
包。
npm uninstall redux redux-thunk
npm i @reduxjs/toolkit
创建文件夹结构
文件夹结构和传统写法一致,只是内容有些区别,这里不再次介绍。
编写state文件内容
这两种写法的核心区别就在这里,我们下面看一下counter和userInfo状态文件使用Redux-Toolkit的写法。在Redux-Toolkit中state文件的写法也有多种,分别是createAction搭配createReducer
,单独使用createSlice
,createAction搭配createSlice
,createAsyncThunk搭配createSlice
下面就用userInfo这个状态为例,介绍这四种写法,项目中可以根据个人习惯和公司要求选择。也可以根据不同state灵活运用。
写法一、createAction搭配createReducer
import { createAction, createReducer } from '@reduxjs/toolkit'
// 定义state类型
type UserInfoType = {
name: string,
age: number
}
// 定义action类型
type UserInfoActionType = {
type: string,
payload: string | number
}
// 定义初始state
const initialState = {
name: '张三',
age: 18
}
// 定义userInfo action
const changeUserNameAction = createAction<string>('CHANGE_USER_NAME')
const changeUserAgeAction = createAction<number>('CHANGE_USER_AGE')
// 定义userInfo reducer
const userInfoReducer = createReducer(initialState, (builder) => {
builder
.addCase(changeUserNameAction, (state: UserInfoType, action: UserInfoActionType) => {
state.name = action.payload as string
})
.addCase(changeUserAgeAction, (state: UserInfoType, action: UserInfoActionType) => {
state.age = action.payload as number
})
.addMatcher((action: UserInfoActionType) => {
return action.type.endsWith('FULFILLED')
}, (state: UserInfoType, action: UserInfoActionType) => {
console.log('action', action)
})
.addDefaultCase((state: UserInfoType, action: UserInfoActionType) => {
console.log('action', action)
})
})
export {
changeUserNameAction,
changeUserAgeAction,
userInfoReducer,
UserInfoType
}
代码解析:
写法二、单独使用createSlice
import { createSlice } from '@reduxjs/toolkit'
// 定义state类型
type UserInfoType = {
name: string,
age: number
}
// 定义action类型
type UserInfoActionType = {
type: string,
payload: string | number
}
// 定义初始state
const initialState = {
name: '张三',
age: 18
}
// 定义userInfo slice
const userInfoSlice = createSlice({
name: 'userInfo',
initialState,
reducers: {
changeUserNameAction(state: UserInfoType, action: UserInfoActionType) {
state.name = action.payload as string
},
changeUserAgeAction(state: UserInfoType, action: UserInfoActionType) {
state.age = action.payload as number
}
}
})
// 取出action creator
const { changeUserNameAction, changeUserAgeAction } = userInfoSlice.actions
// 取出reducer
const userInfoReducer = userInfoSlice.reducer
export {
changeUserNameAction,
changeUserAgeAction,
userInfoReducer,
UserInfoType
}
代码解析:
- 前面定义类型和初始值和之前作用一致,不多作介绍
- 使用
createSlice
定义一个状态切片,createSlice
接收三个必选参数,name,initialState,reducers
,一个可选参数extraReducers
。createSlice
返回的slice对象中包含自动生成的action函数和reducer函数,我们可以从slice对象中将action函数和reducer函数取出,使用方式和之前的action函数和reducer函数相同。 注意点:这里使用reducers写reducer函数时不需要写默认匹配,这里会自动生成一个默认匹配并返回原先的值
- 导出与上述相同
写法三、createAction搭配createSlice
import { createAction, createSlice } from '@reduxjs/toolkit'
// 定义state类型
type UserInfoType = {
name: string,
age: number
}
// 定义action类型
type UserInfoActionType = {
type: string,
payload: string | number
}
// 定义初始state
const initialState = {
name: '张三',
age: 18
}
// 定义userInfo action
const changeUserNameAction = createAction<string>('CHANGE_USER_NAME')
const changeUserAgeAction = createAction<number>('CHANGE_USER_AGE')
// 定义userInfo slice
const userInfoSlice = createSlice({
name: 'userInfo',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(changeUserNameAction, (state: UserInfoType, action: UserInfoActionType) => {
state.name = action.payload as string
})
.addCase(changeUserAgeAction, (state: UserInfoType, action: UserInfoActionType) => {
state.age = action.payload as number
})
.addMatcher((action: UserInfoActionType) => {
return action.type.endsWith('fulfilled')
}, (state: UserInfoType, action: UserInfoActionType) => {
console.log('extraReducers matcher', action)
})
.addDefaultCase((state: UserInfoType, action: UserInfoActionType) => {
console.log('extraReducers default', action)
})
}
})
// 取出reducer
const userInfoReducer = userInfoSlice.reducer
export {
changeUserNameAction,
changeUserAgeAction,
userInfoReducer,
UserInfoType
}
代码解析:
- 前面定义类型和初始值和之前作用一致,不多作介绍
- 使用
createAction
定义action函数与写法一相同 - 当我们在外部定义action函数时,就不需要使用reducers去定义了。**(当然也可以搭配使用,不过这里如果都是同步action不推荐混用,会让代码可读性降低,逻辑不清晰。如果存在异步action可以混用的,后续会介绍)**我们直接使用
extraReducers
引入外部的action,extraReducers
写法与createReducer
第二个参数的写法一致,也有两种,对象和回调函数形式,这里依旧推荐使用回调函数形式,原因相同。 - 这里使用
createAction
定义action函数就不需要从slice取出action了,当然也取不出来,因为没定义reducers,只取出reducer就好 - 同样的导出
写法四、createAsyncThunk搭配createSlice
实际上createAsyncThunk搭配createSlice
的写法与createAction搭配createSlice
类似,只是createAsyncThunk
生成的是异步action
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
// 定义state类型
type UserInfoType = {
name: string,
age: number
}
// 定义action类型
type UserInfoActionType = {
type: string,
payload: string | number
}
// 定义初始state
const initialState = {
name: '张三',
age: 18
}
// 模拟异步请求
const simulateApiRequest = (value: number, delay: number) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value)
}, delay)
})
}
// 定义异步action
const asyncChangeUserAgeAction = createAsyncThunk(
'userInfo/asyncChangeUserAgeAction',
async (value: number) => {
const result = await simulateApiRequest(value, 1000)
return result
}
)
// 定义userInfo slice
const userInfoSlice = createSlice({
name: 'userInfo',
initialState,
reducers: {
changeUserNameAction(state: UserInfoType, action: UserInfoActionType) {
state.name = action.payload as string
},
changeUserAgeAction(state: UserInfoType, action: UserInfoActionType) {
state.age = action.payload as number
}
},
extraReducers: (builder) => {
builder
.addCase(asyncChangeUserAgeAction.fulfilled, (state, action) => {
state.age = action.payload as number
})
.addCase(asyncChangeUserAgeAction.pending, (state, action) => {
console.log(action.meta)
console.log(state)
console.log('pending')
})
.addCase(asyncChangeUserAgeAction.rejected, (state, action) => {
console.log(state)
console.log(action.error)
})
}
})
// 取出action creator
const { changeUserNameAction, changeUserAgeAction } = userInfoSlice.actions
// 取出reducer
const userInfoReducer = userInfoSlice.reducer
export {
changeUserNameAction,
changeUserAgeAction,
asyncChangeUserAgeAction,
userInfoReducer,
UserInfoType
}
代码解析:
写法总结
其实除了这四种写法外还有createAsyncThunk结合createReducer
或者综合各种写法,总之我们需要根据具体的state值和项目逻辑,个人习惯等择优选择。
生成rootReducer
在Redux-Toolkit中不需要使用combineReducers
,只需要生成一个对象就好,对象的key就是后面state对应的key
import { userInfoReducer } from './states/userInfo'
import { countReducer } from './states/counter'
const rootReducer = {
userInfo: userInfoReducer,
counter: countReducer
}
export default rootReducer
创建store
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer
})
export default store
定义整体state类型
总结
后续的注入store和使用状态,修改状态与之前写法完全一致,就不重复介绍,Redux-Toolkit
与传统redux的区别主要是在定义action函数和reducer函数时有新的集成性方法,可以使我们的代码更清晰,可读性更高,至此所有的Redux和Redux-Toolkit
在React项目中的使用方法都介绍完了,感觉不错的可以点赞关注,个人中心还有其他关于React教程,欢迎查阅。
转载自:https://juejin.cn/post/7265666141549035555