redux-toolkit 完整学习笔记

Redux Toolkit(RTK)

1. 安装

@reduxjs/toolkit依赖于eact-redux库,react-redux 包依赖于@types/react-redux, 因此类型定义将与库一起自动安装。

yarn add react-redux @reduxjs/toolkit
yarn add @types/react-redux -D

2. rtk 包含了什么

  • 🚩configureStore():封装了createStore,简化配置项,提供一些现成的默认配置项。它可以自动组合 slice 的 reducer,可以添加任何 Redux 中间件,默认情况下包含 redux-thunk,并开启了 Redux DevTools 扩展。
  • createReducer():帮你将 action type 映射到 reducer 函数,而不是编写 switch...case 语句。另外,它会自动使用 immer 库来让你使用普通的 mutable 代码编写更简单的 immutable 更新,例如 state.todos[3].completed = true。
  • createAction():生成给定 action type 字符串的 action creator 函数。该函数本身已定义了 toString(),因此可以代替常量类型使用。
  • 🚩createSlice():接收一组 reducer 函数的对象,一个 slice 切片名和初始状态 initial state,并自动生成具有相应 action creator 和 action type 的 slice reducer。
  • 🚩createAsyncThunk:接收一个 action type 字符串和一个返回值为 promise 的函数, 并生成一个 thunk 函数,这个 thunk 函数可以基于之前那个 promise ,dispatch 一组 type 为 pending/fulfilled/rejected 的 action。
  • 🚩createEntityAdapter:生成一系列可复用的 reducer 和 selector,从而管理 store 中的规范化数据。
  • 🚩createSelector 计划化Selector实现缓存,来源于 Reselect 库,重新 export 出来以方便使用。

3. configureStore

configureStore 是 Redux Toolkit 中的一个函数,用于创建 Redux store。

它是redux 的一个配置器,简化配置store的过程。

3.1 配置项

常用配置项:

  • reducer:Redux store使用的根reducer函数。
  • middleware:要应用于Redux store的中间件数组。
  • devTools:一个布尔值,指示是否启用Redux DevTools扩展。
  • preloadedState:Redux store的初始状态。
  • enhancers:要应用于Redux store的增强器数组。

不常用配置项:

  • reducerPath:一个字符串,指示Redux store中包含的reducer的名称。
  • immutableCheck:一个布尔值,指示是否启用Redux Toolkit的不可变性检查。
  • serializableCheck:一个布尔值,指示是否启用Redux Toolkit的序列化检查。

3.2 示例

// 简易示例
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'

// 调用configureStore默认使用了
// 1. redux-thunk中间件来支持异步action,
// 2. redux-devtools-extension来支持ReduxDevTools浏览器扩展,
// 3. redux-immutable-state-invariant来检测state的不可变性。
const store = configureStore({
  reducer: rootReducer
})

export default store
// 完整示例
// store.ts文件
/* 
  以下代码完成了下列功能
  - slice reducers被自动传递给了combineReducers()函数
  - redux-thunk和redux-logger被添加为中间件
  - Redux DevTools扩展在生产环境中被禁用
  - 中间件、批量订阅和devtools增强器被组合在一起
*/

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
// redux-logger在此仅作为一个示例中间件来演示(记录Redux store中的每个action和state的变化)
import logger from 'redux-logger'
// redux-batched-subscribe在此仅作为一个示例增强器来演示(提高应用程序的性能和响应速度)
import { batchedSubscribe } from 'redux-batched-subscribe'
// 引入两个上面写好的reducer
import todosReducer from './todos/todosReducer'
import visibilityReducer from './visibility/visibilityReducer'

// 因为使用了rtk的configureStore,所以不需要另外合并combineReducers了
const reducer = {
  todos: todosReducer,
  visibility: visibilityReducer,
}

// getDefaultMiddleware函数返回一个包含默认中间件列表的数组,可以用于添加自定义middleware,同时仍然保留默认middlewar
// 官网不推荐使用数组展开运算符const middleware = [...getDefaultMiddleware(), customMiddleware],请使用concat
// 扩展运算符可能都会导致ts丢失类型
const middleware = (getDefaultMiddleware) => getDefaultMiddleware().concat(logger)

// 更加灵活的自定义默认中间件,了解
// const middleware = (getDefaultMiddleware) =>
//     getDefaultMiddleware({
//       thunk: {
//         extraArgument: myCustomApiService,  // extraArgument 属性指定了一个自定义的 API 服务,它将作为第三个参数传递给 action creator
//       },
//       serializableCheck: false, // 关闭serializableCheck中间件
//     }),

// 定义初始状态
const preloadedState = {
  todos: [
    { text: 'Eat food', completed: true, },
    { text: 'Exercise', completed: false, },
  ],
  visibilityFilter: 'SHOW_COMPLETED',
}

const debounceNotify = _.debounce((notify) => notify())

const store = configureStore({
  reducer,      // reducer是必需的,它指定了应用程序的根reducer
  middleware,      // 中间件,用于处理action。如果多个中间件也可以用数组表示
  devTools: process.env.NODE_ENV !== 'production',      // 是否启用Redux DevTools
  preloadedState,       // 对象,它包含应用程序的初始状态
  enhancers: [batchedSubscribe(debounceNotify)],        // 应用程序的增强器
})

4. createSlice

创建reducer的切片文件,并自动生成action creators。简化 Redux reducer 逻辑和 actions。

4.1 createSlice参数

createSlice 接收一个包含三个主要选项字段的对象:

  • name:一个字符串,将用作生成的 action types 的前缀
  • initialState:reducer 的初始 state
  • reducers:一个对象,其中键是字符串,值是处理特定 actions 的 “case reducer” 函数

4.2 示例

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { GlobalState } from "@/store/interface";
import type { RootState } from "@/store";

// 定义初始化的state
const initialState: GlobalState = {
	token: "",
	userInfo: ""
}

/* 
	Redux 要求我们通过创建数据副本和更新数据副本,来实现不可变地写入所有状态更新。
	不过 Redux Toolkit createSlice 和 createReducer 
	在内部使用 Immer 允许我们编写“可变”的更新逻辑,变成正确的不可变更新。
*/
const globalSlice = createSlice({
	name: 'global',
	initialState,
	/* 
		createSlice 实际上是一个包装器,它使用 createReducer 来生成 reducer 函数,
		这里直接传入reducers对象,就相当于在包装器里调用了createReducer
	*/
	reducers: {
		setToken(state, action: PayloadAction<GlobalState['token']>) {
			// Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。它
			// 并不是真正的改变状态值,因为它使用了 Immer 库
			// 可以检测到“草稿状态“ 的变化并且基于这些变化生产全新的
			// 不可变的状态
			state.token = action.payload;
		},
		// 如果我们需要对dispatch 传来的数据 进行标准化处理,可以用以下写法并传入prepare函数
		// prepare函数首先接受dispatch 的负载(payload)并处理,然后才会传递给reducer。
		// prepare函数作用如其名,是等于对负载做预处理
		setUserInfo:{
			reducer(state,action){
  			state.userInfo = action.payload
			},
			// prepare函数会接受dispatch 参数,并在处理完后把这个负载传递给reducer
      prepare(userInfo) {
        return {
          payload: { 
						name:userInfo.name.trim(),
						address:userInfo.substring(0,10),
						vip:false
					}
        }
      }
		},
		setUserInfo(state, action: PayloadAction<GlobalState['userInfo']>) {
			state.userInfo = action.payload
		},
	}
})

// 导出 slice 中生成的 Redux action creators 和 reducer 函数(被Redux使用来更新state)。
// 每个 case reducer 函数会生成对应的 Action creators,这里就不用单独写action文件来管理action
export const { setToken, setUserInfo } = globalSlice.actions;

// 类比vuex getters,
export const selectToken = (state: RootState) => { state.global.token }

export default globalSlice.reducer;

如果我们使用usedispatch调用action,setToken('66'),// {type: 'global/setToken', payload: '66'}

首先会使用reducers 里的 prepare函数来修改传入的payload负载

然后在将修改完后payload参数传递给reducer 来进行state的更新

5. createAsyncThunk

为异步调用生成 thunk,createAsyncThunk 函数返回的是一个带有状态机的 thunk action。

返回一个Promise,并自动生成pending/fulfilled/rejected 的action type

5.1 createAsyncThunk函数的参数

  1. type:定义action type,字符串。当定义type为'users/requestStatus',将自动生成以下action type,表示异步请求的生命周期

    • pending:'users/requestStatus/pending'
    • fulfilled:'users/requestStatus/fulfilled'
    • rejected:'users/requestStatus/rejected'
  2. payloadCreator:一个 “payload creator” 回调函数,定义异步操作的函数。这个函数有两个参数 payload负载 和 thunkAPI对象。

    • payloadCreator回调的第一个参数,payload负载,dispatch 时无论传入什么都将成为payload creator回调的第一个参数。如果需要传递多个值,请将它们包裹到一个对象中
    • payloadCreator回调的第二个参数,thunkAPI对象包含以下
      • dispatch:Redux 存储dispatch方法
      • getState:Redux 存储getState方法
      • extra:在设置时给 thunk 中间件的“额外参数”。这种常用时某种API的封装器,如果你需要在thunk中使用API调用,你可以使用API封装器来封装所有的URL和查询通讯,并将这个函数作为参数传递给thunk。
      • requestId:该thunk调用的唯一随机ID,用于跟踪单个请求的状态。
      • rejectWithValue(value, [meta]): rejectWithValue 是一个实用函数,您可以return(或throw)自定义rejected的返回值,如果没定义rejectWithValue则返回action.error。如果您还传入 a meta,它将与现有的 合并rejectedAction.meta。
      • fulfillWithValue(value, meta): fulfillWithValue 是一个实用函数,你可以return在你的 action creator 中添加fulfill一个值,同时可以添加到fulfilledAction.meta.
  3. option:payloadCreator回调的第三个参数option的对象

    • condition(thunkaArg, { getState, extra } ): boolean | Promise:用于控制取消thunk的条件,return一个布尔值或promise作为条件是否需要取消。
    • dispatchConditionRejection: boolean :如果condition()返回false,默认行为是根本不会调度任何动作。如果您仍然希望在取消 thunk 时dispatch“rejected”操作,请将此配置项设置为true.
    • idGenerator(arg): stringrequestId:生成请求序列时使用的函数。默认使用nanoid,但您可以实现自己的 ID 生成逻辑。
    • serializeError(error: unknown) => anyminiSerializeError用您自己的序列化逻辑替换内部方法。
    • getPendingMeta({ arg, requestId }, { getState, extra }): any:创建将合并到字段中的对象的函数pendingAction.meta。

5.2 和createAsyncThunk函数 生命周期

  1. 生命周期

这个 thunk action 会 dispatch "pending"、"fulfilled" 和 "rejected" 三种 action 类型,分别对应异步操作的三种状态。

// fetchTodos.pending:todos/fetchTodos/pending
// fetchTodos.fulfilled:todos/fetchTodos/fulfilled
// fetchTodos.rejected:todos/fetchTodos/rejected
export const fetchTodos = createAsyncThunk('todos/fetchTodos', async (_,{getState,dispatch}) => {
  const response = await client.get('/fakeApi/todos')
  return response.todos
})
  1. 生命周期有关的ts类型
// 接口,用于描述一个序列化的错误对象。它包含了错误的名称、消息、代码和堆栈信息。
interface SerializedError {
  name?: string
  message?: string
  code?: string
  stack?: string
}
// 接口,用于描述一个异步操作的“等待中”状态。它包含了action的type、payload和meta(其中包括请求ID和操作参数。)
interface PendingAction<ThunkArg> {
  type: string
  payload: undefined
  meta: {
    requestId: string
    arg: ThunkArg
  }
}
// 接口,用于描述一个异步操作的“已完成”状态。
interface FulfilledAction<ThunkArg, PromiseResult> {
  type: string
  payload: PromiseResult
  meta: {
    requestId: string
    arg: ThunkArg
  }
}
// 接口,用于描述一个异步操作的“已拒绝”状态。它包含了action的type、payload、error对象和meta(其中包括请求ID、操作参数、是否中止和条件。)
interface RejectedAction<ThunkArg> {
  type: string
  payload: undefined
  error: SerializedError | any
  meta: {
    requestId: string
    arg: ThunkArg
    aborted: boolean
    condition: boolean
  }
}
// 接口,用于描述一个异步操作的“已拒绝”状态,并且提供了一个拒绝值。它包含了action的type、payload、error对象和meta(其中包括请求ID、操作参数和是否中止。)
interface RejectedWithValueAction<ThunkArg, RejectedValue> {
  type: string
  payload: RejectedValue
  error: { message: 'Rejected' }
  meta: {
    requestId: string
    arg: ThunkArg
    aborted: boolean
  }
}
// 类型别名,用于描述一个函数,该函数接受请求ID和操作参数,并返回一个“等待中”状态的操作对象。
type Pending = <ThunkArg>(
  requestId: string,
  arg: ThunkArg
) => PendingAction<ThunkArg>
// 类型别名,用于描述一个函数,该函数接受异步操作的结果、请求ID和操作参数,并返回一个“已完成”状态的操作对象。
type Fulfilled = <ThunkArg, PromiseResult>(
  payload: PromiseResult,
  requestId: string,
  arg: ThunkArg
) => FulfilledAction<ThunkArg, PromiseResult>
// 类型别名,用于描述一个函数,该函数接受请求ID和操作参数,并返回一个“已拒绝”状态的操作对象。
type Rejected = <ThunkArg>(
  requestId: string,
  arg: ThunkArg
) => RejectedAction<ThunkArg>
// 类型别名,用于描述一个函数,该函数接受请求ID、操作参数和拒绝值,并返回一个“已拒绝”状态的操作对象。 
type RejectedWithValue = <ThunkArg, RejectedValue>(
  requestId: string,
  arg: ThunkArg
) => RejectedWithValueAction<ThunkArg, RejectedValue>
  1. extraReducers 监听生命周期

extraReducers 一个是用途是监听createAsyncThunk返回的状态机。

另一个用途是用来书写多个reducer重复部分,将他们整理共享化,解决冗余问题。甚至可以使用其他slice下的action。

  • builder.addCase(actionCreator, reducer):定义一个 case reducer,它响应 RTK action creator 生成或者普通字符串定义的 action。
  • builder.addMatcher(matcher, reducer):定义一个 case reducer,它可以响应任何 matcher 函数返回 true 的 action.
  • builder.addDefaultCase(reducer):定义一个 case reducer,如果没有其他 case reducer 被执行,这个 action 就会运行。

你可以将这些链接在一起,例如builder.addCase().addCase().addMatcher().addDefaultCase()。 如果多个匹配器匹配操作,它们将按照定义的顺序运行。

在 Redux Toolkit 中,extraReducers 的 addCase 方法的第一个参数是一个 action creator,用于监听相应的 action 类型。在这个例子中,我们使用了 createAsyncThunk 函数创建了一个名为 "posts/fetchPosts" 的异步 thunk action。在 extraReducers 中,我们监听了这个 thunk action dispatch 的 "pending"、"fulfilled" 和 "rejected" 三种 action 类型,并在相应的 reducer 中更新了 state。

在下面这个例子中,我们使用了 createAsyncThunk 函数创建了一个名为 "posts/fetchPosts" 的异步 thunk action。在 extraReducers 中,我们监听了这个 thunk action dispatch 的 "pending"、"fulfilled" 和 "rejected" 三种 action 类型,并在相应的 reducer 中更新了 state。在 "pending" reducer 中,我们将 state.status 设置为 'loading';在 "fulfilled" reducer 中,我们将 state.status 设置为 'succeeded',并将 action.payload 中的数据添加到 state.posts 数组中;在 "rejected" reducer 中,我们将 state.status 设置为 'failed',并将 action.error.message 存储在 state.error 中。

以下是代码示例:

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { client } from '../../api/client'

export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
  const response = await client.get('/fakeApi/posts')
  return response.data
})

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // omit existing reducers here
  },
  extraReducers(builder) {
    builder
      .addCase(fetchPosts.pending, (state, action) => {
        state.status = 'loading'
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.posts = state.posts.concat(action.payload)
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

5.3 处理thunk的的结果

createAsyncThunk 在内部处理了所有错误,因此我们在日志中看不到任何关于“rejected Promises”的消息。然后它返回它 dispatch 的最终 action:如果成功则返回“已完成” action,如果失败则返回“拒绝” action。Redux Toolkit 有一个名为 unwrapResult 的工具函数,它将返回来自 fulfilled action 的 action.payload 数据,或者如果它是 rejected action 则抛出错误。这让我们可以使用正常的 try/catch 逻辑处理组件中的成功和失败。

但是,想要编写查看实际请求成功或失败的逻辑是很常见的。 Redux Toolkit 向返回的 Promise 添加了一个 .unwrap() 函数,它将返回一个新的 Promise,这个 Promise 在 fulfilled 状态时返回实际的 action.payload 值,或者在 rejected”状态下抛出错误。这让我们可以使用正常的“try/catch”逻辑处理组件中的成功和失败。 因此,如果帖子创建成功,我们将清除输入字段以重置表单,如果失败,则将错误记录到控制台。

  1. unwrap 处理结果为标准的promise风格
// 组件中
const onClick = async () => {
  try {
    const originalPromiseResult = await dispatch(fetchUserById(userId)).unwrap()
    // 处理结果
  } catch (rejectedValueOrSerializedError) {
    // 处理错误
  }
}
  1. rejectWithValue 自定义错误的返回
// 使用rejectWithValue
const updateUser = createAsyncThunk(
  'users/update',
  async (userData, { rejectWithValue }) => {
    const { id, ...fields } = userData
    try {
      const response = await userAPI.updateById(id, fields)
      return response.data.user
    } catch (err) {
      // Use `err.response.data` as `action.payload` for a `rejected` action,
      // by explicitly returning it using the `rejectWithValue()` utility
      return rejectWithValue(err.response.data)
    }
  }
)

5.4 取消thunk

  1. thunk执行前是否取消的判断
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId: number, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  },
	// createAsyncThunk第三个参数option
  {
		// condition 回调用于判断执行前是否需要取消
    // - condition回调 第一个参数是thunk传递的参数。
   	// - condition回调 第二个参数是是个对象,{getState, extra}
    condition: (userId, { getState, extra }) => {
      const { users } = getState()
      const fetchStatus = users.requests[userId]
      if (fetchStatus === 'fulfilled' || fetchStatus === 'loading') {
        // 已经fetch过或者loading中 , 不需要重新fetch
        return false
      }
    },
		// 配置项dispatchConditionRejection用于condition()return false的时候仍然dispatch 一个rejected的action
		dispatchConditionRejection:false

  }
)
  1. 执行thunk中终止
    createAsyncThunk 返回的promise有个abort()方法,可以取消。
function MyComponent(props: { userId: string }) {
  const dispatch = useAppDispatch()
  React.useEffect(() => {
    // Dispatching thunk 返回一个promise
    const promise = dispatch(fetchUserById(props.userId))
    return () => {
      // createAsyncThunk 返回的promise有个abort()方法
      promise.abort()
    }
  }, [props.userId])
}

以上面这种方式取消 thunk 后,它将分派(并返回)一个“thunkName/rejected”操作,错误属性上带有 AbortError。 thunk 不会发送任何进一步的操作。

如果你使用fetch,可以通过 thunkAPI.signal 传递的 AbortSignal 来实际取消异步操作。

const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId: string, thunkAPI) => {
    const response = await fetch(`https://reqres.in/api/users/${userId}`, {
      signal: thunkAPI.signal,
    })
    return await response.json()
  }
)
  1. 通过thunkapi的signal.aborted检查取消的状态
import { createAsyncThunk } from '@reduxjs/toolkit'

const readStream = createAsyncThunk(
  'readStream',
  async (stream: ReadableStream, { signal }) => {
    const reader = stream.getReader()

    let done = false
    let result = ''

    while (!done) {
      if (signal.aborted) {
        throw new Error('stop the work, this has been aborted!')
      }
      const read = await reader.read()
      result += read.value
      done = read.done
    }
    return result
  }
)
  1. 判断一个rejected的错误是由网络错误还是主动取消的

要调查有关 thunk 取消的行为,您可以检查已调度操作的元对象的各种属性。 如果一个 thunk 被取消,promise 的结果将是一个被拒绝的动作(不管那个action是否真的dispatch到store)。

  • meta.condition 将为真,说明在执行前取消。
  • meta.aborted 将为真,说明在运行时终止。
  • 如果这两个都不为真,则 thunk 不会被取消,它只是被拒绝,或者被 Promise 拒绝或 rejectWithValue 拒绝。
  • 如果 thunk 没有被拒绝,则 meta.aborted 和 meta.condition 都将是未定义的。

6. selector、useSelector 和 createSelector

6.1 selector(纯函数)

Selector 是一个纯函数,用来从 Redux store 中获取数据并进行转换或计算,返回视图组件需要的数据,这有助于减少视图组件中的逻辑。

Selector 本质上是一个纯函数,接收数据作为参数并返回新的数据结果。

由于selector 不依赖额外的库,只是个纯函数,以下写法都是可行的。另外别忘了在根组件使用provider 实现全组件透传,保证能在所有组件中访问 Store 中的状态和 dispatch 方法。

// 箭头函数,直接查找
const selectEntities = state => state.entities

// 函数声明,映射数组来派生值
function selectItemIds(state) {
  return state.items.map(item => item.id)
}

// 函数声明,封装深度查找
function selectSomeSpecificField(state) {
  return state.some.deeply.nested.field
}

// 箭头函数,从数组中派生值
const selectItemsWhoseNamesStartWith = (items, namePrefix) =>
  items.filter(item => item.name.startsWith(namePrefix))

Selector 函数通常定义在 Redux 应用程序的两个地方:

  • 在 slice 文件中,与 reducer 逻辑一起
  • 在组件文件中,在组件外部,或在 useSelector 中直接定义

6.2 useSelector( hook 函数,用于在函数式组件中实现数据自动更新)

useSelector 接收一个 selector 函数。selector 函数接收 Redux store 的 state 作为其参数,然后从 state 中取值并返回。

根据Redux官方文档,使用useSelector钩子时,如果selector函数只有一个参数state,则可以直接省略传入参数,因为useSelector会自动将state作为参数传入selector函数中。而如果selector函数有多个参数,则需要手动传入所有参数。

useSelector的作用是在函数式组件中使用 Selector,用于监听 Redux Store 中数据的变化,当 Store 中存储的数据发生改变时,自动重新调用组件进行渲染。

那么它是如何判断数据发生改变呢?使用了===全等,数据如果没变,只是引用地址发生了变化,也会重新渲染。(比如map()或filter()总是返回一个新数组的引用)

const result: any = useSelector(selector: Function, equalityFn?: Function)

useSelector()是React Redux中的一个hook,用于从Redux store中选取state并订阅其变化。第一个参数selector是一个函数,用于选中需要订阅的state。第二个参数equalityFn是一个可选的函数,用于比较前后两次选中的state是否相等。如果不传入equalityFn,useSelector()会使用===运算符进行比较。如果传入了equalityFn,则会使用该函数进行比较。如果前后两次选中的state相等,useSelector()将不会触发组件重新渲染。如果不传入equalityFn,useSelector()将会对选中的整个state进行浅比较,如果传入了equalityFn,则会使用该函数进行比较。

6.3 createSelector (带缓存的 Selector 函数)

const selectTodoIds = state => state.todos.map(todo => todo.id)

useSelector(selectTodoIds) 将总是造成重渲染,因为返回的是一个新数组引用!组件一定会重新渲染,造成性能浪费。

  1. createSelector 接受两个参数:
    • 第一个参数input selector是一个收集依赖数组(不写数组,就按单独的参数分别传入),包含了所有需要 memoize 缓存的 selector。如果这些selector的输入参数没有变化,那么就会返回上一次计算的结果,从而避免了不必要的计算。
    • 第二个参数output selector是一个函数,用于计算新的值。这个函数的入参分别是input selector中selector的输出值,如果任何一个输出===比较厚 不同,它将重新运行 output selector

“input selector”通常应该只提取和返回值,而“output selector”应该完成转换工作。

// memoize缓存函数工作原理
function memoize(fn) {
  const cache = {};
  return function (...args) {
    const key = JSON. stringify(args) ;
    if (key in cache) {
    return cache [key];
    }
    cache [key] = fn(...args) ;
    return cache[key];
  }
}
  1. createSelector 的几个注意点
    • 只记忆最近的一组参数,通过上面的缓存函数工作原理应该就可以理解
    • 所有“input selector”都应该接收相同类型的参数
    • createSelector 可以嵌套使用
    • 如果需要给output selector 传递额外的参数,需要额外定义一个input selector 直接返回这个值
    • createSelector 的默认缓存每个 Selector 的唯一实例,使用一个selector工厂函数,它可以在每次调用时生成一个新的selector实例
// 如果需要给output selector 传递额外的参数,需要额外定义一个input selector 直接返回这个值
const selectItemsByCategory = createSelector(
  [
    // 通常的第一个输入 - 从 state 中提取值
    state => state.items,
    // 获取第二个参数,`category`,并转发到 output selector
    (state, category) => category
  ],
  // Output selector 拿到 (`items, category)` 参数
  (items, category) => items.filter(item => item.category === category)
)
// 这个新函数可以被多次调用,每次调用都会返回一个新的选择器函数,这些函数都可以独立地操作它们自己的 items 和 category 参数。因此,这个函数符合工厂函数的设计模式,它可以帮助我们更轻松地创建和管理对象。 
const makeSelectItemsByCategory = () => {
  const selectItemsByCategory = createSelector(
    [state => state.items, (state, category) => category],
    (items, category) => items.filter(item => item.category === category)
  )
  return selectItemsByCategory
}

6.4 和Selector​相关的一些知识点

  1. 调用 Selector传递额外的参数

    使用useSelector调用Selector函数时,只能传递一个参数,即Redux根state。如果需要传递其他参数,可以将一个匿名Selector函数传递给useSelector,然后在该函数内部调用真正的Selector函数并传递所需的参数。在下面的代码示例中,selectTodoById是真正的Selector函数,可以通过将匿名Selector函数传递给useSelector来传递todoId参数。

import { selectTodoById } from './todosSlice'

function TodoListitem({ todoId }) {
  // 从作用域中捕获 `todoId`,获取 `state` 作为参数,并转发两者
  // 到实际的 Selector 函数来提取结果
  const todo = useSelector(state => selectTodoById(state, todoId))
}
  1. 创建唯一的 Selector 实例

    在许多情况下,需要在多个组件中重用 Selector 函数。如果组件都将使用不同的参数调用 Selector ,它将破坏记忆 - Selector 永远不会连续多次看到相同的参数,因此永远不会返回缓存值。

import { makeSelectItemsByCategory } from './categoriesSlice'

function CategoryList({ category }) {
  // 在挂载时为每个组件实例创建一个新的记忆化 Selector
  const selectItemsByCategory = useMemo(makeSelectItemsByCategory, [])

  const itemsByCategory = useSelector(state =>
    selectItemsByCategory(state, category)
  )
}
  1. 最好将可重用的 Selector 与相应的 reducer 一起定义,而不是定义在组件里。

  2. 适度使用 Selector 和 记忆化

    不要给每个字段定义Selector。始终返回一致的引用和派生数据,但是返回一致的结果 不要使用记忆化。遇到map和filter 每次调用返回新的引用可以shying记忆化。

  3. Redux 中 selector 中分为 整体式 Selector 和 局部式 Selector

    • “整体式” Selector 返回状态树中某一部分,入参通常只有一个,整个state。返回通常不会变,所以要用createSelector作缓存
    • “局部式” Selector 获取特定区域的状态,入参通常包含一个或多个,根据参数计算返回,不作缓存。
// "整体式 Globalized" - 只接受state作为参数,作缓存
import { createSelector } from "reselect";
const selectUsers = state => state.users;
const selectActiveUser = createSelector(
  selectUsers,
  users => users.activeUser
);

// "局部式 Localized" - 接受多个参数,通常不做缓存
const selectUsersByEmail = (state, email) => {
  const { userList } = state.users;
  return userList.find(user => user.email === email);
};

Redux Toolkit 的 createEntityAdapter API 就是这种模式的一个例子。如果你调用 todosAdapter.getSelectors(),不带参数,它会返回一组“局部式” Selector ,这些 Selector 接收 slice state 作为它们的参数。如果你调用 todosAdapter.getSelectors(state => state.todos),它会返回一组“全球化” Selector ,这些 Selector 期望以 Redux 根 state 作为参数来调用。

7. createEntityAdapter(管理状态)

当使用Redux(或其它类似状态管理库)管理应用程序的状态时,它通常是由多个实体组成的,例如用户、博客、注释等。createEntityAdapter的作用就是对这些实体进行管理。

它提供了一个方便的接口来操作实体的状态,并且可以方便地选择和查询实体。例如,通过该API,我们可以轻松地添加、更新和删除实体,并支持按照多种条件查询实体
使用 createEntityAdapter 部分 参考资料

7.1 理解Entity

createEntityAdapter 是 Redux Toolkit 中的一个函数,它的作用是创建一个用于管理实体entity 的适配器 adapter。这个 adapter 可以帮助我们快速地创建 reducer 和 selector,从而简化了 Redux 的开发流程。

createEntityAdapter 的原理是基于一个名为 createSlice 的函数实现的。它会根据我们提供的实体 schema,自动生成一些常用的 reducer 和 selector。这些 reducer 和 selector 可以帮助我们快速地对实体进行 CRUD 操作,从而简化了我们的代码。

7.2 createEntityAdapter 使用

  1. createEntityAdapter 预置的几个reducer函数。

    • addOne / addMany:向 state 添加新 items
    • upsertOne / upsertMany:添加新 items 或更新现有 items
    • updateOne / updateMany:通过提供部分值更新现有 items
    • removeOne / removeMany:根据 ID 删除 items
    • setAll:替换所有现有 items
  2. 创建createEntityAdapter时传入对象,它接受一个对象作为参数,该对象应该具有以下属性:selectId、sortComparer和name。这些属性用于指定如何选择和排序实体的唯一标识符以及它们是如何排序的,以及用于标识将用于管理实体状态的部分。

    • selectId:用于从实体对象中提取唯一标识符的函数。
    • sortComparer:用于在添加、更新或删除实体时排序实体列表的函数。
    • name:用于标识将用于管理实体状态的部分的名称。
// 例如,假设您具有以下用户实体:
{
  id: 'user-123',
  name: 'Alice',
  age: 32,
}
// 如果您的应用程序需要使用 id 属性来标识用户对象,那么您可以将以下函数用于 selectId:
const usersAdapter = createEntityAdapter({
  selectId: user => user.id,
});
// 例如,假设您的应用程序需要按年龄对用户进行排序:
const usersAdapter = createEntityAdapter({
  selectId: user => user.id,
  sortComparer: (a, b) => b.age - a.age,
});
// 例如,假设您的应用程序需要管理用户实体的状态:
const usersAdapter = createEntityAdapter({
  selectId: user => user.id,
  sortComparer: (a, b) => b.age - a.age,
  name: 'users',
});


  1. getInitialState :返回一个类似于 { ids: [], entities: {} } 的对象,用于存储 item 的标准化 state 以及包含所有 item ID 的数组
  2. getSelectors:生成一组标准的 selector 函数

下面是一个示例代码,展示了如何使用 createEntityAdapter:

import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'

const todosAdapter = createEntityAdapter({
  selectId: todo => todo.id,
  sortComparer: (a, b) => a.priority - b.priority,
})

const todosSlice = createSlice({
  name: 'todos',
  initialState: todosAdapter.getInitialState(),
  reducers: {
    todoAdded: todosAdapter.addOne,
    todoRemoved: todosAdapter.removeOne,
    todoUpdated: todosAdapter.updateOne,
    todosReceived: todosAdapter.setAll,
  },
})

export const {
  selectAll: selectAllTodos,
  selectById: selectTodoById,
  selectIds: selectTodoIds,
} = todosAdapter.getSelectors(state => state.todos)

export const { todoAdded, todoRemoved, todoUpdated, todosReceived } = todosSlice.actions

export default todosSlice.reducer

在这个示例中,我们使用 createEntityAdapter 创建了一个名为 todosAdapter 的 adapter。这个 adapter 接受两个参数:selectId 和 sortComparer。它会根据这两个参数自动生成一些常用的 reducer 和 selector,从而简化了我们的代码。

然后,我们使用 createSlice 创建了一个名为 todosSlice 的 slice。这个 slice 接受三个参数:name、initialState 和 reducers。它会根据这三个参数创建一个 reducer,这个 reducer 包含了 todoAdded、todoRemoved、todoUpdated 和 todosReceived 四个 action。

最后,我们使用 todosAdapter.getSelectors 创建了三个 selector:selectAllTodos、selectTodoById 和 selectTodoIds。它们可以帮助我们快速地查询 todos。

8. RTK Query(最新的解决方案,替换 createAsyncThunk 和 createSlice)

  • RTK Query 是 Redux Toolkit 中包含的数据获取和缓存解决方案
    • RTK Query 为你抽象了管理缓存服务器数据的过程,无需编写加载状态、存储结果和发出请求的逻辑
    • RTK Query 建立在 Redux 中使用的相同模式之上,例如异步 thunk

为什么要使用RTK Query?

  1. 根据不同的加载状态显示不同UI组件
  2. 减少对相同数据重复发送请求
  3. 使用乐观更新,提升用户体验
  4. 在用户与UI交互时,管理缓存的生命周期
  5. 进一步优化createAsyncThunk 与 createSlice繁琐的手动操作
  6. 提供了自动缓存、重试、轮询等功能,大大简化了应用程序中处理网络请求的方式。

RTK优秀学习项目
Redux 学习项目
redux 学习笔记1
redux 学习笔记2
RTK Query
〖Redux〗、〖ReduxToolkit〗

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

// 定义我们的单个 API Slice 对象
export const apiSlice = createApi({
  // 缓存减速器预计将添加到 `state.api` (已经默认 - 这是可选的)
  reducerPath: 'api',
  // baseUrl 是 API 的起始 URL,而 prepareHeaders 函数则可以用来设置任意的 HTTP 请求头部信息
  baseQuery: fetchBaseQuery({ 
    baseUrl: '/fakeApi',
    prepareHeaders: (headers, { getState }) => {
      const token = getState().auth.token;
      if (token) {
        headers.set('authorization', `Bearer ${token}`);
      }
      return headers;
    },
  }),
  // “endpoints” 代表对该服务器的操作和请求
  endpoints: builder => ({
    // `getPosts` endpoint 是一个返回数据的 “Query” 操作
    getPosts: builder.query({
      // 通过定义一个 query 选项来提供 URL 路径的剩余部分。请求的 URL 是“/fakeApi/posts”
      query: () => '/posts'
    }),
	  getPost: builder.query({
      query: postId => `/posts/${postId}`
    }),
    addNewPost: builder.mutation({
      query: initialPost => ({
        url: '/posts',
        method: 'POST',
        // 请求体参数
        body: initialPost
      })
    })
  })
})

// 这里我们的 query 选项返回一个包含 {url, method, body} 的对象。 由于我们使用 fetchBaseQuery 来发出请求,body 字段将自动为我们进行 JSON 序列化。

// 为 `getPosts` Query endpoint 导出自动生成的 hooks
export const { useGetPostsQuery, useGetPostQuery, useAddNewPostMutation } = apiSlice

8.1 createApi(管理网络请求)

  • RTK Query 对每个应用程序使用单个 “API slice”,使用 createApi 定义
    • RTK Query 提供与 UI 无关和特定于 React 的 createApi 版本
    • API slice 为不同的服务器操作定义了多个“请求接口”
    • 如果使用 React 集成,API slice 包括自动生成的 React hooks(自动生成action 和 reducer)
  1. createApi参数

    • reducerPath: 必选参数,它为生成的 reducer 定义了预期的顶级状态 slice 字段。可以通过 reducerPath 在 Redux store 中查找和访问对应的 action 和 reducer。
      • 这个reducerPath相当于我们 createSlice配置中的name
      • 如果不提供 reducerPath 选项,则默认为 'api',因此你的所有 RTKQ 缓存数据都将存储在 state.api 下。
    • baseQuery: 必选参数,通过传入fetchBaseQuery包装函数,指定用于处理请求和响应的处理。
      • 我们可以在fetchBaseQuery函数传入所有请求的基本 URL,以及覆盖修改请求标头等行为,例如 baseUrl 或 headers 等。
    • endpoints: 必选参数,对该服务器的操作和请求。
      • 回调函数接受 builder 参数并返回一个对象,该对象包含使用 builder.query() 和 builder.mutation() 创建的请求接口定义。
      • query: 返回用于缓存的数据;mutation: 向服务器发送数据更新。
    • tagTypes: 可选参数,用于配置 RTK 响应状态处理的类型标签。
  2. 导出 API Slice 和 Hooks

RTK Query 的 React 集成会自动为我们定义的 每个 请求接口生成 React hooks! 这些 hooks 封装了在组件挂载时触发请求的过程,以及在处理请求和数据可用时重新渲染组件的过程。
我们可以从这个 API slice 文件中导出这些 hooks,以便在我们的 React 组件中使用。

  • use,任何 React hooks 的正常前缀
  • 请求接口名称,大写
  • 请求接口的类型,Query 或 Mutation
// 为 `getUserInfo` Query endpoint 和 'postUserInfo' Mutation endpoint 导出自动生成的 hooks
// 若请求的查询接口是  getUserInfo,  则自动生成的hooks命名 `useGetUserInfoQuery`
// 若请求的是更新接口是 postUserInfo,则自动生成hooks命名`usePostUserInfoMutation`
export const { useGetUserInfoQuery,usePostUserInfoMutation } = apiSlice
  1. API Slice 的缓存功能

为了使用 API Slice 的缓存功能,需要将它的 middleware 添加到 store 的 middleware 中。这可以通过 getDefaultMiddleware 方法来获取默认的 middleware 然后用 concat 连接 API Slice 的 middleware 完成,即 getDefaultMiddleware().concat(apiSlice.middleware)。

// 配置
import postsReducer from '../features/posts/postsSlice'
import usersReducer from '../features/users/usersSlice'
import notificationsReducer from '../features/notifications/notificationsSlice'
import { apiSlice } from '../features/api/apiSlice'

export default configureStore({
  reducer: {
    posts: postsReducer,
    users: usersReducer,
    notifications: notificationsReducer,
    [apiSlice.reducerPath]: apiSlice.reducer
  },
  middleware: getDefaultMiddleware =>
    getDefaultMiddleware().concat(apiSlice.middleware)
})

RTK Query 为每个唯一的请求接口 + 参数组合创建一个 “cache key”,并分别存储每个 cache key 的结果。这意味着你可以多次使用同一个 Query hooks ,传递不同的查询参数,每个结果将单独缓存在 Redux 存储中。

还需要注意的是查询参数必须是 单一 值!如果需要传递多个参数,则必须传递一个包含多个字段的对象(与createAsyncThunk完全相同)。 RTK Query 将对字段进行浅稳定比较,如果其中任何一个发生更改,则重新获取数据。

8.2 Query hooks

  • 查询请求接口允许从服务器获取缓存数据
    • Query Hooks 返回一个 “data” 值,以及加载状态标志
    • 查询可以手动重新获取,或者使用标签自动重新获取缓存失效

Query查询接口类型Hook 会返回一个对象 { data: posts, isLoading, isSuccess, isError, error }
但是,如果你使用的是 TypeScript,你可能需要保持原始对象不变,并在条件检查中将标志引用为 result.isSuccess,以便 TS 可以正确推断 data 是有效的。

  • data:来自服务器的实际响应内容。 在收到响应之前,该字段将是 “undefined”。
  • isLoading: 一个 boolean,指示此 hooks 当前是否正在向服务器发出 第一次 请求。(请注意,如果参数更改以请求不同的数据,- isLoading 将保持为 false。)
  • isFetching: 一个 boolean,指示 hooks 当前是否正在向服务器发出 any 请求
  • isSuccess: 一个 boolean,指示 hooks 是否已成功请求并有可用的缓存数据(即,现在应该定义 data)
  • isError: 一个 boolean,指示最后一个请求是否有错误
  • error: 一个 serialized 错误对象

定义Query hooks

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  endpoints: builder => ({
    getPosts: builder.query({
      query: () => '/posts'
    }),
    getPost: builder.query({
      query: postId => `/posts/${postId}`
    })
  })
})

export const { useGetPostsQuery, useGetPostQuery } = apiSlice

查询所有并排序

// omit setup
export const PostsList = () => {
  const {
    data: posts = [],
    isLoading,
    isSuccess,
    isError,
    error
  } = useGetPostsQuery()

  const sortedPosts = useMemo(() => {
    const sortedPosts = posts.slice()
    // Sort posts in descending chronological order
    sortedPosts.sort((a, b) => b.date.localeCompare(a.date))
    return sortedPosts
  }, [posts])

  let content

  if (isLoading) {
    content = <Spinner text="Loading..." />
  } else if (isSuccess) {
    content = sortedPosts.map(post => <PostExcerpt key={post.id} post={post} />)
  } else if (isError) {
    content = <div>{error.toString()}</div>
  }

  return (
    <section className="posts-list">
      <h2>Posts</h2>
      {content}
    </section>
  )
}

查询单个

// omit setup
/* 
  RTK Query 为每个唯一的请求接口 + 参数组合创建一个 “cache key”,并分别存储每个 cache key 的结果。这意味着你可以多次使用同一个 Query hooks ,传递不同的查询参数,每个结果将单独缓存在 Redux 存储中。
  还需要注意的是查询参数必须是 单一 值!如果需要传递多个参数,则必须传递一个包含多个字段的对象(与createAsyncThunk完全相同)。 RTK Query 将对字段进行浅稳定比较,如果其中任何一个发生更改,则重新获取数据。
*/
export const SinglePostPage = ({ match }) => {
  const { postId } = match.params

  const { data: post, isFetching, isSuccess } = useGetPostQuery(postId)

  let content
  if (isFetching) {
    content = <Spinner text="Loading..." />
  } else if (isSuccess) {
    content = (
      <article className="post">
        <h2>{post.title}</h2>
        <div>
          <PostAuthor userId={post.user} />
          <TimeAgo timestamp={post.date} />
        </div>
        <p className="post-content">{post.content}</p>
        <ReactionButtons post={post} />
        <Link to={`/editPost/${post.id}`} className="button">
          Edit Post
        </Link>
      </article>
    )
  }

  return <section>{content}</section>
}

8.3 Mutation hooks

  • Mutation 请求接口允许更新服务器上的数据
    • Mutation hooks 返回一个发送更新请求的“触发”函数,以及加载状态
    • 触发函数返回一个可以解包并等待的 Promise

Mutation hooks 返回一个包含两个值的数组:

  • 第一个值是thunk函数。
  • 第二个值是一个对象,会返回一个对象 { data: posts, isLoading, isSuccess, isError, error, fulfilled },其中fulfilled表示异步操作成功后要执行的回调。。

mutation 型的 Hook 相较于 query 型的 Hook,多了一个 fulfilled 属性,表示异步操作成功后将要被执行的回调函数(fulfilled 回调)。每个成功完成的 mutation 都会调用该回调。您可以使用 extraReducers 选项,将一个或多个 reducer 函数链接到 mutation revert(撤消)完成后将要被执行的回调函数(revert 回调)。

需要注意的是,fulfilled 回调可以通过 payload 参数访问异步操作完成后的结果。例如,如果您调用 createUserMutation.mutate({ username: "user1", password: "passwd" }) 并且操作成功,此时 createUserMutation.fulfilled 回调的参数 payload 就是 createUserMutation.data,表示 mutation 完成后的数据。

因此,您可以使用 fulfilled 回调执行任何后续操作,例如 dispatch 一个 action、更新组件的状态等。

提交数据

/* 
  这里我们的 query 选项返回一个包含 {url, method, body} 的对象。 
  由于我们使用 fetchBaseQuery 来发出请求,body 字段将自动为我们进行 JSON 序列化。
*/
export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  endpoints: builder => ({
    getPosts: builder.query({
      query: () => '/posts'
    }),
    getPost: builder.query({
      query: postId => `/posts/${postId}`
    }),
    addNewPost: builder.mutation({
      query: initialPost => ({
        url: '/posts',
        method: 'POST',
        // Include the entire post object as the body of the request
        body: initialPost
      })
    })
  })
})

export const {
  useGetPostsQuery,
  useGetPostQuery,
  useAddNewPostMutation
} = apiSlice

提交数据

// omit setup
export const AddPostForm = () => {
  const [title, setTitle] = useState('')
  const [content, setContent] = useState('')
  const [userId, setUserId] = useState('')

  const [addNewPost, { isLoading }] = useAddNewPostMutation()
  const users = useSelector(selectAllUsers)

  const onTitleChanged = e => setTitle(e.target.value)
  const onContentChanged = e => setContent(e.target.value)
  const onAuthorChanged = e => setUserId(e.target.value)

  const canSave = [title, content, userId].every(Boolean) && !isLoading

  const onSavePostClicked = async () => {
    if (canSave) {
      try {
        await addNewPost({ title, content, user: userId }).unwrap()
        setTitle('')
        setContent('')
        setUserId('')
      } catch (err) {
        console.error('Failed to save the post: ', err)
      }
    }
  }
  // omit rendering logic
}

提交后手动刷新数据(通常不会这么做)

import React, { useMemo } from 'react'
import { Link } from 'react-router-dom'
import classnames from 'classnames'

// omit other imports and PostExcerpt

export const PostsList = () => {
  // Query hooks 结果对象包含一个 “refetch” 函数,我们可以调用它来强制重新获取。 
  const {
    data: posts = [],
    isLoading,
    isFetching,
    isSuccess,
    isError,
    error,
    refetch
  } = useGetPostsQuery()

  const sortedPosts = useMemo(() => {
    const sortedPosts = posts.slice()
    sortedPosts.sort((a, b) => b.date.localeCompare(a.date))
    return sortedPosts
  }, [posts])

  let content

  // Query hooks 有一个 isLoading 标志,如果这是第一个数据请求,则为 true
  if (isLoading) {
    content = <Spinner text="Loading..." />
  } else if (isSuccess) {
    const renderedPosts = sortedPosts.map(post => (
      <PostExcerpt key={post.id} post={post} />
    ))
    // 以及一个 isFetching 标志,当任意数据请求正在进行时为true
    // 我们可以查看 isFetching 标志,并在重新获取过程中再次用加载微调器替换整个帖子列表。
    const containerClassname = classnames('posts-container', {
      disabled: isFetching
    })

    content = <div className={containerClassname}>{renderedPosts}</div>
  } else if (isError) {
    content = <div>{error.toString()}</div>
  }

  return (
    <section className="posts-list">
      <h2>Posts</h2>
      // 手动刷新数据
      <button onClick={refetch}>Refetch Posts</button>
      {content}
    </section>
  )
}

通过标签,实现缓存失效自动刷新
RTK Query 让我们定义查询和 mutations 之间的关系,以启用自动数据重新获取,使用标签。
标签是一个字符串或小对象,可让你命名某些类型的数据和缓存的 无效 部分。
当缓存标签失效时,RTK Query 将自动重新获取标记有该标签的请求接口。

  • API slice 对象中的根字段 tagTypes ,声明一组字符串标签数组,例如 'Post'
  • Query 查询请求接口中的 providesTags 数组,列出了一组描述该查询中数据的标签
  • Mutation 请求接口中的 invalidatesTags 数组,列出了每次 Mutation 将一组标签query查询请求过期,触发重新获取数据(刷新缓存)
export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  // 定义标签,这将帮助识别和处理与数据相关联的所有标签
  tagTypes: ['Post'],
  endpoints: builder => ({
    getPosts: builder.query({
      query: () => '/posts',
      // 使用 providesTags,API slice 将能够跟踪与查询关联的标签(缓存起来),并可以在需要时对其进行快速访问
      // 能够将查询结果缓存,并使用指定的标签进行标记,使该查询结果能够与其他使用相同标签的查询结果进行关联,以实现更快速的重用和更快速的查询。
      providesTags: ['Post']
    }),
    getPost: builder.query({
      query: postId => `/posts/${postId}`
    }),
    addNewPost: builder.mutation({
      query: initialPost => ({
        url: '/posts',
        method: 'POST',
        body: initialPost
      }),
      // 当 mutation 触发时,API slice 会根据 invalidatesTags 中列出的标签强制刷新缓存中相关标签的查询接口。
      // 它会调用与该标签相关联的所有query查询,并强制它们重新获取最新数据,以便您在应用程序中使用最新的数据。
      invalidatesTags: ['Post']
    })
  })
})

8.4 RTK Query 高级查询模式

  • 如何使用带有 ID 的标签来管理缓存失效和重新获取
  • 如何在 React 之外使用 RTK 查询缓存
  • 处理响应数据的技术
  • Implementing optimistic updates and streaming updates

8.4.1 缓存数据订阅生命周期

RTK Query 允许多个组件订阅相同的数据,并且将确保每个唯一的数据集只获取一次。 在内部,RTK Query 为每个请求接口 + 缓存键组合保留一个 action 订阅的引用计数器。不同组件里使用相同参数调用将返回完全相同的结果,包括获取的 “data” 和加载状态标志。

RTK Query 会启动一个内部计时器。 如果在添加任何新的数据订阅之前计时器到期,RTK Query 将自动从缓存中删除该数据,因为应用程序不再需要该数据。但是,如果在计时器到期之前添加了新订阅,则取消计时器,并使用已缓存的数据而无需重新获取它。

默认情况下,未使用的数据会在 60 秒后从缓存中删除,根 API Slice 定义中进行配置,也可以使用 keepUnusedDataFor 标志在各个请求接口定义中覆盖,该标志指定缓存生存期 秒。

举例,当我们在A页面请求数据,当我们切换到B页面,此时A页面卸载了,活动订阅删除,RTK Query 立即启动 “remove this post data” 计时器。但是B页面立即挂载并并使用相同的缓存键订阅相同的 Post 数据(默认60秒内)。因此,RTK Query 取消了计时器并继续使用相同的缓存数据,而不是从服务器获取数据。

8.4.2 特定的缓存标签可用于更细粒度的缓存失效(选择性更新)

  • 缓存标签可以是 'Post' 或
  • 请求接口可以根据结果和 arg 缓存键提供或使缓存标记无效

之前在8.3 中,我们是如何解决,mutation完自动刷新query这个问题的。我们在query中 providesTags 一个标签,在mutation中 invalidatesTags 一个标签。这其中会使用整个query缓存失效强制刷新整个列表,但其实我们一次只是更新列表中的一项。

那么有没有一种方法,能选择性更新,RTK Query 让我们可以定义特定的标签,这让我们在无效缓存方面(强制更新)更有选择性

之前在Query Hook 中定义了一个 providesTags 字段,它是一个字符串数组。providesTags也可以定义为一个函数,有3个参数,并返回一个数组。这允许我们根据正在获取的数据的 ID 创建标签条目。同样,invalidatesTags 也可以是回调。

  • result:接口返回的结果
  • error:错误
  • arg:查询参数

关于选择性更新,必须要知道的前置知识是关于query和mutation标签的匹配规则。
对于 createApi 的 endpoints 中的 builder.query 和 builder.mutation 方法,两个方法中的标签机制是不同的,它们之间是互相独立的。具体来说,builder.query 方法中的标签会与 builder.mutation 方法中的标签分别缓存,并且两种方法的标签是不相交的。

  • builder.query,如果指定了字符串标签,则只有和此字符串标签名称相同的缓存数据会被无效,和对象形式标签没有关系。
  • builder.mutation,标签需要使用双重匹配方式,使用带有类型和 ID 属性的对象标签。对象形式标签的值可以是任意值,它们只需要具有相同的类型和 ID 属性才可能匹配缓存中的数据。

因此,在 builder.mutation 方法中,不能使用字符串标签,因为这样将不会和缓存中的数据精确匹配,从而无法保证缓存的正确性。

// 请看下面示例如何正确的使用标签达到选择性更新。
// RTK Query 知道当我们进行编辑并且具有该 ID 的特定标签缓存无效时,它需要重新获取单个帖子和帖子列表
export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  // 定义Post标签
  tagTypes: ['Post'],
  endpoints: builder => ({

    getPosts: builder.query({
      query: () => '/posts',
      providesTags: (result = [], error, arg) => [  // 使用默认的参数-空数组,安全处理返回值
        // ①我们通常不这么做,仅为整个列表提供一个通用的 'Post' 标签。
        // 'Post',
        // ②推荐这么做,为整个列表提供通用 'Post' 标签并使其具有任意 ID 的附加标签
        {type: 'Post', id: 'LIST'},
        {type: 'Post', id: 'ALL'},
        // 以及为每个接收到的帖子对象提供一个特定的 {type: 'Post', id} 标签
        ...result.map(({ id }) => ({ type: 'Post', id }))
      ]
    }),
    getPost: builder.query({
      query: postId => `/posts/${postId}`,
      // 为单个 post 对象提供特定的 {type: 'Post', id} 标签
      providesTags: (result, error, arg) => [{ type: 'Post', id: arg }]
    }),

    addNewPost: builder.mutation({
      query: initialPost => ({
        url: '/posts',
        method: 'POST',
        body: initialPost
      }),
      // ①我们通常不这么做,使一般的'Post'标签缓存无效,重新获取整个列表。
      // invalidatesTags: ['Post']
      // ②使具有任意 ID 的附加标签无效,强制重新获取 getPosts 请求接口的帖子列表
      invalidatesTags: [{type: 'Post', id: 'ALL'}]
    }),
    editPost: builder.mutation({
      query: post => ({
        url: `posts/${post.id}`,
        method: 'PATCH',
        body: post
      }),
      // 使特定的 {type: 'Post', id} 标签缓存无效,强制更新。
      // 这将强制重新获取 “getPost” query接口中的单个帖子
      // 以及 “getPosts” query接口中的整个帖子列表,因为它们都提供了与 “{type, id}” 值匹配的标签。
      invalidatesTags: (result, error, arg) => [{ type: 'Post', id: arg.id }]
    })
  })
})

8.4.3 RTK Query 的 API 与 UI 无关,可以在 React 之外使用(endpoint.initiate())

  • 请求接口对象包括用于发起请求、生成结果 selectors 和匹配请求 action 对象的函数
  1. API slice 对象的结构

  2. endpoints 字段里定义的接口都有一个请求接口对象,每个请求接口对象包含

  • 我们从根 API slice 对象导出的相同主 query/mutation hooks,但命名为 useQuery 或 useMutation
  • 对于查询请求接口,一组额外的 query hook 用于惰性查询或部分订阅等场景
  • 一组 "matcher" 实用程序 用于检查由对该请求接口的请求 dispatch 的 pending/fulfilled/rejected action
  • 触发对此请求接口的请求的 initiate thunk
  • 一个 select 函数,创建 memoized selectors,可以检索缓存的结果数据 + 此请求接口的状态条目
// 忽略部分依赖
import { apiSlice } from './features/api/apiSlice'

async function main() {
  // 启动 mock API server
  await worker.start({ onUnhandledRequest: 'bypass' })

  store.dispatch(apiSlice.endpoints.getUsers.initiate())

  ReactDOM.render(
    <React.StrictMode>
      <Provider store={store}>
        <App />
      </Provider>
    </React.StrictMode>,
    document.getElementById('root')
  )
}
main()

8.4.4 RTK query 使用记忆化、持久化select(endpoint.select()) ,

createEntityAdapter 创建的selector,刷新后将会丢失因为对应state 没有数据了。现在我们正在为 RTK Query 的缓存获取数据,我们应该将这些 selector 替换为从缓存中读取的等价物。

import { createSlice, createEntityAdapter,createSelector} from '@reduxjs/toolkit'
import { apiSlice } from '../api/apiSlice'

/* 暂时忽略适配器 - 我们很快会再次使用它
const usersAdapter = createEntityAdapter()
const initialState = usersAdapter.getInitialState()
*/

// API slice 请求接口中的 endpoint.select() 函数将在我们每次调用它时创建一个新的 memoized selector 函数。
// select() 将参数作为缓存键,并且这必须与你作为参数传递给 query hook 或 initiate() thunk 的缓存键相同。
// 生成的 selector 使用该缓存键来准确知道它应该从存储中的缓存状态返回哪个缓存结果。如果不传参数,缓存键变成undefiend。
export const selectUsersResult = apiSlice.endpoints.getUsers.select()

const emptyUsers = []

export const selectAllUsers = createSelector(
  selectUsersResult,
  usersResult => usersResult?.data ?? emptyUsers
)

export const selectUserById = createSelector(
  selectAllUsers,   // input selector
  (state, userId) => userId,  // 为了给output selector 传递userId参数
  (users, userId) => users.find(user => user.id === userId) // output selector
)

/* 暂时忽略 selector——我们稍后再讨论
export const {
  selectAll: selectAllUsers,
  selectById: selectUserById,
} = usersAdapter.getSelectors((state) => state.users)
*/

8.4.5 实现请求接口分包按需加载(injectEndpoints())

RTK Query 支持使用 apiSlice.injectEndpoints() 拆分请求接口定义。

import { apiSlice } from '../api/apiSlice'

// injectEndpoints() 改变原始 API slice 对象以添加额外的请求接口定义,然后返回它。
export const extendedApiSlice = apiSlice.injectEndpoints({
  endpoints: builder => ({
    getUsers: builder.query({
      query: () => '/users'
    })
  })
})

export const { useGetUsersQuery } = extendedApiSlice

export const selectUsersResult = extendedApiSlice.endpoints.getUsers.select()

此时,apiSlice 和 extendedApiSlice 是同一个对象,但在这里参考 extendedApiSlice 对象而不是 apiSlice 会有所帮助,提醒自己。(如果你使用的是 TypeScript,这一点更为重要,因为只有 extendedApiSlice 值具有为新请求接口添加的类型。)

8.4.6 响应可以根据需要以不同的方式进行转换

  • 请求接口可以定义一个 transformResponse 回调来在缓存之前修改数据
  • 可以给 Hooks 一个 selectFromResult 选项来提取/转换数据
  • 组件可以读取整个值并使用 useMemo 进行转换
  1. transformResponse

请求接口可以定义一个 transformResponse 处理程序,该处理程序可以在缓存之前提取或修改从服务器接收到的数据。

性能和规范化 中,我们讨论了将数据存储在归一化结构中有用的原因。特别是,它让我们可以根据 ID 查找和更新项目,而不必遍历数组来查找正确的项目。

我们的 selectUserById selector 当前必须遍历缓存的用户数组才能找到正确的 User 对象。如果我们要使用归一化方法转换要存储的响应数据,我们可以将其简化为直接通过 ID 查找用户。

我们之前在 usersSlice 中使用 createEntityAdapter 来管理归一化用户数据。现在可以将 createEntityAdapter 集成到我们的 extendedApiSlice 中,并实际使用 createEntityAdapter 在数据缓存之前对其进行转换。并且将取消注释我们最初拥有的 usersAdapter 行,并再次使用它的更新函数和 selector。

import { apiSlice } from '../api/apiSlice'

const usersAdapter = createEntityAdapter()

const initialState = usersAdapter.getInitialState()

export const extendedApiSlice = apiSlice.injectEndpoints({
  endpoints: builder => ({
    // 我们在 getUsers 请求接口添加了一个 transformResponse 选项。
    // 它接收整个响应数据体作为其参数,并应返回要缓存的实际数据。
    getUsers: builder.query({
      query: () => '/users',
      transformResponse: responseData => {
        // 通过调用usersAdapter.setAll(initialState, responseData),
        // 它将返回标准的 {ids: [],entities: {}} 归一化数据结构,包含所有接收到的项目。
        return usersAdapter.setAll(initialState, responseData)
      }
    })
  })
})

export const { useGetUsersQuery } = extendedApiSlice

// API slice 请求接口中的 endpoint.select() 函数将在我们每次调用它时创建一个新的 memoized selector 函数。
// select() 将参数作为缓存键,并且这必须与你作为参数传递给 query hook 或 initiate() thunk 的缓存键相同。
// 生成的 selector 使用该缓存键来准确知道它应该从存储中的缓存状态返回哪个缓存结果。如果不传参数,缓存键变成undefiend。
export const selectUsersResult = extendedApiSlice.endpoints.getUsers.select()

const selectUsersData = createSelector(
  selectUsersResult,
  usersResult => usersResult.data
)

// adapter.getSelectors() 函数需要给定一个输入 selector,以便它知道在哪里可以找到归一化数据。
// 在这种情况下,数据嵌套在 RTK Query cache reducer 中,因此我们从缓存状态中选择正确的字段。
export const { selectAll: selectAllUsers, selectById: selectUserById } =
  usersAdapter.getSelectors(state => selectUsersData(state) ?? initialState)
  1. selectFromResult

从旧的 postsSlice 读取的最后一个组件是 ,它根据当前用户过滤帖子列表。我们已经看到,可以使用 useGetPostsQuery() 获取整个帖子列表,然后在组件中对其进行转换,例如在 useMemo 中进行排序。query hooks 还使我们能够通过提供 selectFromResult 选项来选择缓存状态的片段,并且仅在所选片段更改时重新渲染。

我们可以使用 selectFromResult 让 从缓存中读取过滤后的帖子列表。然而,为了让 selectFromResult 避免不必要的重新渲染,我们需要确保我们提取的任何数据都被正确记忆。为此,我们应该创建一个新的 selector 实例, 组件可以在每次渲染时重用它,以便 selector 根据其输入来记忆结果。

我们在这里创建的 memoized selector 函数有一个关键的区别。通常,selector 期望整个 Redux state 作为它们的第一个参数,并从 state 中提取或派生一个值。但是,在这种情况下,我们只处理保存在缓存中的 “result” 值。result 对象内部有一个 “data” 字段,其中包含我们需要的实际值,以及一些请求元数据字段。

由于 result 保存在 Redux 存储中,我们不能改变它 - 我们需要返回一个新对象。query hooks 将对返回的对象进行浅层比较,并且仅在其中一个字段发生更改时才重新渲染组件 我们可以通过仅返回此组件所需的特定字段来优化重新渲染 - 如果我们不需要其余的元数据标志,我们可以完全省略它们。如果你确实需要它们,你可以传播原始的 result 值以将它们包含在输出中。

import { createSelector } from '@reduxjs/toolkit'

import { selectUserById } from '../users/usersSlice'
import { useGetPostsQuery } from '../api/apiSlice'

export const UserPage = ({ match }) => {
  const { userId } = match.params

  const user = useSelector(state => selectUserById(state, userId))

  // 通过每次调用 selectPostsForUser(result, userId),它会记住过滤后的数组,只有在获取的数据或用户ID发生变化时才会重新计算。
  const selectPostsForUser = useMemo(() => {
    const emptyArray = []
    // 返回此页面的唯一 selector 实例,以便
     // 过滤后的结果被正确记忆
    return createSelector(
      res => res.data,
      (res, userId) => userId,
      (data, userId) => data?.filter(post => post.user === userId) ?? emptyArray
    )
  }, [])

  // 使用相同的帖子查询,但仅提取其部分数据
  // 在这种情况下,我们将调用字段 “postsForUser”,我们可以从 hook 结果中解构这个新字段。
  const { postsForUser } = useGetPostsQuery(undefined, {
    // selectFromResult 回调从服务器接收包含原始请求元数据和 data 的 result 对象,并且应该返回一些提取或派生的值。
    // 因为 query hooks 为这里返回的任何内容添加了一个额外的 refetch 方法,所以最好始终从 selectFromResult 返回一个对象,其中包含你需要的字段。
    selectFromResult: result => ({
      // 我们可以选择在此处包含结果中的其他元数据字段
      ...result,
      // 在 hook 结果对象中包含一个名为 “postsForUser” 的字段,
       // 这将是一个过滤的帖子列表
      postsForUser: selectPostsForUser(result, userId)
    })
  })

  // omit rendering logic
}

8.4.7 Optimistic Updates​ 乐观更新

RTK Query 允许你通过基于请求生命周期处理程序修改客户端缓存来实现 Optimistic Updates。请求接口可以定义一个 onQueryStarted 函数,该函数将在请求开始时调用,我们可以在该处理程序中运行其他逻辑。

onQueryStarted 处理程序接收两个参数。第一个是请求开始时传递的缓存键 arg。 第二个是一个对象,它包含一些与 createAsyncThunk 中的 thunkApi 相同的字段({dispatch, getState, extra, requestId}),但也包含一个名为 queryFulfilled 的 Promise。这个 Promise 将在请求返回时解析,并根据请求执行或拒绝。

API slice 对象包含一个 updateQueryData 实用函数,可让我们更新缓存值。它需要三个参数:要更新的请求接口的名称、用于标识特定缓存数据的相同缓存键值以及更新缓存数据的回调。 updateQueryData 使用 Immer,因此你可以像在 createSlice 中一样“改变”起草的缓存数据。

export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/fakeApi' }),
  tagTypes: ['Post'],
  endpoints: builder => ({
    // 忽略部分依赖

    addReaction: builder.mutation({
      query: ({ postId, reaction }) => ({
        url: `posts/${postId}/reactions`,
        method: 'POST',
        // 在一个真实的应用程序中,我们可能需要以某种方式基于用户 ID
        // 这样用户就不能多次做出相同的反应
        body: { reaction }
      }),
      async onQueryStarted({ postId, reaction }, { dispatch, queryFulfilled }) {
        // `updateQueryData` 需要请求接口名称和缓存键参数,
        // 所以它知道要更新哪一块缓存状态
        const patchResult = dispatch(
          apiSlice.util.updateQueryData('getPosts', undefined, draft => {
            // `draft` 是 Immer-wrapped 的,可以像 createSlice 中一样 “mutated”
            const post = draft.find(post => post.id === postId)
            if (post) {
              post.reactions[reaction]++
            }
          })
        )
        // 默认情况下,我们期望请求会成功。如果请求失败,我们可以 await queryFulfilled,捕获失败,并撤消补丁更改以恢复 optimistic update。
        try {
          // 当我们 dispatch 该 action 时,返回值是一个 patchResult 对象
          await queryFulfilled
        } catch {
          // 调用 patchResult.undo(),它会自动 dispatch 一个操作来反转补丁差异更改。
          patchResult.undo()
        }
      }
    })
  })
})

8.4.8 RTK Query 具有用于操作缓存数据以获得更好用户体验的高级选项

  • onQueryStarted 生命周期可用于通过在请求返回之前立即更新缓存来进行 optimistic updates
  • onCacheEntryAdded 生命周期可用于通过基于服务器推送连接随时间更新缓存来进行流式更新
posted @ 2023-05-09 07:41  wanglei1900  阅读(1413)  评论(0编辑  收藏  举报