如何缓存我已经请求的数据并使用 React 和 Redux Toolkit 从商店访问它

IT技术 reactjs caching redux react-redux redux-toolkit
2021-05-13 22:00:59

如何使用 React Redux Toolkit 从商店获取数据并获取缓存版本(如果我已经请求)?

我需要请求多个用户,例如 user1、user2 和 user3。如果我在 user1 已经被请求之后提出请求,那么我不想再次从 API 中获取 user1。相反,它应该给我来自商店的 user1 的信息。

如何使用 Redux Toolkit 切片在 React 中执行此操作?

1个回答

工具

Redux Toolkit 可以帮助解决这个问题,但我们需要在工具包中组合各种“工具”。

React 与 Redux

我们将创建一个useUser自定义的 React 钩子来通过 id 加载用户。

我们需要在我们的钩子/组件中使用单独的钩子来读取数据 ( useSelector) 和启动一个 fetch ( useDispatch)。存储用户状态永远是 Redux 的工作。除此之外,在我们是否在 React 或 Redux 中处理某些逻辑方面还有一些余地。

我们可以看一下所选择的值user在自定义挂钩,只有dispatchrequestUser当动作userundefined或者,我们可以dispatch requestUser所有的时间和有requestUserthunk的检查,看是否需要做使用抓取condition的设置createAsyncThunk

基本方法

我们天真的方法只是检查用户是否已经存在于状态中。我们不知道是否有针对此用户的任何其他请求已在等待处理。

让我们假设你有一些函数接受一个 id 并获取用户:

const fetchUser = async (userId) => {
    const res = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
    return res.data;
};

我们创建一个userAdapter助手:

const userAdapter = createEntityAdapter();

// needs to know the location of this slice in the state
export const userSelectors = userAdapter.getSelectors((state) => state.users);

export const { selectById: selectUserById } = userSelectors;

我们创建了一个requestUserthunk 动作创建器,它只在用户尚未加载时才执行提取:

export const requestUser = createAsyncThunk("user/fetchById", 
  // call some API function
  async (userId) => {
    return await fetchUser(userId);
  }, {
    // return false to cancel
    condition: (userId, { getState }) => {
        const existing = selectUserById(getState(), userId);
        return !existing;
    }
  }
);

我们可以使用createSlice来创建reducer。userAdapter帮助我们更新状态。

const userSlice = createSlice({
    name: "users",
    initialState: userAdapter.getInitialState(),
    reducers: {
    // we don't need this, but you could add other actions here
    },
    extraReducers: (builder) => {
        builder.addCase(requestUser.fulfilled, (state, action) => {
            userAdapter.upsertOne(state, action.payload);
        });
    }
});
export const userReducer = userSlice.reducer;

但是由于我们的reducers财产是空的,我们也可以使用createReducer

export const userReducer = createReducer(
    userAdapter.getInitialState(),
    (builder) => {
        builder.addCase(requestUser.fulfilled, (state, action) => {
            userAdapter.upsertOne(state, action.payload);
        });
    }
)

我们的 React 钩子从选择器返回值,但也会触发dispatcha useEffect

export const useUser = (userId: EntityId): User | undefined => {
  // initiate the fetch inside a useEffect
  const dispatch = useDispatch();
  useEffect(
    () => {
      dispatch(requestUser(userId));
    },
    // runs once per hook or if userId changes
    [dispatch, userId]
  );

  // get the value from the selector
  return useSelector((state) => selectUserById(state, userId));
};

正在加载

如果用户已经加载前一种方法会忽略提取,但是如果它已经加载呢?我们可以对同一用户同时进行多次提取。

为了解决这个问题,我们的状态需要存储每个用户的获取状态。文档示例中,我们可以看到它们在用户实体旁边存储了一个键控状态对象(您也可以将状态存储为实体的一部分)。

我们需要添加一个空status字典作为我们的属性initialState

const initialState = {
  ...userAdapter.getInitialState(),
  status: {}
};

我们需要更新状态以响应所有三个requestUser操作。我们可以userId通过查看 的meta.arg属性来获得调用 thunkaction

export const userReducer = createReducer(
  initialState,
  (builder) => {
    builder.addCase(requestUser.pending, (state, action) => {
      state.status[action.meta.arg] = 'pending';
    });
    builder.addCase(requestUser.fulfilled, (state, action) => {
      state.status[action.meta.arg] = 'fulfilled';
      userAdapter.upsertOne(state, action.payload);
    });
    builder.addCase(requestUser.rejected, (state, action) => {
      state.status[action.meta.arg] = 'rejected';
    });
  }
);

我们可以通过 id 从状态中选择一个状态:

export const selectUserStatusById = (state, userId) => state.users.status[userId];

我们的 thunk 在确定是否应该从 API 获取时应该查看状态。如果它已经'pending'或,我们不想加载'fulfilled'如果是'rejected'或,我们将加载undefined

export const requestUser = createAsyncThunk("user/fetchById", 
    // call some API function
    async (userId) => {
        return await fetchUser(userId);
    }, {
        // return false to cancel
        condition: (userId, { getState }) => {
            const status = selectUserStatusById(getState(), userId);
            return status !== "fulfilled" && status !== "pending";
        }
    }
);