React-Redux: Blog List App pt2, Matching and Retrieving Author Data

Intro

This is a continuation of our previous post where we are creating a simple app to fetch blog post data from an API and display it in a list. In this post we are going to fetch the author data for each post, which is related with an ID and display the author name for each post.

Easy Way vs Hard Way

The easy method to accomplish this would be to simply fetch all of the author data from the API and then pull the data with matching ID’s. And we could do that. But this would not be practical in large scale applications. The API that we are working with only has 10 authors, but if we were fetching data from an API or database that had millions of records and pulling out a small number of them that would be a huge waste of bandwidth, so we need to selectively request records from the API that match our ID.

The Process

fetch author id diagram

Fetch User Action

First we need to create an action that makes another API request for a piece of user data. However this time we need to include a variable in the request, which is a user ID. Knowing that we can create the following action in our action index.

export const fetchUser = (id) => async (dispatch) => {
  const response = await jsonPlaceholder.get("/users/" + id);

  dispatch({type: "FETCH_USER", payload: response.data });
};

or with ES2015 syntax we can create the string using back ticks .

export const fetchUser = (id) => async (dispatch) => {
  const response = await jsonPlaceholder.get(`/users/${id}`);

  dispatch({type: "FETCH_USER", payload: response.data });
};

Scaffold UserHeader Component

We are going to be making a new component that will just display the author name, and call that UserHeader. Because it will need to get props we need it to be a class based component. We can scaffold that out like so.

import React from 'react';

class UserHeader extends React.Component {
    render() {
        return <div> User Header </div>
    }
}

export default UserHeader;

and then if we head back to our PostList component we can add the UserHeader component as a child component, and pass in the author ID as a prop.

import React from "react";
import { connect } from "react-redux";
import { fetchPosts } from "../actions";
import UserHeader from './UserHeader';


class PostList extends React.Component {
  componentDidMount() {
    this.props.fetchPosts();
  }

  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() {
    console.log(this.props.posts);
    return <div className="ui relaxed divided list">{this.renderList()}</div>;
  }
}

const mapStateToProps = (state) => {
  return { posts: state.posts };
};

export default connect(mapStateToProps, { fetchPosts })(PostList);

Next we can make sure that every time the UserHeader component appears on screen we attempt to fetch the user name. To do this we create an event listener for componentDidMount and we hook up connect with our fetchUser Action.

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() {
    return <div> User Header </div>;
  }
}

export default connect(null, { fetchUser })(UserHeader);

We don’t have a mapStateToProps function yet so we set the first argument of Connect to null. Now that we have an action and a component, we can work on our reducer.

Users Reducer

Inside the reducers index let’s make the following usersReducer.js.

export default (state = [], action) => {
  switch (action.type) {
    case "FETCH_USER":
      return [...state, action.payload];
    default:
      return state;
  }
};

and of course we import that into our reducers index file.

import { combineReducers } from 'redux';
import postsReducer from './postsReducer';
import usersReducer from './usersReducer';

export default combineReducers({
    posts: postsReducer,
    users: usersReducer
});

Finding Relevant Users

At this point we have used the Author/User ID’s from the posts, and using our fetchUser action pulled all of those users and put them in the store. Which is great, instead of just requesting all of the users wholesale we have only requested the ones that we need. But now we have all of those users in one array in the store. So now inside of our UserHeader component we need to pull the pull out the correct user for each post and insert it into the UserHeader component.

To accomplish this we are going to use the vanilla JavaScript array method .find.

MDN: Array.prototype.find()

And with this .find method we are saying, for each user, find the first user who’s id property matches the property of the user id being passed in from props (which is the user ID of the author of this particular post). We throw in a quick null check after that, not strictly necessary.

Then we render that users name in the div.

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.users.find((user) => user.id === this.props.userId);

    if (!user) {
        return null;
    }

    return <div className="header"> {user.name} </div>;
  }
}

const mapStateToProps = (state) => {
  return { users: state.users };
};

export default connect(mapStateToProps, { fetchUser })(UserHeader);

Lastly we add our mapStateToProps function, pull in the state and map users to the key users. We also replace the null first argument in our connect function.

authors are displayed

And our authors are now displayed nicely.

Refactoring UserHeader Component

Now, this is working. But we can refactor this component to be better, especially on larger applications. Currently we are passing in the whole list of users to this component and then filtering out the one we want inside of the component. That is way more data than this component actually needs, and limits our ability to re-use this component, and re-usability is the goal!

Extracting Logic To mapStateToProps

The mapStateToProps function is where we tell the store what information we get passed down as a prop. Currently we are asking for all the users and filtering in the component. But if we move that filter logic into the mapStateToProps function we can request just the author we need every time.

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

ownProps

Above we have done a couple things. We have moved the .find logic into mapStateToProps, and we have added a second argument there called ownProps.

Redux Docs: Connect: Extracting Data with mapStateToProps

We must use this second argument because we really needed to reference this.props.userId. But props is only available inside of the component, which our mapStateToProps function is not. ownProps is a reference to the props that are about to be sent into this component.

We have also updated the name of the key to user to more accurately reflect the prop that we will be getting back now.

return { user: state.users.find((user) => user.id === ownProps.userId) };

Lastly we have updated the definition of our user constant in the component. We are only getting back one user now, so we simply destructure that property off of props.

const { user } = this.props;

GitHub Repo

Ncoughlin: React-Bloglist