数据标准化
数据标准化
大多数应用程序通常会处理深度嵌套或关系型的数据。数据规范化的目标是有效地组织你状态下的数据。这通常是通过将集合存储为具有 id 的键的对象,同时存储这些 id 的排序数组来实现的。
手动标准化
归一化数据不需要任何特殊的库。下面是一个基本的例子,说明你如何对 fetchAll API 请求的响应进行归一化,该请求返回的数据是以{ users: [{id: 1, first_name: ’normalized’, last_name: ‘person’}] },使用一些手写的逻辑。
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import userAPI from "./userAPI";
export const fetchUsers = createAsyncThunk("users/fetchAll", async () => {
const response = await userAPI.fetchAll();
return response.data;
});
export const slice = createSlice({
name: "users",
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
// reduce the collection by the id property into a shape of { 1: { ...user }}
const byId = action.payload.users.reduce((byId, user) => {
byId[user.id] = user;
return byId;
}, {});
state.entities = byId;
state.ids = Object.keys(byId);
});
},
});
虽然我们有能力编写这段代码,但它确实会变得很重复,尤其是当你在处理多种类型的数据时。此外,这个例子只处理将条目加载到状态中,而不是更新它们。
normalizr
normalizr 是现有的一个流行的数据归一化库。你可以在没有 Redux 的情况下单独使用它,但它与 Redux 一起使用非常普遍。典型的用法是格式化来自 API 响应的集合,然后在你的 reducers 中处理它们。
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { normalize, schema } from "normalizr";
import userAPI from "./userAPI";
const userEntity = new schema.Entity("users");
export const fetchUsers = createAsyncThunk("users/fetchAll", async () => {
const response = await userAPI.fetchAll();
// Normalize the data before passing it to our reducer
const normalized = normalize(response.data, [userEntity]);
return normalized.entities;
});
export const slice = createSlice({
name: "users",
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.entities = action.payload.users;
state.ids = Object.keys(action.payload.users);
});
},
});
与手写版本一样,这并不处理向状态中添加额外的条目,或者稍后更新它们–它只是将收到的所有条目加载进来。
createEntityAdapter
Redux Toolkit 的 createEntityAdapter API 提供了一种标准化的方式,通过将一个集合放入 { ids: [], entities: {} }
. 除了这个预定义的状态形状,它还会生成一组知道如何处理数据的还原函数和选择器。
import {
createSlice,
createAsyncThunk,
createEntityAdapter,
} from "@reduxjs/toolkit";
import userAPI from "./userAPI";
export const fetchUsers = createAsyncThunk("users/fetchAll", async () => {
const response = await userAPI.fetchAll();
// In this case, `response.data` would be:
// [{id: 1, first_name: 'Example', last_name: 'User'}]
return response.data;
});
export const updateUser = createAsyncThunk("users/updateOne", async (arg) => {
const response = await userAPI.updateUser(arg);
// In this case, `response.data` would be:
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
return response.data;
});
export const usersAdapter = createEntityAdapter();
// By default, `createEntityAdapter` gives you `{ ids: [], entities: {} }`.
// If you want to track 'loading' or other keys, you would initialize them here:
// `getInitialState({ loading: false, activeRequestId: null })`
const initialState = usersAdapter.getInitialState();
export const slice = createSlice({
name: "users",
initialState,
reducers: {
removeUser: usersAdapter.removeOne,
},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany);
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
const { id, ...changes } = payload;
usersAdapter.updateOne(state, { id, changes });
});
},
});
const reducer = slice.reducer;
export default reducer;
export const { removeUser } = slice.actions;