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?

multiple requests for the same object example server on fire

Lodash Memoize

Lodash Documentation: 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.

fetch posts and users action overview

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.

fetch posts and users action overview, part 1 complete

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.

MDN: 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!

array of ten authors

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.

api requests now unique

GitHub Repo

Ncoughlin: React-Bloglist