likes
comments
collection
share

React项目中使用Redux,Redux-Toolkit

作者站长头像
站长
· 阅读数 48

前言

React项目中使用Redux(传统写法)

安装核心包

我们在React项目中使用redux需要安装下面几个核心包:

  1. redux:redux的核心包
  2. react-redux:React项目中使用redux的核心包,可以使redux的状态在组件中使用,修改等。
  3. redux-thunk:redux常用中间件,可以使action creator返回一个函数
npm i redux react-redux redux-thunk

创建文件夹结构

我们在项目中使用redux,首先应该创建一个redux的文件夹,这里存放我们所有redux相关的文件,下面是我个人总结的redux文件目录。注:每个人都有自己的编程习惯,每个公司也都有各自的编程要求,因此这里的目录结构仅供参开,如何封装可根据自己和公司的习惯修改,合理即可。React项目中使用Redux,Redux-Toolkit文件目录介绍:

  1. states文件夹:用于存放我们所有的全局状态,每个文件就是一个状态,里面包含该状态的所有内容
  2. rootReducer.ts:用于结合所有状态的reducer函数,并生成一个总的reducer
  3. stateType:用于定义整个状态集合的类型,这里引入每个状态的类型并整合成一个对象
  4. 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
}

文件解析:

  1. 定义state类型:定义单个state类型,用来导出最后合成一个完整的状态集合的类型
  2. 定义action类型:定义action类型,用于定义reducer函数时给参数声明类型
  3. 定义action type常量:定义action常量的写法是一种redux的标准写法。定义常量可以防止拼写错误和重复定义,有利于代码维护,当我们想要修改某个action type的字符串是,只需要修改常量一处就可以,不需要多处修改字符串。
  4. 定义action creator:定义action creator函数也是redux的标准写法,这样可以避免使用dispatch派发某个action时,重复书写action对象。并且用action creator函数还可以处理一些异步action的场景,后续会介绍。
  5. 定义reducer:定义reducer函数,用于根据不同action计算生成最新的状态值,每个state核心的函数
  6. 导出函数和类型:导出其他文件需要用的函数和类型,供其他文件使用

生成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

文件解析:

  1. 引入redux核心apicreateStore, applyMiddleware
  2. 引入常用中间件redux-thunk,该中间件可以让你的action creator函数可以返回一个函数,而不是直接返回一个action对象。在返回的函数中可以进行异步操作,从而实现异步修改状态,例如我们模拟异步修改counter值,就可以这样编写action creator函数。
const decrementCountAction = (payload: number): any => {
    return (dispatch: Dispatch) => {
        setTimeout(() => {
            dispatch({ type: decrementCount, payload })
        }, 1000)
    }
}
  1. 引入rootReducer函数
  2. 创建store并导出

定义整体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

代码解析:

  1. 我们派发action时,需要从react-redux导入核心方法useDispatch,该hook会返回一个方法,就是我们创建store中的核心方法dispatch。
  2. 导入需要派发的action creator方法,action creator方法接收一个参数,该参数会结合方法中定义的action type返回一个具体的action对象,用于传入dispatch函数中,进行派发。
  3. 创建四个测试按钮,当点击按钮时,派发对应的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

文件解析:

  1. 想要使用redux中的状态,首先我们需要从react-redux中引入两个核心方法useSelector, shallowEqual
  2. useSelector, shallowEqual详解:``useSelector方法用来返回过滤后的对象,该方法接收两个参数,第一个参数是必传参数,是一个selector方法。第二个参数是shallowEqual,这个函数用于对象浅比较,其作用就是当我们组件引用的state没有发生改变时,不渲染当前组件。因为我们知道,我们每次派发action时,所有的state的reducer函数都会执行,当没有匹配的action时,那些对象类型的state就会返回一个和之前值相等的新的对象引用。若不使用shallowEqual,就会导致所有引用redux对象状态的组件都渲染,造成不必要的浪费。所以这里使用shallowEqual只进行浅比较,不比较对象引用,只有当对象值发生改变时才会重新渲染组件。(可以在组件中打印console.log('Demo03 render')和console.log('Demo02 render')进行测试,测试使用shallowEqual和不使用的打印情况,这里我就不做测试了。
  3. 定义StoreSelector类型,用于断言storeSelector方法和useSelector返回值的类型,这里这样断言是因为我们能确定返回值类型,但是ts没有推断出来
  4. 定义storeSelector方法是一个过滤state的方法,我们在一个组件中不可能使用所有的redux状态,因此我们用storeSelector方法取出我们所需要的部分就可以了。该方法接收一个参数是整体state的对象集合,然后返回一个对象,对象中包含我们需要的state。我们需要将该方法传入useSelector中获取我们所需要的状态。
  5. 在dom中使用状态

代码测试

上面代码不是伪代码,可以正常执行,在项目中运行后会得到如下界面,为了区分不同组件我加了不同背景色。当我们点击每个按钮时,就会发现引用对应状态的组件进行会响应式的变化。React项目中使用Redux,Redux-Toolkit

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单独使用createSlicecreateAction搭配createSlicecreateAsyncThunk搭配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
}

代码解析:

  1. 前面定义类型和初始值和之前作用一致,不多作介绍
  2. 使用createSlice定义一个状态切片,createSlice接收三个必选参数,name,initialState,reducers,一个可选参数extraReducerscreateSlice返回的slice对象中包含自动生成的action函数和reducer函数,我们可以从slice对象中将action函数和reducer函数取出,使用方式和之前的action函数和reducer函数相同。
  3. 注意点:这里使用reducers写reducer函数时不需要写默认匹配,这里会自动生成一个默认匹配并返回原先的值
  4. 导出与上述相同

写法三、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
}

代码解析:

  1. 前面定义类型和初始值和之前作用一致,不多作介绍
  2. 使用createAction定义action函数与写法一相同
  3. 当我们在外部定义action函数时,就不需要使用reducers去定义了。**(当然也可以搭配使用,不过这里如果都是同步action不推荐混用,会让代码可读性降低,逻辑不清晰。如果存在异步action可以混用的,后续会介绍)**我们直接使用extraReducers引入外部的action,extraReducers写法与createReducer第二个参数的写法一致,也有两种,对象和回调函数形式,这里依旧推荐使用回调函数形式,原因相同。
  4. 这里使用createAction定义action函数就不需要从slice取出action了,当然也取不出来,因为没定义reducers,只取出reducer就好
  5. 同样的导出

写法四、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
评论
请登录