Redux Toolkit 调用 API 的四种方式
上篇笔记写的比较乱,加上这回又学了点新的东西,所以重新整合一下。
本地 API 用的是 json-server,端口设置在 3005,数据包含:
{
"users": [
{
"id": 1,
"name": "Myra"
},
{
"name": "Pete Bernhard",
"id": 9
},
{
"name": "Bert Block",
"id": 10
},
{
"name": "Rachael Bayer",
"id": 11
}
]
}
基础写法
这种方法利用了 Redux Toolkit 现在会将同名的 reducers 与 actions 进行归并,对比使用 redux 不需要额外进行 actions 的声明,因此减少了代码量。
副作用的触发和逻辑放在 Component 中,优化方式可以通过写对应的 hooks 降低代码的重复率,或者使用 thunk。
-
slice
与原生的 Redux 相比,RTK 新定义了一个 slice,将 reducers、actions、state 放入了 slice 中进行管理:
import { createSlice } from '@reduxjs/toolkit'; export const userSliceNative = createSlice({ name: 'users', initialState: { data: [], isLoading: false, error: null, }, reducers: { fetchingData(state) { state.isLoading = true; }, fatchingDataCompleted(state) { state.isLoading = false; }, setData(state, action) { state.data = action.payload; }, setError(state, action) { state.data = action.error; }, }, });
-
store:
store 的整合也较为简单,这里将 actions 放入了 store 中,这样其他组件可以直接从 store 中进行导入,非必需。
import { configureStore } from '@reduxjs/toolkit'; import logger from 'redux-logger'; import { userSliceNative } from './slices/userSliceNative'; export const store = configureStore({ reducer: { userNative: userSliceNative.reducer, }, }); export const userNativeActions = userSliceNative.actions;
-
react component:
Again,如果决定用这种写法了,useEffect 中的代码可以通过编写一个 custom hooks 进行一定程度上的优化,减少代码重复率。
import axios from 'axios'; import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { userNativeActions } from '../store'; const UserNative = () => { const { data, isLoading, error } = useSelector((state) => state.userNative); const dispatch = useDispatch(); useEffect(() => { dispatch(userNativeActions.fetchingData()); axios .get('http://localhost:3005/users') .then((res) => { dispatch(userNativeActions.setData(res.data)); }) .catch((err) => dispatch(userNativeActions.setError(err))) .finally(dispatch(userNativeActions.fatchingDataCompleted())); }, []); let content; if (isLoading) { content = 'User is loading'; } else if (error) { content = 'Load user fail'; } else { content = data.map((user) => <div key={user.id}>{user.name}</div>); } return <div>{content}</div>; }; export default UserNative;
thunk 实现
这里就是写了一个额外的 action creator,RTK 现在原生就支持 thunk 实现。
store 的配置和第一种写法一样,就不重复 cv 了。
-
slice:
import axios from 'axios'; // import from central store import { userNativeActions } from '..'; import { createSlice } from '@reduxjs/toolkit'; export const userSliceNative = createSlice({ name: 'users', initialState: { data: [], isLoading: false, error: null, }, reducers: { fetchingData(state) { state.isLoading = true; }, fatchingDataCompleted(state) { state.isLoading = false; }, setData(state, action) { state.data = action.payload; }, setError(state, action) { state.data = action.error; }, }, }); export const fetchUsersThunk = () => { return (dispatch) => { dispatch(userNativeActions.fetchingData()); axios .get('http://localhost:3005/users') .then((res) => { dispatch(userNativeActions.setData(res.data)); }) .catch((err) => dispatch(userNativeActions.setError(err))) .finally(dispatch(userNativeActions.fatchingDataCompleted())); }; };
-
component:
import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { fetchUsersThunk } from '../store/thunks/fetchUsersThunk'; const UserThunk = () => { const { data, isLoading, error } = useSelector((state) => state.userNative); const dispatch = useDispatch(); useEffect(() => { dispatch(fetchUsersThunk()); }, []); let content; if (isLoading) { content = 'User is loading'; } else if (error) { content = 'Load user fail'; } else { content = data.map((user) => <div key={user.id}>{user.name}</div>); } return <div>{content}</div>; }; export default UserThunk;
asyncThunk
asyncThunk 是 RTK 内部提供的一个对异步 thunk 的处理方式,优点在于 Promise 的状态是 RTK 内部进行管理的,不需要用户手动实现。
-
slice:
与完全靠用户手写一个 custom action creator,状态管理需要用户在 custom action creator 中判断并触发不太一样的一点就是,这里的 custom action creator 非常短,短到只需要返回调用成功时,reducer 中需要处理的结果。至于异步调用的 pending/fulfilled/rejected,这点基本由 RTK 进行管理。
在 slice 中的 extraReducers 中,只需要根据 RTK 管理的状态更新对应的数据即可。
import { createSlice } from '@reduxjs/toolkit'; const userSlice = createSlice({ name: 'users', initialState: { data: [], isLoading: false, error: null, }, extraReducers(builder) { // retrieve builder.addCase(fetchUsers.pending, (state, action) => { state.isLoading = true; }); builder.addCase(fetchUsers.fulfilled, (state, action) => { state.isLoading = false; state.data = action.payload; }); builder.addCase(fetchUsers.rejected, (state, action) => { state.isLoading = false; state.error = action.error; }); }, }); export const fetchUsers = createAsyncThunk('users/fetch', async () => { const response = await axios.get('http://localhost:3005/users'); return response.data; }); export const usersReducer = userSlice.reducer;
-
store:
store 的挂载方式也与之前的一样
import { configureStore } from '@reduxjs/toolkit'; import { usersReducer } from './slices/usersSlice'; import logger from 'redux-logger'; import { userSliceNative } from './slices/userSliceNative'; export const store = configureStore({ reducer: { userNative: userSliceNative.reducer, users: usersReducer, }, middleware: (getDefaultMiddleware) => { return getDefaultMiddleware().concat(logger); }, });
-
component:
关于 dispatch、useState 这块代码也可以另外封装一个 custom hooks 去进行实现。使用 asyncThunk 的封装应该相对而言更加容易,毕竟 Promise 的三个状态都是有 RTK 进行定义和管理的,基本上可以保证一致性。
function UsersList() { const [isLoadingUsers, setIsLoadingUsers] = useState(false); const [loadingUsersError, setLoadingUsersError] = useState(null); const dispatch = useDispatch(); const { data } = useSelector((state) => state.users); useEffect(() => { setIsLoadingUsers(true); dispatch(fetchUsers()) .unwrap() .catch((err) => setLoadingUsersError(err)) .finally(() => setIsLoadingUsers(false)); }, []); let content; if (isLoadingUsers) { content = 'User is loading'; } else if (loadingUsersError) { content = 'Load user fail'; } else { content = data.map((user) => <div key={user.id}>{user.name}</div>); } return <div>{content}</div>; }
对比 与前两者:
前两者对于 Promise 处理的 reducers 还是属于手动的,使用 asyncThunk 则显得更加的规范化,不过保存的数据格式倒是一致的:
Redux Toolkit Query
RTKQ 有点对标 React Query,都是对 API 进行 cache 的解决方案,基础的实现方法如下:
-
slice/api:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'; const userApi = createApi({ reducerPath: 'users', // pre-configured fetch baseQuery: fetchBaseQuery({ baseUrl: 'http://localhost:3005', }), endpoints(builder) { return { fetchUsers: builder.query({ query: (user) => { return { url: '/users', method: 'GET', }; }, }), }; }, }); export const { useFetchUsersQuery } = userApi; export { userApi };
-
store:
import { configureStore } from '@reduxjs/toolkit'; import logger from 'redux-logger'; import { userApi } from './apis/userApi'; export const store = configureStore({ reducer: { [userApi.reducerPath]: userApi.reducer, }, middleware: (getDefaultMiddleware) => { return getDefaultMiddleware().concat(userApi.middleware).concat(logger); }, });
-
component:
import React from 'react'; import { useFetchUsersQuery } from '../store/apis/userApi'; const UserRTK = () => { const { data, isFetching, error } = useFetchUsersQuery(); let content; if (isFetching) { content = 'User is loading'; } else if (error) { content = 'Load user fail'; } else { content = data.map((user) => <div key={user.id}>{user.name}</div>); } return <div>{content}</div>; }; export default UserRTK;
RTKQ 内部的管理流程大致如下:
middleware 显示 register 对应的 Query,接着就像 Thunk2 一样,内部对 pending/fulfilled/rejected 的状态进行管理,这里也能看到一个 internalSubscriptions 的存在。
这里只是一个简单的 Fetch,并不包含 CRUD,不过需要注意的是,拉数据是调用 builder.query
进行创建,而其他会对数据库产生影响的作用则使用 builder.mutate
进行创建。
除此之外,如果当前的 API 没有被触发,那么对应的数据并不会存在(因为没有被 cache),换言之如果我还有一个 product 的 slice/api,只是在用户页面没有调用 product 的 RTKQ,那么 product 就不会存在于 state 中。
RTKQ 只所以能够被称之为是一个对 query 的解决方案,也是因为通过一些配置就能够实现一些比较麻烦的功能:
而且 RTKQ 内部还有一个 tagging system,RTKQ 在 Fetch 阶段可以提供一个 tag,其内部会对这个 tag 进行缓存,在 mutation 阶段用户可以通过 invalidatesTags
对 tag 去无效华内部的 cache,当 RTKQ 察觉到 tag 的变化后,那么就会重新调用 query 去拉取最新的数据。
下一步大概就是研究一下 RTKQ 这个 tagging system 了,毕竟我还挺需要手动更新数据而不是让 RTKQ 去重新拉取数据(这个也是可以实现的)。