score:12
Edit: This answer predates the release of RTK Query which has made this task much easier! RTK Query automatically handles caching and much more. Check out the docs for how to set it up.
Keep reading if you are interested in understanding more about some of the concepts at play.
Tools
Redux Toolkit can help with this but we need to combine various "tools" in the toolkit.
- createEntityAdapter allows us to store and select entities like a user object in a structured way based on a unique ID.
- createAsyncThunk will create the thunk action that fetches data from the API.
- createSlice or createReducer creates our reducer.
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";
}
}
);
Source: stackoverflow.com
Related Query
- How can I cache data that I already requested and access it from the store using React and Redux Toolkit
- How can I store the data from scanning a QR code in an array using react?
- How to send array of objects data from child component to parent component and store in the parent object using react hooks?
- How can I update react app that gets it's data from the node mongo backend using redux
- How do we return a Promise from a store.dispatch in Redux - saga so that we can wait for the resolve and then render in SSR?
- How to make the Service Worker cache data from API and update the cache when needed
- How to retrieve data from the server using fetch get method and show it in a table
- How to store data from API so that it can be accessible in other components
- How to retrieve data from Redux store using functional components and avoiding useSelector & useDispatch
- Using Redux Toolkit, how do I access the store from a non-react file?
- How to send data from react and use the request body in express, using Axios?
- How do I correctly add data from multiple endpoints called inside useEffect to the state object using React Hooks and Context API?
- How can I create a button which Fetches image from API and download to the local with using ReactJs
- How to use a variable in which data is fetched from mongoDB , in another file where we can use the data stored in that variable
- How to modify the code such that can call usehook using react and typescript?
- In a Functional Component, how do you access props from the redux store using react-redux connect?
- How to get the data from Redux store using class components in React
- how can I store and access data attributes on elements like select options in React
- How to fetch json data from a url that requires an api key to access the data in reactjs?
- How can I pass a value from the backend to a React frontend and use that value in the CSS for the frontend?
- How can I render data, which I receive from two different inputs, in the UI that I am rendering, using React?
- How to delete data from DB using React, and hitting a rest API that i created using php and mysql?
- How to use a data from the 1st API call and use that in my 2nd API call?
- How can I delete a row using the reduce method from my redux store
- How can I get and store Data from Textfield in Form Container?
- How to access data outside the function using axios and react js
- How can I remove data from the page and it doesn't show?
- How can I access the text input fields when using express-fileupload from react
- How to get data from the backend that needs authorization using React
- how do i access the required data using map and key
More Query from same tag
- Typescript IndexOf Issue in Array<String> useState
- osx create-react-app not installing
- React: Generate and save PDF onClick() problem - Kendo
- I can't access the redux store properly in react components
- Not able to install react-router
- The value got in multiplication showing some difference
- Passing components as prop in React and calling them as JSX tags
- fetch() retrieved data in JS (React) throws error with no apparent reason
- How to change fetch API link for deployment?
- React router dom not rendering child component but url changed
- Passing a property aquired asynchronously
- Getting undefined when importing svg as ReactComponent in create-react-app project
- React Application Environment Variable Undefined after Deploying
- The best way to retrieve data of a range of different child components in React Js
- What is the difference between @react-navigation/stack vs @react-navigation/native-stack?
- Way to access axios response data outside/ global
- Why twice bind on onClick is required in React?
- How to loop images using map method in react?
- Data is not set on time on the state using hooks in React JS
- what does props in export default do?
- passing values from component to redux store
- How to set credentials property in ReactJs fetch by fetch-interceptor
- How do I upload some special file extensions (djvu, jp2, ...) on web
- Error "Maximum update depth exceeded. This can happen when a component calls setState inside useEffect"
- Not able to access the animate.css's animation keyframe names from scss file
- react-dom: won't accept javascript inside the angular brackets
- Reactjs how to add variable in URL parameter
- this.setState not updating
- What is the BEST way to do the error handling in react using Axios?
- how to concat react-query previous data with new data