React-Redux: Preventing Repetitive API Requests
Intro
In our last post we were continuing with our simple blog post application where we are pulling in blogs posts and author data from an API and displaying it in a list. We got the application to display correctly, but now we have some items to clean up.
Repetitive API Requests
Here’s one of the problems with our app right here, we are making repetitive API requests for the same data over and over. How do we fix this?
Lodash Memoize
Solving this problem with memoize would be the easy way to handle this, however it requires the use of an outside library, which by default is not going to be my preferred option.
This method also makes it so that you are unable to re-fetch a resource a second time (in case it has been updated), which is eventually going to be a problem.
So in fact, I will skip this and instead describe the vanilla JS method to solve this problem.
Overfetching Solution
To solve our repetitive API request problem we are going to make a new action called fetchPostsAndUser
.
Once this new action is complete we will be fetching all the unique author data to the store before our UserHeader component even loads, and we will be able to remove the actions from the UserHeader entirely.
Scaffolding fetchPostsAndUsers
Here is our actions index with a scaffold of our new function.
import jsonPlaceholder from "../apis/jsonPlaceholder";
export const fetchPostsAndUsers = () => async dispatch => {
await dispatch(fetchPosts());
};
export const fetchPosts = () => async (dispatch) => {
const response = await jsonPlaceholder.get("/posts");
dispatch({ type: "FETCH_POSTS", payload: response.data });
};
export const fetchUser = (id) => async (dispatch) => {
const response = await jsonPlaceholder.get(`/users/${id}`);
dispatch({type: "FETCH_USER", payload: response.data });
};
And note that in our new fetchPostsAndUsers
function we put the fetchPosts
function as an argument inside of dispatch. Why is that? We are calling dispatch twice?
In fetchPostsAndUsers
, if we don’t call dispatch(fetchPosts())
, AKA only call fetchPosts()
, then the function returned from fetchPosts
is not going to be in the redux dispatch pipeline, if it’s not in the pipeline, the final dispatch({ type: "FETCH_USER", payload: response.data }
is not going to be called, then we will not get the data. calling dispatch(fetchPosts())
, the pipeline is going to be: dispatch fetchPosts function, then dispatch the inner function of fetchPosts, then dispatch the final object.
In a nutshell we are stacking actions in the dispatch pipeline, and if a child function requires the dispatch pipeline the parent function must be in the pipeline as well.
Replace Previous Action
After we create this action we can go back to our PostList component and replace the fetchPosts action with fetchPostsAndUsers in all the relevant places.
import React from "react";
import { connect } from "react-redux";
import { fetchPostsAndUsers } from "../actions";
import UserHeader from './UserHeader';
class PostList extends React.Component {
componentDidMount() {
this.props.fetchPostsAndUsers();
}
renderList() {
return this.props.posts.map((post) => {
return (
<div className="item" key={post.id}>
<i className="large middle aligned icon user" />
<div className="content">
<div className="description">
<h2>{post.title}</h2>
<p>{post.body}</p>
</div>
<UserHeader userId= {post.userId} />
</div>
</div>
);
});
}
render() {
return <div className="ui relaxed divided list">{this.renderList()}</div>;
}
}
const mapStateToProps = (state) => {
return { posts: state.posts };
};
export default connect(mapStateToProps, { fetchPostsAndUsers })(PostList);
At this point the first step is complete. Now we need to get a list of posts.
Get Unique Author ID's
If you recall, Redux-Thunk lets us pass in two arguments to our functions, dispatch and getState.
React-Redux: Asynchronous Actions (Redux-Thunk)
We will now finally be making use of this second argument. If we call getState we will now have access to all of the posts in the store. Then using some ES6 magic we can iterate over the posts and create a new array with only unique values using set.
const uniqueUsers = [...new Set(getState().posts.map(post => post.userId))];
and if we console.log that we can see that we an array with the ID’s of the authors each one time!
Now that we have an array of unique users called uniqueUsers
we can iterate over this array and for each ID request the user data using our previously created fetchUser
function.
uniqueUsers.forEach(id => dispatch (fetchUser(id)));
Note that this time we do not need to await
the fetchUser
function like we previously did for fetchPosts
. That is because there is nothing after this point in the function that would require we wait for it, whereas before with fetchPosts
we needed that to finish before we could construct our uniqueUsers
constant.
So all together our action index looks like this.
import jsonPlaceholder from "../apis/jsonPlaceholder";
export const fetchPostsAndUsers = () => async (dispatch, getState) => {
await dispatch(fetchPosts());
// create array of unique id's
const uniqueUsers = [...new Set(getState().posts.map(post => post.userId))];
uniqueUsers.forEach(id => dispatch (fetchUser(id)));
};
export const fetchPosts = () => async (dispatch) => {
const response = await jsonPlaceholder.get("/posts");
dispatch({ type: "FETCH_POSTS", payload: response.data });
};
export const fetchUser = (id) => async (dispatch) => {
const response = await jsonPlaceholder.get(`/users/${id}`);
dispatch({type: "FETCH_USER", payload: response.data });
};
Update UserHeader Component
At this point we are still making redundant requests, because we have not updated our UserHeader component to take advantage of our new action. Let’s do that now.
Old Component
import React from "react";
import { connect } from "react-redux";
import { fetchUser } from "../actions";
class UserHeader extends React.Component {
componentDidMount() {
this.props.fetchUser(this.props.userId);
}
render() {
const { user } = this.props;
if (!user) {
return null;
}
return <div className="header"> {user.name} </div>;
}
}
const mapStateToProps = (state, ownProps) => {
return { user: state.users.find((user) => user.id === ownProps.userId) };
};
export default connect(mapStateToProps, { fetchUser })(UserHeader);
New Component
We can now remove all of the action creator items from this component because all the user data was generated with the actions in the PostList component and is already available to us here as props!
import React from "react";
import { connect } from "react-redux";
class UserHeader extends React.Component {
render() {
const { user } = this.props;
if (!user) {
return null;
}
return <div className="header"> {user.name} </div>;
}
}
const mapStateToProps = (state, ownProps) => {
return { user: state.users.find((user) => user.id === ownProps.userId) };
};
export default connect(mapStateToProps)(UserHeader);
and we can see now that we are making a unique API request for each user.
GitHub Repo
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.