Welcome to 16892 Developer Community-Open, Learning,Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

How can I get data from the store using React Redux Toolkit and get a cached version if I already requested it?

I need to request multiple users for example user1, user2, and user3. If I make a request for user1 after it has already been requested then I do not want to fetch user1 from the API again. Instead it should give me the info of the user1 from the store.

How can I do this in React with a Redux Toolkit slice?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
2.1k views
Welcome To Ask or Share your Answers For Others

1 Answer

Tools

Redux Toolkit can help with this but we need to combine various "tools" in the toolkit.

React vs. Redux

We are going to create a useUser custom React hook to load a user by id.

We will need to use separate hooks in our hooks/components for reading the data (useSelector) and initiating a fetch (useDispatch). Storing the user state will always be the job of Redux. Beyond that, there is some leeway in terms of whether we handle certain logic in React or in Redux.

We could look at the selected value of user in the custom hook and only dispatch the requestUser action if user is undefined. Or we could dispatch requestUser all the time and have the requestUser thunk check to see if it needs to do the fetch using the condition setting of createAsyncThunk.

Basic Approach

Our na?ve approach just checks if the user already exists in the state. We don't know if any other requests for this user are already pending.

Let's assume that you have some function which takes an id and fetches the user:

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

We create a userAdapter helper:

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;

We create a requestUser thunk action creator that only executes the fetch if the user is not already loaded:

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;
    }
  }
);

We can use createSlice to create the reducer. The userAdapter helps us update the state.

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;

But since our reducers property is empty, we could just as well use createReducer:

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

Our React hook returns the value from the selector, but also triggers a dispatch with a 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));
};

isLoading

The previous approach ignored the fetch if the user was already loaded, but what about if it is already loading? We could have multiple fetches for the same user occurring simultaneously.

Our state needs to store the fetch status of each user in order to fix this problem. In the docs example we can see that they store a keyed object of statuses alongside the user entities (you could also store the status as part of the entity).

We need to add an empty status dictionary as a property on our initialState:

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

We need to update the status in response to all three requestUser actions. We can get the userId that the thunk was called with by looking at the meta.arg property of the action:

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';
    });
  }
);

We can select a status from the state by id:

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

Our thunk should look at the status when determining if it should fetch from the API. We do not want to load if it is already 'pending' or 'fulfilled'. We will load if it is 'rejected' or 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";
        }
    }
);

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to 16892 Developer Community-Open, Learning and Share
...