Quickstart

A quick dive into getting started with Lore

Step 4: Improve the User Experience

In this step we're going to improve the user experience for pagination.

You can view the finished code for this step by checking out the pagination.4 branch of the completed project.

Why is this happening?

Each time we click a pagination link, we change the URL in the browser. This change causes the application to re-render. When the Feed component re-renders, the connect call retrives the page of data associated with the page query parameter in the URL. Since that data hasn't been fetched yet, it passes a collection to Feed that has no data, and since Feed is rendering whatever it receives as a prop, it renders nothing.

How do we fix this?

To provide a better experience, we need to hold onto the previous tweets until the new page has been fetched from the API.

To do that, we're going to change our rendering strategy so that instead of using the Feed's props to determine what gets rendered, we're going to use Feed's state, and manage which page to render ourselves.

Update Feed

Start by providing some initial state and a componentWillRecieveProps() method to the Feed component that look like this:

// src/components/Feed.js
...
getInitialState() {
  const { tweets } = this.props;

  return {
    tweets: tweets,
    nextTweets: tweets
  };
},

componentWillReceiveProps(nextProps) {
  const nextTweets = nextProps.tweets;

  if (nextTweets.state === PayloadStates.FETCHING) {
    this.setState({
      nextTweets: nextTweets,
      isFetching: true
    });
  } else {
    this.setState({
      tweets: nextTweets,
      nextTweets: nextTweets,
      isFetching: false
    });
  }
},
...
// src/components/Feed.js
...
constructor(props) {
  super(props);
  this.state = {
    tweets: props.tweets,
    nextTweets: props.tweets
  };
}

componentWillReceiveProps(nextProps) {
  const nextTweets = nextProps.tweets;

  if (nextTweets.state === PayloadStates.FETCHING) {
    this.setState({
      nextTweets: nextTweets,
      isFetching: true
    });
  } else {
    this.setState({
      tweets: nextTweets,
      nextTweets: nextTweets,
      isFetching: false
    });
  }
}
...
// src/components/Feed.js
...
constructor(props) {
  super(props);
  this.state = {
    tweets: props.tweets,
    nextTweets: props.tweets
  };
}

componentWillReceiveProps(nextProps) {
  const nextTweets = nextProps.tweets;

  if (nextTweets.state === PayloadStates.FETCHING) {
    this.setState({
      nextTweets: nextTweets,
      isFetching: true
    });
  } else {
    this.setState({
      tweets: nextTweets,
      nextTweets: nextTweets,
      isFetching: false
    });
  }
}
...

Then update the render() method to look like this:

// src/components/Feed.js
render() {
  const { tweets, nextTweets } = this.state;
  const currentPage = nextTweets.query.pagination.page;
  const paginationLinks = [];

  // check if we're fetching the next page of tweets
  const isFetchingNextTweets = nextTweets.state === PayloadStates.FETCHING;

  if (tweets.state === PayloadStates.FETCHING) {
    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <div className="loader"/>
      </div>
    );
  }

  // calculate the number of pagination links from our metadata, then
  // generate an array of pagination links
  const numberOfPages = Math.ceil(tweets.meta.totalCount / tweets.meta.perPage);
  for (let pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
    paginationLinks.push(this.renderPaginationLink(pageNumber, currentPage));
  }

  return (
    <div className="feed">
      <h2 className="title">
        Feed
      </h2>
      <ul className={`media-list tweets ${isFetchingNextTweets ? 'transition' : ''}`}>
        {tweets.data.map(this.renderTweet)}
      </ul>
      <nav>
        <ul className="pagination">
          {paginationLinks}
        </ul>
      </nav>
    </div>
  );
}

In the code above, we've created a state variable called tweets, and updated ourrender() method so that we render the state version of tweets instead of what we get from props. Then we've added a componentWillReceiveProps() method that will allow us to inspect the incoming data from props and decide whether or not to render it.

If the new page is being fetched, then we continue to render the previous page, but add the transition class to the rendered list, which will provide a visual cue to the user that the new page is being fetched.

If the data is not being fetched, then we update our state variable to reflect the new page.

If you'd like to see a more advanced example of pagination, see the pagination example.

Visual Check-in

If everything went well, your application should now look like this.

Code Changes

Below is a list of files modified during this step.

src/components/Feed.js

import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import { connect } from 'lore-hook-connect';
import { Link } from 'react-router';
import PayloadStates from '../constants/PayloadStates';
import Tweet from './Tweet';

export default connect(function(getState, props) {
  const { location } = props;

  return {
    tweets: getState('tweet.find', {
      pagination: {
        sort: 'createdAt DESC',
        page: location.query.page || '1'
      }
    })
  };
})(
createReactClass({
  displayName: 'Feed',

  propTypes: {
    tweets: PropTypes.object.isRequired
  },

  getInitialState() {
    const { tweets } = this.props;

    return {
      tweets: tweets,
      nextTweets: tweets
    };
  },

  componentWillReceiveProps(nextProps) {
    const nextTweets = nextProps.tweets;

    if (nextTweets.state === PayloadStates.FETCHING) {
      this.setState({
        nextTweets: nextTweets,
        isFetching: true
      });
    } else {
      this.setState({
        tweets: nextTweets,
        nextTweets: nextTweets,
        isFetching: false
      });
    }
  },

  renderTweet(tweet) {
    return (
      <Tweet key={tweet.id} tweet={tweet} />
    );
  },

  renderPaginationLink(page, currentPage) {
    return (
      <li key={page} className={currentPage === String(page) ? 'active' : ''}>
        <Link to={{ pathname: '/', query: { page: page } }}>
          {page}
        </Link>
      </li>
    );
  },

  render() {
    const { tweets, nextTweets } = this.state;
    const currentPage = nextTweets.query.pagination.page;
    const paginationLinks = [];

    if (tweets.state === PayloadStates.FETCHING) {
      return (
        <div className="feed">
          <h2 className="title">
            Feed
          </h2>
          <div className="loader"/>
        </div>
      );
    }

    // check if we're fetching the next page of tweets
    const isFetchingNextTweets = nextTweets.state === PayloadStates.FETCHING;

    // calculate the number of pagination links from our metadata, then
    // generate an array of pagination links
    const numberOfPages = Math.ceil(tweets.meta.totalCount / tweets.meta.perPage);
    for (let pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
      paginationLinks.push(this.renderPaginationLink(pageNumber, currentPage));
    }

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <ul className={`media-list tweets ${isFetchingNextTweets ? 'transition' : ''}`}>
          {tweets.data.map(this.renderTweet)}
        </ul>
        <nav>
          <ul className="pagination">
            {paginationLinks}
          </ul>
        </nav>
      </div>
    );
  }

})
);
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'lore-hook-connect';
import { Link } from 'react-router';
import PayloadStates from '../constants/PayloadStates';
import Tweet from './Tweet';

class Feed extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      tweets: props.tweets,
      nextTweets: props.tweets
    };
  }

  componentWillReceiveProps(nextProps) {
    const nextTweets = nextProps.tweets;

    if (nextTweets.state === PayloadStates.FETCHING) {
      this.setState({
        nextTweets: nextTweets,
        isFetching: true
      });
    } else {
      this.setState({
        tweets: nextTweets,
        nextTweets: nextTweets,
        isFetching: false
      });
    }
  }

  renderTweet(tweet) {
    return (
      <Tweet key={tweet.id} tweet={tweet} />
    );
  }

  renderPaginationLink(page, currentPage) {
    return (
      <li key={page} className={currentPage === String(page) ? 'active' : ''}>
        <Link to={{ pathname: '/', query: { page: page } }}>
          {page}
        </Link>
      </li>
    );
  }

  render() {
    const { tweets, nextTweets } = this.state;
    const currentPage = nextTweets.query.pagination.page;
    const paginationLinks = [];

    // check if we're fetching the next page of tweets
    const isFetchingNextTweets = nextTweets.state === PayloadStates.FETCHING;

    if (tweets.state === PayloadStates.FETCHING) {
      return (
        <div className="feed">
          <h2 className="title">
            Feed
          </h2>
          <div className="loader"/>
        </div>
      );
    }

    // calculate the number of pagination links from our metadata, then
    // generate an array of pagination links
    const numberOfPages = Math.ceil(tweets.meta.totalCount / tweets.meta.perPage);
    for (let pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
      paginationLinks.push(this.renderPaginationLink(pageNumber, currentPage));
    }

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <ul className={`media-list tweets ${isFetchingNextTweets ? 'transition' : ''}`}>
          {tweets.data.map(this.renderTweet)}
        </ul>
        <nav>
          <ul className="pagination">
            {paginationLinks}
          </ul>
        </nav>
      </div>
    );
  }

}

Feed.propTypes = {
  tweets: PropTypes.object.isRequired
};

export default connect(function(getState, props) {
  const { location } = props;

  return {
    tweets: getState('tweet.find', {
      pagination: {
        sort: 'createdAt DESC',
        page: location.query.page || '1'
      }
    })
  };
})(Feed);
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'lore-hook-connect';
import { Link } from 'react-router';
import PayloadStates from '../constants/PayloadStates';
import Tweet from './Tweet';

@connect(function(getState, props) {
  const { location } = props;

  return {
    tweets: getState('tweet.find', {
      pagination: {
        sort: 'createdAt DESC',
        page: location.query.page || '1'
      }
    })
  };
})
class Feed extends React.Component {

  static propTypes = {
    tweets: PropTypes.object.isRequired
  };

  constructor(props) {
    super(props);
    this.state = {
      tweets: props.tweets,
      nextTweets: props.tweets
    };
  }

  componentWillReceiveProps(nextProps) {
    const nextTweets = nextProps.tweets;

    if (nextTweets.state === PayloadStates.FETCHING) {
      this.setState({
        nextTweets: nextTweets,
        isFetching: true
      });
    } else {
      this.setState({
        tweets: nextTweets,
        nextTweets: nextTweets,
        isFetching: false
      });
    }
  }

  renderTweet(tweet) {
    return (
      <Tweet key={tweet.id} tweet={tweet} />
    );
  }

  renderPaginationLink(page, currentPage) {
    return (
      <li key={page} className={currentPage === String(page) ? 'active' : ''}>
        <Link to={{ pathname: '/', query: { page: page } }}>
          {page}
        </Link>
      </li>
    );
  }

  render() {
    const { tweets, nextTweets } = this.state;
    const currentPage = nextTweets.query.pagination.page;
    const paginationLinks = [];

    // check if we're fetching the next page of tweets
    const isFetchingNextTweets = nextTweets.state === PayloadStates.FETCHING;

    if (tweets.state === PayloadStates.FETCHING) {
      return (
        <div className="feed">
          <h2 className="title">
            Feed
          </h2>
          <div className="loader"/>
        </div>
      );
    }

    // calculate the number of pagination links from our metadata, then
    // generate an array of pagination links
    const numberOfPages = Math.ceil(tweets.meta.totalCount / tweets.meta.perPage);
    for (let pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
      paginationLinks.push(this.renderPaginationLink(pageNumber, currentPage));
    }

    return (
      <div className="feed">
        <h2 className="title">
          Feed
        </h2>
        <ul className={`media-list tweets ${isFetchingNextTweets ? 'transition' : ''}`}>
          {tweets.data.map(this.renderTweet)}
        </ul>
        <nav>
          <ul className="pagination">
            {paginationLinks}
          </ul>
        </nav>
      </div>
    );
  }

}

export default Feed;

Next Steps

In the next section we're going to replace our pagination links with infinite scrolling behavior.