Redux从简单到进阶(拥抱redux-toolkit)

lxf2023-04-22 07:36:01

上一次研究 react+redux,已经是 3 年前了,感觉也应该更新一下自己的 redux 知识库了。当时是结合自己当时学习的内容,直接在项目中引用 redux,并未记录 redux 的学习过程,正好现在升级 redux,再来一篇关于 Redux 的学习记录吧。

附上之前的研究 link: 里面有我自己对 redux 使用的一个封装过程:>
React 性能优化 之 React + Redux + immutable 最佳实践(避免重复渲染)

因为官网的例子我觉得代码有点太多,所以我就自己按User的增删查改来实现的。大家在看的时候,最好也将这个例子敲一遍。这样,在选取使用那个tool的时候,才得心应手。

1. 什么是 redux

这里使用一个最基础的 redux 例子,就不再重新编写例子了。redux 基础例子。 他的数据流使用官网的一个图片是理解整个处理流程:
Redux从简单到进阶(拥抱redux-toolkit)

1.1 利用 Redux Toolkit 简化 redux 管理

上面的 redux 請求,整个流程设计 action, reducer, store, 再到 UI 的重回,不管页面做什么一个小的改变,我们都需要手动地去派发 action,使得 reducer 更新 store 中的数据。redux 现在提供了一个 toolkit 工具包,可以让我们的步骤进行简化,接下来我们使用它来做一些小例子。

2. toolkit 基础使用

2.1 createSlice 基础使用

createSlice 允许我们提供一个带 reducer 函数的对象,它将根据我们列出的reducer,自动生成action type字符串action creator 函数

const initialState: Query = {
  resourceKey: '',
  limit: 20,
  pageKey: '1',
}

export const querySlice = createSlice({
  name: 'query',
  initialState,

  // 我们提供的 reducer 函数
  reducers: {
    updatePageKey: (state, action) => {
      console.log(action)
      state.pageKey = action.payload
    },
    updateResourceKey: (state, action) => {
      state.resourceKey = action.payload
    },
  },
})

// 自动导出action type 字符串
export const { updatePageKey, updateResourceKey } = querySlice.actions

export const selectQuery = (state) => state.query

// 生成的reducer函数
export default querySlice.reducer

2.2 configureStore

与 redux 最初的 createStore 一样,创建 redux store 实例

import { configureStore } from '@reduxjs/toolkit'
import { Query } from './model/query'
import queryReducer from './slice/query'

const reducer = {
  reducer: {
    query: queryReducer,
  },
}
export default configureStore(reducer)

2.3 在 class 组件中的使用

在 class 组件中使用,我们无法使用钩子函数。我们需要用到connect高阶组件。
connect组件的第一个参数是mapStateToProps,它将从 Store 中选择组件需要的数据。Store 中的 state 每次改变都会调用mapStateToProps,mapStateToProps接受整个 store,并根据情况返回组件需要的数据。
connect第二个参数如果没有指定,那么组件将默认为 dispatch对象。在组件中使用,则应该是dispatch({ type: 'INCREMENT' })这种方式,将操作分派到 store。我们可以提供mapDispatchToProps参数,用来建立 UI 组件的参数到 store.dispatch 方法的映射。定义了哪些用户的操作应该当作 Action 传递给 store。

对这两个参数有疑问的可以参考阮一峰日志mapDispatchToProps

import React from 'react';
import { connect } from 'react-redux';
import { updatePageKey, updateResourceKey } from '@store/slice/query';
import { State } from '@store/index';
import { Query } from '@store/model/query';

interface MainPageProps {
	updatePageKey: (value: string) => void;
	query: Query;
  }

class MainPage extends React.PureComponent<MainPageProps> {
	constructor (props) {
		super(props);
	}
	clickUpdatePageKey = () => {
		this.props.updatePageKey('10');
	}
	render () {
		return (
  			<button onClick={this.clickUpdatePageKey}>click me {this.props.query.pageKey}</button>
		)
	}
}

const mapStateToProps = (state: State) => ({
	query: state.query,
});

export default connect(mapStateToProps, {
	updatePageKey,
	updateResourceKey,
})(MainPage as any);

2.4 在 function 组件中的使用

虽然上面写了 class 相关的实例,但是其实官网上写的例子,都是使用的 functio 组件。编写方式如下:

import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { selectQuery, updatePageKey } from '@store/slice/query'
import { Query } from '@store/model/query'

const MainPage = () => {
  const query: Query = useSelector(selectQuery)
  const dispath = useDispatch()

  const clickUpdatePageKey = () => {
    dispath(updatePageKey(11))
  }

  return <button onClick={clickUpdatePageKey}>click me {query.pageKey}</button>
}
export default React.memo(MainPage)

3. toolkit 同步新增修改删除

该例子是本地 list 数组对象的新增,删除,修改和查询的例子。

3. 1users 数组 slice 新增

这里面的基础操作其实和上面的例子是一样的,但是增加了常见的业务类型:新增、删除、修改和查询。
但是这里引入了一个新的功能:prepare。是对 reducer 执行前的一个准备操作。它可以接受多个参数,应该返回一个包含 payload 字段的对象。例如这里,一般在页面上新增,不会有id的填写,在prepare函数中,预处理该字段。

还有一个点需要注意:在delete操作中,必须返回新的state对象。不然state不会更新。

import { createSlice } from '@reduxjs/toolkit'
import User from '@store/model/user'

const initialState: Array<User> = [
  {
    id: '1',
    extId: 'extId',
    name: 'zhaoyezi',
    type: '1type',
    email: 'zhao.yehzi@qq.com',
    profileImageUrl: 'http://dd.png',
  },
  {
    id: '2',
    extId: 'extId',
    name: 'zhaoyezi',
    type: '1type',
    email: 'zhao.yehzi@qq.com',
    profileImageUrl: 'http://dd.png',
  },
  {
    id: '3',
    extId: 'extId',
    name: 'zhaoyezi',
    type: '1type',
    email: 'zhao.yehzi@qq.com',
    profileImageUrl: 'http://dd.png',
  },
]
export const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    getUsers: (state) => {
      return state
    },
    addUser: {
      reducer(state, action) {
        state.push(action.payload)
      },
      prepare({ name, type, email, profileImageUrl }) {
        return {
          payload: {
            id: `${Math.random()}`.replace('0.', ''),
            extId: `${Math.random()}`.replace('0.', ''),
            name,
            type,
            email,
            profileImageUrl,
          },
        }
      },
    },
    updateUser: (state, action) => {
      const { id, name, type, email, profileImageUrl } = action.payload
      const existingItem = state.find((item) => item.id === id)
      if (existingItem) {
        existingItem.name = name
        existingItem.type = type
        existingItem.email = email
        existingItem.profileImageUrl = profileImageUrl
      }
    },
    deleteUser: (state, action) => {
      state = state.filter((item) => {
        return item.id !== action.payload.id
      })

      // 注意返回state,不然不会更新
      return state
    },
  },
})

export const { addUser, getUsers, deleteUser, updateUser } = usersSlice.actions
export const selectUsers = (state) => state.users
export default usersSlice.reducer

3.2 userslice 添加到 store

当写好一个新的 slice,不要忘记添加到 store 中,不然该数据不会被管理哦。

const reducer = {
  reducer: {
    query: queryReducer,
    users: usersReducer,
  },
}
export default configureStore(reducer)

3.3 组件中 dispatch 修改

虽然样式简陋,不影响我们理解该业务,该组件中只是用了新增、删除、更新以及列表查询显示的基础函数。可以直观地看到如何更新 store 数据。

import User from '@store/model/user'
import {
  addUser,
  deleteUser,
  selectUsers,
  updateUser,
} from '@store/slice/users'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'

const ListOperate = () => {
  const users: User[] = useSelector(selectUsers)
  const dispath = useDispatch()

  const addUserEvent = () => {
    dispath(
      addUser({
        name: 'zhaoyehongnew',
        type: '2',
        email: 'dd@qq.com',
        profileImageUrl: 'profileImageUrl',
      }),
    )
  }

  const deleteUserEvent = () => {
    dispath(deleteUser({ id: users[1].id }))
  }

  const updateUserEvent = () => {
    dispath(updateUser({ ...users[0], name: 'zhaoyezi__change' }))
  }
  return (
    <div>
      {users.map((user) => {
        return (
          <div key={user.id} style={{ border: '1px solid' }}>
            <div>name: {user.name}</div>
            <ul>
              <li>id: {user.id}</li>
              <li>type: {user.type}</li>
              <li>email: {user.email}</li>
              <li>profileImageUrl: {user.profileImageUrl}</li>
            </ul>
          </div>
        )
      })}
      <button onClick={addUserEvent}> addUser</button>
      <button onClick={updateUserEvent}> updateUser</button>
      <button onClick={deleteUserEvent}> deleteUser</button>
    </div>
  )
}
export default React.memo(ListOperate)

4. 异步数据请求

开发中,我们最常用的 api 数据,是不能直接 dispatch 的,因为其中包含了 promise 异步相关的信息。异步 action,请点击这个异步 action 实例进行学习。

这里我们主要研究 使用 Redux Toolkit createAsyncThunk API 来简化异步调用。
Redux Store 对异步逻辑其实是没有感知的,它只懂得如何同步 dispatch action,调用 root reducer 函数更新 State,并通知 UI 数据发生了变化。任何的异步都必须发生在 Store 之外。

Redux middleware 的作用,可以起到 调用(检查)当前 store 的状态,使异步逻辑与 Store 发生交互。例如利用 middleware 处理以下类型(引用自官网):

  • dispatch action 时执行额外的逻辑(例如打印 action 的日志和状态)
  • 暂停、修改、延迟、替换或停止 dispatch 的 action
  • 编写可以访问 dispatch 和 getState 的额外代码
  • 教 dispatch 如何接受除普通 action 对象之外的其他值,例如函数和 promise,通过拦截它们并 dispatch 实际 action 对象来代替

最常见的异步 middleware 是redux-thunk,可以编写直接包含异步逻辑的普通函数。redux-toolkitconfigureStore默认设置了异步 thuks。提供了createAsyncThunk异步 API。以下是添加 thuk 的流程图:
Redux从简单到进阶(拥抱redux-toolkit)

4.1 createAsyncThunk 使用

一般 api 的请求会有几个状态:

  • 请求未开始
  • 请求进行中
  • 请求成功,返回我们需要的数据
  • 请求失败,可能有错误信息
    Redux Toolkit 的createAsyncThunk API 生成 thuk,会为你自动 dispath 那些 start|success|failure的 action。
  • 第一个参数:将用作生成的 action 类型的前缀的字符串
  • 第二个参数:一个 “payload creator” 回调函数,它应该返回一个包含一些数据的 Promise,或者一个被拒绝的带有错误的 Promise。

4.2 改进 userSlice

这里使用createSlice方法的extraReducers字段,接收名为builder的参数函数。
builder对象提供了方法,可以定义额外的case reducer。这些reducer将响应slice之外定义的action

  • builder.addCase(actionCreator, reducer) 处理异步 thunk dispatch 的每个 action。
  • createAsyncThunk 有 pending, fulfilled, failed 三种状态,可根据不同状态显示不同内容
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'
import request from '@server/base/request'
import User from '@store/model/user'

const initialState: UserInfo = {
  data: [],
  status: 'idle',
  error: '',
}

// 使用 Redux Toolkit createAsyncThunk API 来简化异步调用
export const getUserList = createAsyncThunk('/users/getlist', async () => {
  // eslint-disable-next-line new-cap
  const response = await request.get('/list')
  return response
})

export const addUser = createAsyncThunk('/user/add', async (user: User) => {
  const response = await request.post('/add', { id: 'new', ...user })
  return response
})
export const updateUser = createAsyncThunk(
  '/user/update',
  async (user: User) => {
    const response = await request.post('/update', { id: 'new', ...user })
    return response
  },
)
export const deleteUser = createAsyncThunk(
  '/user/delete',
  async (user: User) => {
    const response = await request.post('/delete', { id: 'new' })
    return response
  },
)

const userListSlice = createSlice({
  name: 'userInfo',
  initialState,
  reducers: {},
  extraReducers(builder) {
    builder

      // list user
      .addCase(getUserList.pending, (state) => {
        state.status = 'loading'
      })
      .addCase(getUserList.fulfilled, (state, action: PayloadAction<any>) => {
        state.status = 'succeeded'
        state.data = state.data.concat(action.payload)
      })
      .addCase(getUserList.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })

      .addCase(addUser.fulfilled, (state, action: any) => {
        state.status = 'succeeded'
        state.data.push({ ...action.meta.arg, id: 'new' })
      })
      .addCase(updateUser.fulfilled, (state, action: any) => {
        state.status = 'succeeded'
        state.data[2] = { ...action.meta.arg, id: 'new' }
      })
      .addCase(deleteUser.fulfilled, (state, action: any) => {
        state.status = 'succeeded'
        state.data = state.data.filter((item) => item.id !== action.meta.arg)
      })
  },
})

export const selectList = (state) => state.userInfo.data
export default userListSlice.reducer

4.3 configureStore 引入异步内容

const reducer = {
  reducer: {
    query: queryReducer,
    users: usersReducer,
    userInfo: userListReducer,
  },
}
export default configureStore(reducer)

4.4 组件内对象的增删查看

与上面的 User 管理相比较,显示并没有任何的改变,只是在 dispatch 的时候,对象是createAsyncThunk产生的 Action,其他无任何区别

import { AnyAction } from '@reduxjs/toolkit';
import { State } from '@store/index';
import User from '@store/model/user';
import { addUser, deleteUser, getUserList, selectList, updateUser } from '@store/slice/userapi';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';

const ListOperate = () => {
	const userList: User[] = useSelector(selectList);
	const status = useSelector((state: State) => state.userInfo.status);
	const dispatch = useDispatch();

	useEffect(() => {
		if (status === 'idle') {
			dispatch(getUserList());
		}
	}, [status, dispatch])


	const addUserEvent = () => {
		dispatch(addUser({ name: 'zhaoyehongnew', type: '2', email: 'dd@qq.com', profileImageUrl: 'profileImageUrl' }) as AnyAction);
	}

		const updateUserEvent = () => {
			dispatch(updateUser({ ...userList[2], name: 'zhaoyezi__change' }));
	}

	const deleteUserEvent = () => {
		dispatch(deleteUser(userList[2].id));
	}


	return (
		<div>
			{ userList.map((user, key) => {
				return (
					<div key={ key } style={ { border: '1px solid' } }>
						<div>name: { user.name }</div>
						<ul>
							<li>id: { user.id }</li>
							<li>type: { user.type }</li>
							<li>email: { user.email }</li>
							<li>profileImageUrl: { user.profileImageUrl }</li>
						</ul>
					</div>
				)
			})
			}
			<button onClick={ addUserEvent }> addUser</button>
			<button onClick={ updateUserEvent }> updateUser</button>
			<button onClick={ deleteUserEvent }> deleteUser</button>
		</div>
	)
}
export default React.memo(ListOperate);

可以在控制台,看到 dispatch 的 action 信息:
Redux从简单到进阶(拥抱redux-toolkit)

5. 性能优化与数据范式化

5.1 组件实例

现在,我们基于之前的同步 user 列表显示组件,进行改进。我们为 user 新增一个标识 status, 0:离职,1:在职。
在组件中,通过使用了 useSelector钩子函数。filter 出两个不同类型的 user 数组。

5.1.1 userSelectorSlice

该组件与上面同步 userSlice 一样,但是新增 2 个过滤的方法。并在 filter 过滤是打印日志,表示取数值的时候,进入了该方法。当然不要忘记加入configureStore

import { createSlice } from '@reduxjs/toolkit'
import User from '@store/model/user'

const initialState: Array<User> = [
  {
    id: '1',
    extId: 'extId',
    name: 'zhaoyezi1',
    type: '1',
    email: 'zhao.yehong@qq.com',
    profileImageUrl: 'http://dd.png',
  },
  {
    id: '2',
    extId: 'extId',
    name: 'zhaoyezi2',
    type: '0',
    email: 'zhao.yehong@qq.com',
    profileImageUrl: 'http://dd.png',
  },
]
export const userSelectorSlice = createSlice({
  name: 'userSelector',
  initialState,
  reducers: {
    getUsers: (state) => {
      // ... 同上
    },
    addUser: {
      // ... 同上
    },
    deleteUser: (state, action) => {
      // ... 同上
    },
    updateUser: (state, action) => {
      // ... 同上
    },
  },
})
export const {
  addUser,
  getUsers,
  deleteUser,
  updateUser,
} = userSelectorSlice.actions

export const selectWorkUsers = (state) => {
  console.log('-----selectWorkUsers')
  return state.userSelector.filter((user) => user.type === '1')
}
export const selectLeaveUsers = (state) => {
  console.log('-----selectLeaveUsers')
  return state.userSelector.filter((user) => user.type === '0')
}
export default userSelectorSlice.reducer

5.1.2 组件渲染

增删改查入口。记得使用工作人员与离职人员的数据渲染。

我们这里根据需求,使用了离职人员组件在职人员组件,这里的增加删除与修改,是对在职人员添加一条信息,并修改与删除该条信息。并使用了React.memo提升了组件的性能。

并且,我们将 离职组件在职组件取值两次,渲染两次。

const ListOperate = () => {
  const dispath = useDispatch()
  const workUser: User[] = useSelector(selectWorkUsers)
  const workUser2: User[] = useSelector(selectWorkUsers)
  const leaveUser: User[] = useSelector(selectLeaveUsers)
  const leaveUser2: User[] = useSelector(selectLeaveUsers)

  const addUserEvent = () => {
    dispath(
      addUser({
        name: 'zhaoyezinew',
        type: '1',
        email: 'dd@qq.com',
        profileImageUrl: 'profileImageUrl',
      }),
    )
  }

  const deleteUserEvent = () => {
    dispath(deleteUser({ id: workUser[1].id }))
  }

  const updateUserEvent = () => {
    dispath(updateUser({ ...workUser[1], name: 'zhaoyezi__change' }))
  }
  return (
    <div>
      <div>-----Worker-----</div>
      <Works worksUser={workUser} />
      <div>-----leave</div>
      <Leave leaveUser={leaveUser} />
      =============================
      <div>-----Worker-----</div>
      <Works worksUser={workUser} />
      <div>-----leave</div>
      <Leave leaveUser={leaveUser} />
      <button onClick={addUserEvent}> addUser</button>
      <button onClick={updateUserEvent}> updateUser</button>
      <button onClick={deleteUserEvent}> deleteUser</button>
    </div>
  )
}
export default React.memo(ListOperate)

在职人员组件:

const Works = ({ worksUser }: Props) => {
  return (
    <div>
      {worksUser.map((user) => {
        return <UserInfo key={user.id} user={user} />
      })}
    </div>
  )
}
export default React.memo(Works)

离职人员组件:

const Leave = ({ leaveUser }: Props) => {
  return (
    <div>
      {leaveUser.map((user) => {
        return <UserInfo key={user.id} user={user} />
      })}
    </div>
  )
}
export default React.memo(Leave)

人员信息显示组件:

const UserInfo = ({ user }: { user: User }) => {
  return (
    <div style={{ border: '1px solid' }}>
      <div>name: {user.name}</div>
      <ul>
        <li>id: {user.id}</li>
        <li>type: {user.type}</li>
        <li>email: {user.email}</li>
        <li>profileImageUrl: {user.profileImageUrl}</li>
      </ul>
    </div>
  )
}
export default React.memo(UserInfo)

这里插一个话题,查看下面 gif 动态图。使用了 React.memo 后,对页面进行增加、删除、修改操作,

  • 第一个记录板块:新增一个 UserInfo 组件
  • 第二个记录板块:只修改了一个 UserInfo 组件
  • 第三个记录板块:刚刚新增的 UserInfo 组件已经被删除掉,其他相关的 UserInfo 组件并未重新渲染
    Redux从简单到进阶(拥抱redux-toolkit)

5.1.3 useSelector 调用情况

上面我们在 slice 函数中,进行了打印日志,我们Workerleave各自调用了 2 次,我们来看打印情况:
Redux从简单到进阶(拥抱redux-toolkit)
根据上图,可以得出 useSelector每次都返回的是一个新的数组。

5.2 记忆化函数 Selector

其实按照我们正常的需求来说,每调用一次useSelector都会返回新数组,并不会有太大的影响。但是在实际项目中,可能会遇到依据 state 中的数据,通过许多步骤,计算出新的值来进行显示,如果数据量太大,那可能就会影响到性能,例如下面写的伪代码:

const getTransformeData = useSelector((state) => {
  // 取出state的someData
  const { someData } = state
  // 第一步过滤获取filterData
  const filteredData = expensiveFiltering(someData)
  // 依赖过去数据进行排序
  const sortedData = expensiveSorting(filteredData)
  // 依赖排序数据进行转换
  const transformedData = expensiveTransformation(sortedData)
  // 最后返回值
  return { data: transformedData }
})

上面的代码,对someData进行了 3 步数据转换,最后计算出了值。按照我们对useSelector的分析,每调用一次都会执行一次,那么小号就会特别的大。此时我们就可以利用Reselect函数了。

Reselect 是一个创建记忆化 selector 函数的库。它有一个 createSelector 函数,可以创建记忆化的 selector,只有在输入发生变化时才会重新计算结果。Redux Toolkit 导出了 createSelector 函数,这里我们来使用它。

createSelector 可以接受多个输入选择器,它们可以作为单独的参数或数组提供。所有输入选择器的结果作为单独的参数提供给输出选择器。

const selectOne = (state) => state.a
const selectTwo = (state) => state.b
const selectThree = (state) => state.c

const selectABC = createSelector(
  [selectOne, selectTwo, selectThree],
  (a, b, c) => {
    console.log('come in')
    // 数据处理
    return a * b * c
  },
)

// 首次调用, 打印`come in`
const abc = selectABC(state)
// 第二次调用:不打印
const abc = selectABC(state)
// 第三次调用:不打印
const abc = selectABC(state)

但是我们需要注意:Reselect 只记忆最近的一组参数。这意味着如果你用不同的输入重复调用一个选择器,输入选择器将每次都被执行。并将缓存更新为最新的这一次请求的结果。只有输入参数与当前存储记忆的数据一致,那么才会使用缓存数据。

const selectOne = (state) => state.a
const selectTwo = (state, paramId) => paramId

const selectABC = createSelector([selectOne, selectTwo], (a, b) => {
  console.log('come in')
  // 数据处理
  return a * b * c
})

// 首次调用, 打印`come in`
const abc1 = useSelector((state) => selectABC(state, '1'))
// 第二次调用,参数改变,打印`come in`
const abc2 = useSelector((state) => selectABC(state, '2'))
// 第二次调用:参数未改变,不打印
const abc3 = useSelector((state) => selectABC(state, '2'))

5.2.1 userSelectorSlice 添加 selectUserByType

根据上面对createSelector的使用分析,我们改善对workerleave人员的值的使用。

export const selectAllUser = (state) => state.userSelector
export const types = (state) => state.userSelector.map((user) => user.type)

export const selectUserByType = createSelector(
  [(state) => state.userSelector, (state, type) => type],
  (users, type) => {
    console.log('---selectUserByType')
    return users.filter((user) => user.type === type)
  },
)

5.2.2 组件中使用

const ListOperate = () => {
  const workUser: User[] = useSelector((state) => selectUserByType(state, '1'))
  const workUser2: User[] = useSelector((state) => selectUserByType(state, '1'))
  const leaveUser: User[] = useSelector((state) => selectUserByType(state, '0'))
  const leaveUser2: User[] = useSelector((state) =>
    selectUserByType(state, '0'),
  )
  // ...... 同上
  return (
    <div>
      <div>-----Worker-----</div>
      <Works worksUser={workUser} />
      <div>-----leave</div>
      <Leave leaveUser={leaveUser} />
      =============================
      <div>-----Worker-----</div>
      <Works worksUser={workUser2} />
      <div>-----leave</div>
      <Leave leaveUser={leaveUser2} />
      <button onClick={addUserEvent}> addUser</button>
      <button onClick={updateUserEvent}> updateUser</button>
      <button onClick={deleteUserEvent}> deleteUser</button>
    </div>
  )
}
export default React.memo(ListOperate)

查看打印,只打印了 2 次数据,2 条 workder 数据请求打印一次,2 条 leave 数据请求打印一次。

Redux从简单到进阶(拥抱redux-toolkit)
Reselect优秀文章推荐

5.3 范式化数据

什么是范式化 State 结构(引自官网):

  • 我们 state 中的每个特定数据只有一个副本,不存在重复。
  • 已范式化的数据保存在查找表中,其中项目 ID 是键,项本身是值。
  • 也可能有一个特定项用于保存所有 ID 的数组。

范式化数据,该数据结构,让我们通过id查询边的简单,不需要进行 filter 遍历数组即可获得。

{
  users: {
    ids: ["user1", "user2", "user3"],
    entities: {
      "user1": {id: "user1", firstName, lastName},
      "user2": {id: "user2", firstName, lastName},
      "user3": {id: "user3", firstName, lastName},
    }
  }
}

5.3.1 createEntityAdapter 管理范式化 State

createEntityAdapterRedux Toolkit提供的将数据范式化的 API,传入集合后,将返回{ids:[], entities: {}}结构,同时也会生成相关的reducer函数与selector函数。

  • 有一个选填参数sortComparer:可指定字段对数据排序
  • 返回对象 adapter,有getSelectors函数,传入一个 selector,它从 Redux 根 state 返回这个特定的 state slice,它会生成类似于 selectAll 和 selectById 的选择器。
  • 返回对象 adapter,有getInitialState函数:生成一个空的 {ids: [], entities: {}}对象。也可传入你希望的其他数据,进行合并。

5.3.2 使用 createEntityAdapter 改造异步 Users 实例

createEntityAdapter CRUD 函数

5.3.2.1 slice 函数改造

添加这个新的 slice,不要忘记加入 Store 中

import {
  createAsyncThunk,
  createEntityAdapter,
  createSelector,
  createSlice,
  EntityAdapter,
  PayloadAction,
} from '@reduxjs/toolkit'
import request from '@server/base/request'
import User from '@store/model/user'
import { State } from '..'

export interface UserInfo {
  data: User[];
  status: string;
  error: string;
}

// 使用 Redux Toolkit createAsyncThunk API 来简化异步调用(函数调用无改变)
export const getUserList = createAsyncThunk('/users/getlist', async () => {
  const response = await request.get('/list')
  return response
})
export const addUser = createAsyncThunk('/user/add', async (user: User) => {
  const response = await request.post('/add', { id: 'new', ...user })
  return response
})
export const updateUser = createAsyncThunk(
  '/user/update',
  async (user: User) => {
    const response = await request.post('/update', { id: 'new', ...user })
    return response
  },
)
export const deleteUser = createAsyncThunk(
  '/user/delete',
  async (user: User) => {
    const response = await request.post('/delete', { id: 'new' })
    return response
  },
)

// 1. 使用 createEntityAdapter 创建 adaptor,并按照id排序
const userListApater: EntityAdapter<User> = createEntityAdapter({
  sortComparer: (a: User, b: User) => b.id.localeCompare(a.id),
})

// 2. getInitialState 返回一个空的 {ids: [], entities: {}} 范式化 state 对象,我们传入了参数,会进行合并数据
const initialState = userListApater.getInitialState({
  status: 'idle',
  error: null,
})

// 3. 实例化slice
const userEntitiesSlice = createSlice({
  name: 'userEntities',
  initialState,
  reducers: {
    updateUserEmail(state, action) {
      const { id, email } = action.payload
      const userItem = state.entities[id]
      userItem.email = email
    },
  },
  extraReducers(builder) {
    builder

      // list user
      .addCase(getUserList.pending, (state) => {
        state.status = 'loading'
      })
      // 将现有 state 和 action.payload 传入 postsAdapter.upsertMany 函数,从而将所有用户数据添加到 state 中
      .addCase(getUserList.fulfilled, (state, action: PayloadAction<any>) => {
        state.status = 'succeeded'
        userListApater.upsertMany(state, action.payload)
      })
      .addCase(getUserList.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })

      // 将一个新用户添加到我们的 state 中
      .addCase(addUser.fulfilled, (state, action: any) => {
        state.status = 'succeeded'
        userListApater.addOne(state, { ...action.meta.arg, id: 'new' })
      })
      // 修改用户
      .addCase(updateUser.fulfilled, (state, action: any) => {
        state.status = 'succeeded'
        userListApater.updateOne(state, { ...action.meta.arg })
      })
      // 删除用户: 记住参数是一个Number, 可以根据typescript 类型进去找到
      .addCase(deleteUser.fulfilled, (state, action: any) => {
        state.status = 'succeeded'
        userListApater.removeOne(state, action.meta.arg)
      })
  },
})

// 本次新增一个同步的reducer, 异步的 createAsyncThunk 返回的内容就是action,因此方法名就是action 的name
export const { updateUserEmail } = userEntitiesSlice.actions

// 使用getSelectors,自动生成 selector函数,去掉以前手写的selector
// 因为需要知道这些方法依赖于谁生成的selector,需要去从那个数据中找,索引需要传入state中的参数
export const {
  selectAll: selectAllUsers,
  selectById: selectUserById,
  selectIds: selectUserIds,
  // selectEntities
  // selectTotal
} = userListApater.getSelectors((state: State) => state.userEntities)

export const selectStatus = (state: State) => {
  console.log(state.userEntities, '---------------')
  return state.userEntities.status
}

// Reselect 中的 createSelector 函数,以开启缓存计算结果的功能。 这里只是简单写一下,没有使用
export const selectUserItemById = createSelector(
  [selectAllUsers, (state, id) => id],
  (users, id) => users.filter((user) => user.id === id),
)

export default userEntitiesSlice.reducer

5.3.2.2 Component 中使用

同等怎删改查,没有什么特别的信息。

const Index = () => {
  const userList = useSelector(selectAllUsers)
  const status = useSelector(selectStatus)

  const dispatch = useDispatch()

  // 首次调用需要dispatch,获取数据
  useEffect(() => {
    if (status === 'idle') {
      dispatch(getUserList())
    }
  }, [status, dispatch])

  const addUserEvent = () => {
    dispatch(
      addUser({
        id: 'new',
        name: 'zhaoyezinew',
        type: '2',
        email: 'dd@qq.com',
        profileImageUrl: 'profileImageUrl',
      }),
    )
  }

  const updateUserEvent = () => {
    // 参数格式: {id: xx, changes: {}}
    dispatch(
      updateUser({
        id: userList[0].id,
        changes: { ...userList[0], name: 'zhaoyezi__change' },
      }),
    )
  }

  const deleteUserEvent = () => {
    dispatch(deleteUser(userList[0].id))
  }
  return (
    <div>
      {userList.map((user, key) => {
        return (
          <div key={key} style={{ border: '1px solid' }}>
            <div>name: {user.name}</div>
            <ul>
              <li>id: {user.id}</li>
              <li>type: {user.type}</li>
              <li>email: {user.email}</li>
              <li>profileImageUrl: {user.profileImageUrl}</li>
            </ul>
          </div>
        )
      })}
      <button onClick={addUserEvent}> addUser</button>
      <button onClick={updateUserEvent}> updateUser</button>
      <button onClick={deleteUserEvent}> deleteUser</button>
    </div>
  )
}
export default React.memo(Index)

6. RTK Query

RTK Query是一种专门为 Reudx 应用程序设计的数据获取和缓存解决方案。使用它后,不需要手动编写数据获取和缓存逻辑。Redux Toolkit提供了该 API。

在异步请求中,我们使用了createAsyncThunk结合createSlice。我们需要创建异步Thunk、发起请求、管理状态(例如 extraReducers 处理各种状态)。

在过去的几年里,React 社区已经意识到 “数据获取和缓存” 实际上是一组不同于 “状态管理” 的关注点。虽然你可以使用 Redux 之类的状态管理库来缓存数据,但用例差异较大,因此值得使用专门为数据获取用例构建的工具,而`RTK Query`也就应运而生。

6.1 RTK Qeury API

createApi: 核心功能,允许定义一组请求接口来描述一系列请求接口检索数据,包括如何获取和转换该数据的配置。一般每个基本 URL,应该定义一个api slice

import { createApi } from '@reduxjs/toolkit/query'

fetchBaseQuery:fetch 的一个小包装,用来简化请求,处理请求和相应。

/* 自动生成的特定于 React 的入口点,对应于定义请求接口的 hooks  */
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react'

当使用RTK Query,我们不在关心Store状态本身,而是考虑管理缓存数据。更关注定义数据来自那里更新应该如何发送缓存数据什么时候应该重新获取,缓存数据应该如何更新

6.2 改进异步 UserList 实例

6.2.1 apiSlice

先来一个增删改查实例,然后我们根据 code 进行解析字段:

// 从特定于 React 的入口点导入 RTK Query 方法
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import User from '@store/model/user'

// 定义单独的api Slice 对象
export const apiSlice = createApi({
  // 缓存减速器预计将添加到 `state.api`,默认为api, 这里自定义为rtkapi
  reducerPath: 'rtkapi',
  // 我们所有的请求都有以 “/api 开头的 URL, 看起来不需要我们再request对象中添加 url前缀了
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  // “endpoints” 代表对该服务器的操作和请求
  endpoints: (builder) => ({
    // `getUsers` endpoint 是一个返回数据的 “Query” 操作D
    getUsers: builder.query({
      // 请求的 URL 是“/api/list"
      query: () => '/list',
    }),
    getUser: builder.query({
      query: (id) => `/user/${id}`,
    }),
    addUser: builder.mutation({
      query: (newUser: User) => ({
        url: '/add',
        method: 'POST',
        body: newUser, // 新增的整个user信息作为body的参数
      }),
    }),
    updateUser: builder.mutation({
      query: (updateUser) => ({
        url: `/update/${updateUser.id}`,
        method: 'POST',
        body: updateUser, // 修改的整个user信息作为body的参数
      }),
    }),
    deleteUser: builder.mutation({
      query: (id) => ({
        url: '/delete',
        method: 'POST',
        body: { id },
      }),
    }),
  }),
})

export const {
  useGetUsersQuery,
  useGetUserQuery,
  useAddUserMutation,
  useUpdateUserMutation,
  useDeleteUserMutation,
} = apiSlice
export default apiSlice

字段名称

file namedescription
baseQuery用来从服务器获取数据 的函数,一般使用 fetchBaseQuery创建实例,参数是URL 前缀
endpoints与服务器交互的操作,可以是请求接口queries(缓存数据),也可以mutations(向服务器更新数据)
reducerPath生成的 reducer 定义了预期的顶级状态 slice 字段 (在 store 中的字段名名称),默认为api, RTKQ 缓存数据都将存储在 state.api

接口 hook 规则

apiSlice中,导出了一个名叫usegetUsersQuery,这是RTK Query的 React 集成会自动为我们定义的 每个请求接口生成 React hooks。这些 hook 封装了组件挂载时出发的请求过程,以及在处理请求和数据可用时重新渲染组件的过程, hook 命名规则:use + 接口名称(GetUsers) + 请求类型(Query)

  • use: react hook 前缀
  • 请求接口名称,首字母大写
  • 请求接口类型:Query 或 Mutation

6.2.3 store 配置

store 中引入apiSlice的 cache reducer,并在中间件中添加apiSlice生成的自定义middleware,该中间件管理缓存的什么周期和控制是否过期

const config = {
  reducer: {
    query: queryReducer,
    users: usersReducer,
    userInfo: userListReducer,
    userSelector: userSelectorReducer,
    userEntities: userEntitiesReducer,
    [apiSlice.reducerPath]: apiSlice.reducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(apiSlice.middleware),
}
const store = configureStore(config)
export default store

6.2.4 组件使用 Query Hooks

之前组件中我们使用了useSelectoruseDispatchuseEffect,从存储中读取数据,并在mount时调用getUserList thunk 获取数据。现在usegetUsersQuery Hook 取代了这些所有 hook。

Query Hooks生成的结果对象:

file namedescription
data来自服务器的实际响应内容, 响应前为 undefined
isLoading此 hooks 当前是否正在向服务器发出 第一次请求。(请注意,如果参数更改以请求不同的数据,isLoading 将保持为 false。
isFetchinghooks 当前是否正在向服务器发出 any 请求
isSuccesshooks 是否已成功请求并有可用的缓存数据
isError指示最后一个请求是否有错误
error一个 serialized 错误对象

Mutation hooks 返回的数据:

file namedescription
useAddUserMutation -(addUser)触发函数: 使用你的参数,向服务器发请求
isLoading志以指示请求是否正在进行中

import User from '@store/model/user'
import {
  useAddUserMutation,
  useDeleteUserMutation,
  useGetUserQuery,
  useGetUsersQuery,
  useUpdateUserMutation,
} from '@store/slice/rtkapi'
import React, { useMemo } from 'react'
import UserInfo from './Reselect/userInfo'

const item = {
  id: 'new',
  name: 'zhaoyezinew',
  type: '2',
  email: 'dd@naver.com',
  profileImageUrl: 'profileImageUrl',
}
const Rtk = () => {
  const {
    data: user = {},
    isLoading,
    isSuccess,
    isError,
    error,
  } = useGetUserQuery(1)
  const { data: userList = [] } = useGetUsersQuery({})

  // 数据回来是服务器原本的数据格式,想要对数据进行处理,可以在useMemo中操作
  const dealData = useMemo(() => {
    const data = userList.slice()
    data.sort((a, b) => b.id.localeCompare(a.id))
    return data
  }, [userList])

  const [addUser, { isLoading: addLoading }] = useAddUserMutation()
  const [updateUser] = useUpdateUserMutation()
  const [deleteUser] = useDeleteUserMutation()
  console.log(isLoading, isSuccess, isError, error, user)

  const addUserEvent = async () => {
    const newUser: User = item
    try {
      // 会返回一个带有 .unwrap() 方法的特殊 Promise ,我们可以 await addNewPost().unwrap() 使用标准的 try/catch 块来处理任何潜在的错误
      await addUser(newUser).unwrap()
    } catch (err) {
      console.log(err)
    }
  }

  const updateUserEvent = () => {
    updateUser({ ...item, name: 'zhaoyezi__change' })
  }

  const deleteUserEvent = () => {
    deleteUser('new')
  }

  return (
    <div>
      {dealData.map((u, key) => {
        return <UserInfo key={key} user={u} />
      })}
      <button onClick={addUserEvent}> addUser</button>
      <button onClick={updateUserEvent}> updateUser</button>
      <button onClick={deleteUserEvent}> deleteUser</button>

      {addLoading ? ' add loading' : 'add success'}
    </div>
  )
}
export default React.memo(Rtk)

使用注意事项

  1. RTK Hook 数据直接从 Hook 中拿取,所以类似于createEntityAdapter传入操作数据的方法(例如排序)就需要在使用的时候做处理(例如在 useMemo 中做处理)
  2. Query Hook查询参数必须是单一值,传递多个参数,使用一个对象传递(将对字段进行浅对比,如果其中任何一个发生更改,则重新获取数据)。
  3. Mutation hooks 的 hook 使用: 会返回一个带有 .unwrap() 方法的特殊 Promise ,我们可以 await addNewPost().unwrap() 使用标准的 try/catch 块来处理任何潜在的错误

6.3 刷新缓存数据

按照上面的实现,已完成增删改查,按照下图 gif 操作,增、删、改,通过接口查询,可以看到数据已经修改。但是界面上确没有任何反馈。

Redux从简单到进阶(拥抱redux-toolkit)

6.3.1 手动强制重新获取(refetch 函数)

Query hooks 结果对象包含一个 “refetch” 函数,我们可以调用它来进行重新获取数据。

const Rtk = () => {
  const {
    data: user = {},
    isLoading,
    isSuccess,
    isError,
    error,
  } = useGetUserQuery(1)
  // 析构refetch方法
  const { data: userList = [], refetch } = useGetUsersQuery({})
  // .......
  return (
    <div>
      ....
      <button onClick={addUserEvent}> addUser</button>
      <button onClick={updateUserEvent}> updateUser</button>
      <button onClick={deleteUserEvent}> deleteUser</button>
      // 使用手动调用refetch方法
      <button onClick={refetch}> 手动重新获取数据列表</button>
      {addLoading ? ' add loading' : 'add success'}
    </div>
  )
}
export default React.memo(Rtk)

Redux从简单到进阶(拥抱redux-toolkit)

6.3.2 缓存失效自动更新

RTK Query 让我们定义使用标签, 来处理查询Querymutations 之间的关系,以启用自动数据重新获取。标签是一个字符串或小对象,当缓存标签关联的数据失效,RTK Query将自动重新获取 有该标记的请求接口。标签使用需要向我们的 API slice 添加三条信息:

  • API slice 对象中的根 tagTypes 字段,声明数据类型的字符串标签名称数组,例如 'User' (tag名称随便取值)
  • 查询请求接口中的 “providesTags” 数组,列出了一组描述该查询中数据的标签
  • Mutation 请求接口中的“invalidatesTags”数组,列出了每次 Mutation 运行时失效的一组标签
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import User from '@store/model/user';

export const apiSlice = createApi({
  reducerPath: 'rtkapi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
	//  tagTypes 字段,声明数据类型的字符串标签名称数组
  tagTypes: ['User'],
  endpoints: builder => ({
    getUsers: builder.query({
      query: () => '/list',
      providesTags: ['User'],
    }),
    getUser: builder.query({
      query: (id) => `/user/${id}`,
    }),
    addUser: builder.mutation({
      query: (newUser: User) => ({
        url: '/add',
        method: 'POST',
        body: newUser, 
      }),
			// mutation 配置执行请求后,失效的标签
      invalidatesTags: ['User'],
    }),
    updateUser: builder.mutation({
      query: (updateUser) => ({
        url: `/update/${updateUser.id}`,
        method: 'POST',
        body: updateUser, 
      }),
			// mutation 配置执行请求后,失效的标签
      invalidatesTags: ['User'],
    }),
    deleteUser: builder.mutation({
      query: (id) => ({
        url: '/delete',
        method: 'POST',
        body: { id },
      }),
			// mutation 配置执行请求后,失效的标签
      invalidatesTags: ['User'],
    }),
  }),
});

export const { useGetUsersQuery, useGetUserQuery, useAddUserMutation, useUpdateUserMutation, useDeleteUserMutation } = apiSlice;
export default apiSlice;

通过配置了tag, 当我们点击 新增、删除、修改的时候,list接口触发了重新请求。达到了自动更新的目的。
Redux从简单到进阶(拥抱redux-toolkit)

7 RTK Query 高级查询

RTK Query 允许多个组件订阅相同的数据,并且将确保每个唯一的数据集只获取一次。
在内部,RTK Query 为每个请求接口 + 缓存键组合保留一个 action 订阅的引用计数器。
例如: 组件A 与组件B 调用了 useGetUserQuery(42), 在不同组件中,两个Hook返回结果完全相同。当组件A 与 组件B都卸载时,该hook数据订阅量为0,则RTK Query会启动内部计时器,到时间会将其自动从缓存中删除数据。不过倒计时过程中有订阅,则取消计时器,直接使用缓存内容。默认倒计时60秒(keepUnusedDataFor 总代吗。在接口中进行覆盖)

7.1 指定特定item无效

根据上面的tag指定,可以让缓存失效自动更细,但是我们每次只是修改了某一条,也会导致全部数据更新。RTK Query提供了定义特定标签,让我们指定具体某条数据更新

  • 特定标签格式{type: 'User', id: 123}

RTK Query Slice:

// 从特定于 React 的入口点导入 RTK Query 方法
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import User from '@store/model/user';

export const specialApiSlice = createApi({
  reducerPath: 'specialRtkapi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['User'],
  endpoints: builder => ({
    getUsers: builder.query({
      query: () => '/list',

			// 我们 为整个列表提供一个通用的 'User' 标签,以及为每个接收到的帖子对象提供一个特定的 {type: 'User', id} 标签。
			// 只要 任意一个标签失效,都会进行重新请求数据
      providesTags: (result = [], error, arg) => {
        const tags = [
          'User',
          ...result.map(({ id }) => ({ type: 'User', id })),
        ];
        return tags;
      },
    }),
    getUser: builder.query({
      query: (id: string) => `/user/${id}`,
			// 为单个 user 对象提供特定的 {type: 'User', id} 对象, 例如updateUser 方法使得这个标签失效,会将该接口从新请求
      // providesTags: (result, error, arg) => {
      //   return [{ type: 'User', id: arg }];
      // },
    }),
    addUser: builder.mutation({
      query: (newUser: User) => ({
        url: '/add',
        method: 'POST',
        body: newUser,
      }),
			// 使得User标签失效, getUsers 接口会重新请求
      invalidatesTags: ['User'],
    }),
    updateUser: builder.mutation({
      query: (updateUser) => ({
        url: `/update/${updateUser.id}`,
        method: 'POST',
        body: updateUser,
      }),
			// 通过标签失效,当前已经使用了通过id, 使用 getUser Hook取值的 相关数据接口,会重新再次请求
      invalidatesTags: (result, error, arg) => {
        return [{ type: 'User', id: arg.id }]
      },
    }),
    deleteUser: builder.mutation({
      query: (id) => ({
        url: '/delete',
        method: 'POST',
        body: { id },
      }),

			// 删除会使得 getPost重新请求
      invalidatesTags: ['User'],
    }),
  }),
});

export const { useGetUsersQuery, useGetUserQuery, useAddUserMutation, useUpdateUserMutation, useDeleteUserMutation } = specialApiSlice;
export default specialApiSlice;

7.1.1 不使用providesTags

这里我们先注释 getUser的 providesTags: (result, error, arg) => [{ type: 'User', id: arg }]

 getUser: builder.query({
      query: (id: string) => `/user/${id}`,
      // providesTags: (result, error, arg) => [{ type: 'User', id: arg }]
	}),
  • 在Component中使用useGetUserQuery(const { data: userdetail } = useGetUserQuery('new');),获取id为new的数据,无数据,为undefined。会缓存到store中。
  • 点击 addUser 按钮, 新增id为new的数据。userdetail 为undefined
  • 点击 updateUser按钮,修改id为new的数据,userdetail 为undefined

Redux从简单到进阶(拥抱redux-toolkit)

7.1.2 使用providesTags

打开getUser的providesTags:

 getUser: builder.query({
      query: (id: string) => `/user/${id}`,
      providesTags: (result, error, arg) => [{ type: 'User', id: arg }]
	}),
  • 在Component中使用useGetUserQuery(const { data: userdetail } = useGetUserQuery('new');),获取id为new的数据,无数据,为undefined。会缓存到store中。
  • 点击 addUser 按钮, 新增id为new的数据。userdetail 不为空,新建的数据
  • 点击 updateUser按钮,修改id为new的数据,userdetail 不为空,为修改的数据
  • 点击 deleteUser按钮,删除id为new的数据,userdetail 为空
    Redux从简单到进阶(拥抱redux-toolkit)

7.2 数据拆分,单独管理,响应数据预处理

使用 RTK Query,每一个应用程序都会有一个API Slice。上面的例子,在apislice中定义了所有的接口,但如果项目接口比较多,就需要对项目数据进行拆分管理。RTK Query提供apiSlice.injectEndpoints拆分请求接口。虽然数据才分管理,但是最终也只会提供一个带单个middleware和cache reducer的api slice。

  • 这里,我们将 getContents请求拆分出来,放入contentSlice.ts,然后再注入到apiSlice中。injectEndpoints()改变原始API Slice对象,添加了额外的接口定义,然后返回。
  • 请求接口定义transformResponse处理程序,将请求回来的数据在缓存之前提取或修改数据
  • 集成createEntityAdapter 来范式化管理contents数据

7.2.1 injectEndpoints 使用

// 从特定于 React 的入口点导入 RTK Query 方法
import { createEntityAdapter, createSelector } from '@reduxjs/toolkit';
import apiSlice from './specialrtkapi';


const contentAdapter = createEntityAdapter();
const initialState = contentAdapter.getInitialState();

export const contentsSpecialApiSlice = apiSlice.injectEndpoints({
  endpoints: builder => ({
    getContents: builder.query({
      query: () => '/contents',
      transformResponse (responseData, meta, arg) {
        // responseData[0].resourceName = `transformResponse:${responseData[0].resourceName}`;
        return contentAdapter.setAll(initialState, responseData);
      },
    }),
  }),
})

 // 通过apiclice 默认导出的 use+ 名称+ query(或者mountain) 的hook,用于组建中使用
export const { useGetContentsQuery } = contentsSpecialApiSlice;

// 调用 `endpoints.select(someArg)` 会生成一个新的 selector 函数,该 selector 将返回带有这些参数的查询的查询结果对象。
// 要为特定查询参数生成 selector,请调用 `select(theQueryArg)`。
export const selectContentsResult = contentsSpecialApiSlice.endpoints.getContents.select();

// // 使用createSelector创建缓存数据。
// // contents 为 {"status": "uninitialized","isUninitialized": true,"isLoading": false, "isSuccess": false,"isError": false, data: xxx }
const selectContentsData = createSelector(selectContentsResult, (contentResult) => contentResult.data)

export const { selectAll: selectContentsAll, selectById: selectContentById } = contentAdapter.getSelectors((state) => selectContentsData(state) ?? initialState);


// 也可以直接从 apiSlice 中获取其他的接口数据再缓存
// export const selectUsers = apiSlice.endpoints.getUsers.select({});
// export const selecctAllUsers = createSelector(selectUsers, users => users || []);

7.2.2 组价引入slice

直接使用Adapter selector,第一个问题: 接口不会请求。

  • 解决办法:使用useGetContentsQuery调用一次: const { isLoading, isFetching, error } = useGetContentsQuery({}); 虽然接口调用请求了,但是contents的值取不到。 将 useGetContentsQuery({})的参数{}去掉,请求成功。具体原理暂时不清楚。解决问题URL: stackoverflow

官网实例。

  • 没有请求useGetContentsQuery的方法, 在程序入口调用store.dispatch(contentsSpecialApiSlice.endpoints.getContents.initiate())方法进行初始化。
import { selectContentsAll, useGetContentsQuery } from '@store/slice/hightSpecialrtkapi';
import React from 'react';
import { useSelector } from 'react-redux';

const HightSpecialRtk = () => {

	const { isLoading, isFetching, error } = useGetContentsQuery();

	const contents = useSelector(selectContentsAll)
console.log(contents)
	return (
		<div>
			<div>Contens:</div>
			{
				contents.map((content, key) => {
					return <div key={ key }>name: { content.resourceName }</div>
				})
			}
		</div>
	)
}
export default React.memo(HightSpecialRtk)

7.2.3 从结果中选值

这里有官网实例,也比较好理解了。从结果中取值

一般来说有3中数据转换的方式:

  • 1.将原始响应保存在缓存中,读取组件中的完整结果并导出值。(per-component / useMemo:当只有某些特定组件需要转换缓存数据时)
  • 2.在存储到缓存之前转换响应 :(transformResponse:请求接口的所有消费者都需要特定的格式,例如标准化响应以实现更快的 ID 查找)
  • 3.将原始响应保存在缓存中,使用 selectFromResult 读取派生结果。(selectFromResult:请求接口的一些消费者只需要部分数据,例如过滤列表)

其中前两种我们都已经在前面探讨过了,下面是第三种的例子如下:

  • 我们先获取了用户信息
  • 根据用户id,过滤 content列表。

import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from '@store/index';
import { useGetContentsQuery } from '@store/slice/hightSpecialrtkapi';
import { getUserList, selectStatus, selectUserById } from '@store/slice/userEntityAdapter';
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';

const RtkByResult = () => {
	const { userId } = useParams();

	// 获取单个用户信息
	const user = useSelector((state) => selectUserById(state, userId))
	const status = useAppSelector(selectStatus);

	// 先获取用户列表数据
	const dispatch = useDispatch();
	useEffect(() => {
		if (status === 'idle') {
			dispatch(getUserList());
		}
	}, [status, dispatch])

	const selectContentsForUser = useMemo(() => {
		const emptyArray = []
		// 返回此页面的唯一 selector 实例,以便过滤后的结果被正确记忆
		return createSelector(
			res => res.data,
			(res, id) => id,
			(data, id) => {
				const a = Object.keys(data.entities).map((cId) => {
					console.log('--come in')
					if (data.entities[cId].user === Number(id)) {
						return data.entities[cId];
					}
				});

				return a || emptyArray;
			},
		)
	}, [])

   // 使用相同的Content查询,但仅提取其部分数据
	// 这里的 memoized selector 函数有一个关键的区别:一般第一个参数是 `selector 期望整个 Redux state 作为它们的第一个参数`
	// 这里我们只处理了保存在缓存中的`result`值,result 对象内部有一个 “data” 字段,其中包含我们需要的实际值,以及一些请求元数据字段
	const { contetnForUser } = useGetContentsQuery(undefined, {
		selectFromResult: result => ({
			// 我们可以选择在此处包含结果中的其他元数据字段
			...result,
			// 在 hook 结果对象中包含一个名为 contetnForUser 的字段,
			// 这将是一个过滤的帖子列表
			contetnForUser: selectContentsForUser(result, userId),
		}),
	})

	return (
		<div>
			<div>User: { user && user.name } 的content 列表:</div>
			{
				contetnForUser.map((content, key) => {
					return <div key={ key }>name: { content.resourceName }</div>
				})
			}
		</div>
	)
}
export default React.memo(RtkByResult);

7.3 Optimistic Updates

继续上面分片的实例,当想服务器发起更新某一条Content数据,我们不用根据Tag重新获取Content里诶包,可以只更新客户端的缓存数据。在发起update请求时,有一个请求什么周期处理程序 onQueryStarted ,在请求开始时调用,我们可以在这里对客户端的content list进行局部更新。

下面的slice,就是在上面的分片slice基础上,添加一个addContent接口,并在onQueryStarted 进行局部更新。

export const contentsSpecialApiSlice = apiSlice.injectEndpoints({
  endpoints: builder => ({
    getContents: builder.query({
      query: () => '/contents',
      transformResponse (responseData, meta, arg) {
        // responseData[0].resourceName = `transformResponse:${responseData[0].resourceName}`;
        return contentAdapter.setAll(initialState, responseData);
      },
      providesTags: ['Content'],
    }),
    addContent: builder.mutation({
      query: (content) => ({
        url: `/content/${content.resourceNo}`,
        method: 'POST',
        body: content,
      }),
      async onQueryStarted (content, { dispatch, queryFulfilled }) {
        // `updateQueryData` 需要请求接口名称和缓存键参数,所以它知道要更新哪一块缓存状态
        const patchResult = dispatch(
					// 这里 getContents 没有参数,所以第二个参数为undefined
          contentsSpecialApiSlice.util.updateQueryData('getContents', undefined, draft => {
            // `draft` 是 Immer-wrapped 的,可以像 createSlice 中一样 “mutated”
            contentAdapter.updateOne(draft, { id: content.resourceNo, changes: content });
          }),
        )
        try {
          await queryFulfilled
        } catch {
          patchResult.undo()
        }
      },
    }),
  }),
})

按下图,虽然没有使用Posttag,也没有重新获取post列表接口,但是界面的信息还是更新了。
Redux从简单到进阶(拥抱redux-toolkit)

8 流式缓存更新

RTK Query 提供了一个 onCacheEntryAdded 请求接口生命周期处理程序,让我们可以对缓存数据实施“流式更新”。 具体需要再去研究吧,感觉和上面的 onQueryStarted是异曲同工的。 流式缓存更新

9. 问题

9.1 dipatch typescript 报错处理

后面在添加typescript的操作中,遇到了dipatch 异步的验证校验出错。

  • 解决办法

9.2 全局错误获取方式

第一种方式: RTK Query 如何获取全局Loading?

const isSomeQueryPending = useAppSelector((state: RootState) => {
  return Object.values(state.specialRtkapi.queries).some(q => {
		// 可以拿到好几个参数,可以进行通用错误等信息的处理
     console.log(q.originalArgs, q.data, q.error); 
		 return q.status === 'pending' 
    })
});
console.log('special isSomeQueryPending: ', isSomeQueryPending)

第二种方式:createApi 中的baseQuery,自定义。里面的参数可以拿到dispatch, 可以修改store中的其他数据(第二个参数中)

const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (args: any, api, extraOptions) => {
  ......
  const result = await rawBaseQuery(args, api, extraOptions);
  const { error, data } = result;
  const resData = jsonBig.parse(data);
  // 异常处理
  if (resData?.code !== '0' || !resData) {
   
  }
  return result;
}

export const logSlice = createApi({
  reducerPath: 'logApi',
  baseQuery: dynamicBaseQuery,

9.3 表单发送请求

reateApi({
  reducerPath: 'logApi',
  baseQuery: fetchBaseQuery({
    baseUrl: '/service',
    prepareHeaders (headers, api) {
      headers.set(
        'Content-Type',
        'application/x-www-form-urlencoded;charset=UTF-8',
      );
      return headers;
    },
  }),
  tagTypes: ['Log'],

  endpoints: builder => ({
   .....
     

9.4 动态设置baseQuery 解决未初始化值

  • 动态设置baseQuery: 加入window.domain 对象项目初始化时还未设置,可以通过动态设置baseQuery解决问题
const rawBaseQuery = fetchBaseQuery({
  baseUrl: window.domain,
  credentials: 'include',
  prepareHeaders (headers, api) {
    headers.set(
      'Content-Type',
      'application/x-www-form-urlencoded;charset=UTF-8',
    );
    .....
    return headers;
  },
})

const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> = async (args: any, api, extraOptions) => {
  args.url = `${window.domain}${args.url}`;
  return rawBaseQuery(args, api, extraOptions)
}

export const logSlice = createApi({
  reducerPath: 'logApi',
  baseQuery: dynamicBaseQuery,
  tagTypes: ['Log'],

  endpoints: builder => ({

9.5 处理biginit

responseHandler: 将返回值处理为text,在利用json-bigint 库处理。

import jsonBigInit from 'json-bigint';
const jsonBig = jsonBigInit({ storeAsString: true });
......
 query: (query: Query) => ({
    url: '/service/activity/list',
    body: qs.stringify(query),
    method: 'POST',
    responseHandler: (response) => response.text(),
    }),
    transformResponse (response: APIResponseType<LogResult>, meta, arg) {
    return jsonBig.parse(response);
    },

10. 结束

redux提供的这个toolkit包终于完成了,其实看的时候感觉只有一个懵懂的印象,确实还是需要自己手动去敲一遍才会熟悉。大家多动动收吧~~