Quickstart
A quick dive into getting started with Lore
A quick dive into getting started with Lore
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.
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.
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.
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.
If everything went well, your application should now look like this.
Below is a list of files modified during this step.
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;
In the next section we're going to replace our pagination links with infinite scrolling behavior.