React - Pagination (Part 1)
In this video we are going to add in Pagination to our React CRUD implementation. The end result will be very similar in look-and-feel to how the Angular Pagination was implemented, but thwe code itself is quite different.
Much like how we made use of the Angular UI Bootstrap project, in the React implementation we will make use of React Bootstrap which will give us a set of immediately usable components that we can easily add into our project.
One thing to note here is that the React Bootstrap project is not yet at version 1.0.0 (at the time of writing), and as such, is not yet as easy to use as the Angular UI Bootstrap, in my opinion anyway. That said, it is fully featured and working enough to get started. So... lets!
Fixing An Initial Bug
If you have been following along with the Angular implementation then you will remember that we had to modify our code to handle the concept of Pagination from our API result.
To quickly clarify: originally this code (the Twig, Angular, and React code) was a very basic setup designed to show how we can talk to a Symfony 3 API from Angular, or React, or any other modern front end library. It needn't even be a JavaScript front-end - literally anything with a web connection to our API could have its own implementation.
Because our original implementation was as simple as possible, we didn't worry about Pagination, Sorting, Filtering, or any of that jazz.
Then, once we'd figured all that out, we took it to the immediate next level of functionality - by adding Pagination, Filtering, Sorting, and Limiting - and we did that earlier in this series.
Adding this had a side effect of changing the result returned from our API.
Whereas previously we would return just a bunch of blog posts, now we get back the blog posts and also various fields telling us how many records there are, how many records we see per page, and so on. If you are unsure on this then it is covered in more detail in this video and the associated write up.
With the blog posts now being returned alongside the paging information, we must update the reference we were using in our API response.
Thankfully, after all these words, this is a simple one liner:
// /src/containers/list.js
componentDidMount() {
fetchBlogPosts()
.then(apiResponse => {
console.log('blog posts', apiResponse);
this.setState({
blogPosts: apiResponse.data
});
});
}
We've simply pointed blogPosts
at apiResponse.data
, instead of simply using the apiResponse
.
Note that in the video this comes under data.data
. I have change the variable name used in the then
function parameter simply to make this more understandable. The word data
is very generic, and it isn't immediately obvious if you are new to JavaScript what data
may actually be.
Adding React Bootstrap
Adding React Bootstrap to our project is very simple:
npm install --save react-bootstrap
Allow npm
to do its thing, and within a few seconds we should have React Bootstrap added to our project.
Importing The Paginator
Adding the dependency is easy enough. Now we actually need to use it.
Again, doing this is fairly straightforward - certainly easier than writing a paginator for ourselves.
To begin with, we will copy / paste one of the examples from the Pagination section on the React Bootstrap docs:
// /src/containers/blogPosts/list.js
render() {
return (
<div>
<Table blogPosts={this.state.blogPosts}
onDelete={this.onDelete.bind(this)} />
<Pagination
bsSize="medium"
items={10}
activePage={1}/>
</div>
);
}
One other thing that is important and should be better documented (imo) is that we must have the import
statement to actually use the Pagination
component:
// /src/containers/blogPosts/list.js
import React, { Component } from 'react';
import { Pagination } from 'react-bootstrap';
export default class List extends Component {
// etc
And with that, we should have a very basic implementation of the Pagination component added to our list view.
Of course it won't actually work as expected at this stage, for a few reasons:
Firstly, to just get this on the page we removed the onSelect
prop which would be used to pass through the function we would like to invoke whenever one of the pagination options is pressed - page 1, page 6, 'next', whatever.
Secondly, we've hardcoded all the values. And there's an interesting point to this.
Notice that we have set 10 items
. When I began playing with the Pagination component, I mistakenly thought items
meant the total number of items I was paginating over. For example, if my API result contained 100 blog posts, I would have 100 items
. Not so. items
means the amount of page options / choices to display. So if we want to show a user 10 pages, then really that's 10 `items. Confusing.
Don't Panic, Make It Dynamic
We already have some of the information we need to make parts of this work in a dynamic fashion. Our API response contains the currentPage
, itemsPerPage
, and totalItems
. We can definitely make use of the currentPage
for the activePage
prop.
To do this we must pass the value from the result of fetchBlogPosts()
to the Pagination
component. How we do this may not be immediately obvious, so let's quickly cover the approach:
We currently get all the blog posts whenever the componentDidMount
function is invoked. React handles this for us behind the scenes.
Once we have all these blog posts, we call the this.setState
method to update the component's state with the fetched list of blog posts:
// /src/containers/blogPosts/list.js
componentDidMount() {
fetchBlogPosts()
.then(apiResponse => {
console.log('blog posts', apiResponse);
this.setState({
blogPosts: apiResponse.data
});
});
}
Whilst we're doing this, we could also make a note of the currentPageNumber
on the state
also:
// /src/containers/blogPosts/list.js
componentDidMount() {
fetchBlogPosts()
.then(apiResponse => {
console.log('blog posts', apiResponse);
this.setState({
blogPosts: apiResponse.data,
currentPageNumber: apiResponse.currentPageNumber
});
});
}
Easy enough.
So we can go ahead and update the Pagination
component's props
to use this new state
value:
// /src/containers/blogPosts/list.js
render() {
return (
<div>
<Table blogPosts={this.state.blogPosts}
onDelete={this.onDelete.bind(this)} />
<Pagination
bsSize="medium"
items={10}
activePage={this.state.currentPageNumber}/>
</div>
);
}
But we have a slight problem here. When this component first renders, that value of this.state.currentPageNumber
is going to be undefined
.
To fix this we simply need to update the this.state
definition in our constructor
method:
// /src/containers/blogPosts/list.js
constructor(props) {
super(props);
this.state = {
blogPosts: [],
currentPageNumber: 1
};
};
And with that the activePage
should be behaving in a dynamic fashion.
We have no real way of proving this at the moment though, so feel free to swap out these values for hardcoded numbers and see how it reacts (no pun intended).
Dynamic Page Numbers
To be clear here, again, the React Bootstrap Pagination component has confusing terminology around what I would call the total amount of pages, and what the Pagination
component calls items
.
What we need to do is figure out a formula to determine how many page numbers we show to the user.
This formula is pretty straightforward - our API gives us the values we need, but not the final figure required to make this super simple. No bother, let's do a bit of psuedo-code division:
var totalPages = apiResult.totalItems / apiResult.itemsPerPage;
Let's plug some numbers in to ensure this makes sense:
var totalPages = 100 / 10; // 10 page numbers
var totalPages = 60 / 5; // 12 page numbers
var totalPages = 11 / 10; // 1.1 page numbers... oops
We have a problem here if we don't handle remainders.
There are two common functions for this kind of thing - ceil
and floor
- both part of the Math
object.
floor
- rounds down
ceil
- short for ceiling - rounds up
We don't want to round down as in our third example, 1.1
would become 1
page, which means the 11th result would be on an inaccessible page.
By using Math.ceil
we can round up, giving us 2 pages. One page with 10 results, and a second page with only 1 result. Perfect.
Now, the next question becomes:
Where do we put this formula?
As it's an on the fly computation happening at render
time and involving only the values already held in state
, we can put it in the render()
function. This is perfectly acceptable practice, to the very best of my knowledge.
// /src/containers/blogPosts/list.js
render() {
let totalPages = Math.ceil(this.state.totalItems / this.state.numItemsPerPage);
return (
<div>
<Table blogPosts={this.state.blogPosts}
onDelete={this.onDelete.bind(this)} />
<Pagination
bsSize="medium"
items={totalPages}
activePage={this.state.currentPageNumber}/>
</div>
);
}
We do need to make sure that we update the initial state and update the state values with the values returned in fetchBlogPosts()
though:
// /src/containers/blogPosts/list.js
constructor(props) {
super(props);
this.state = {
blogPosts: [],
currentPageNumber: 1,
totalItems: 1,
itemsPerPage: 10
};
};
componentDidMount() {
fetchBlogPosts()
.then(apiResponse => {
console.log('blog posts', apiResponse);
this.setState({
blogPosts: apiResponse.data,
currentPageNumber: apiResponse.currentPageNumber,
totalItems: apiResponse.totalItems,
itemsPerPage: apiResponse.itemsPerPage
});
});
}
We are almost there with a working Pagination
component. One last thing to fix, and that is to trigger / invoke a function whenever we choose one of the available page numbers.
Selecting Page Numbers
The last step for this video is to implement the function that is passed in the onSelect
prop. This function will be invoked / called / triggered whenever a user clicks on one of the numbers that represent a page in our list.
The function we will create at this stage will simply update the value in the state
for currentPageNumber
. We will worry about the API call for this in the next video.
Firstly, let's update the Pagination
component to add the onSelect
back in:
// /src/containers/blogPosts/list.js
render() {
let totalPages = Math.ceil(this.state.totalItems / this.state.numItemsPerPage);
return (
<div>
<Table blogPosts={this.state.blogPosts}
onDelete={this.onDelete.bind(this)} />
<Pagination
bsSize="medium"
items={totalPages}
activePage={this.state.currentPageNumber}
onSelect={this.handleSelect}/>
</div>
);
}
Easy enough, we just added the onSelect
prop, and told it we want to use the method defined under this.handleSelect
. We haven't actually created that method yet, so let's do so:
// /src/containers/blogPosts/list.js
handleSelect(number) {
console.log('handle select', number);
}
Incidentally, the reason I use the word 'method' here instead of 'function' is that we are working with a class. Functions inside classes are called methods, even though they are still functions. Geeky.
The number
here will be the number that the user clicked in our Pagination
list of items
.
This is fine, but it doesn't really get us anywhere.
To actually achieve something we want to update the currentPageNumber
in the list
's state
. However, this will cause a problem:
// /src/containers/blogPosts/list.js
handleSelect(number) {
console.log('handle select', number);
this.setState({currentPageNumber: number});
}
Which looks like it should work, but if we try this we get a:
Uncaught TypeError: Cannot read property 'setState' of undefined
Which is kinda weird. What's happening here though is that we have changed the context of this
. JavaScript now thinks we are trying to call setState
with this
set to the context of the Pagination
component, rather than what we expected - the list
class.
We can fix this by using bind
, which will bind the context of this
to the list
, like we want / expect.
// /src/containers/blogPosts/list.js
render() {
let totalPages = Math.ceil(this.state.totalItems / this.state.numItemsPerPage);
return (
<div>
<Table blogPosts={this.state.blogPosts}
onDelete={this.onDelete.bind(this)} />
<Pagination
bsSize="medium"
items={totalPages}
activePage={this.state.currentPageNumber}
onSelect={this.handleSelect.bind(this)}/>
</div>
);
}
And with this sorted we should now be able to correctly call setState
and the behaviour should be as expected.
At this stage we can start making the pagination component actually do things with our API. We will continue on with this in the very next video.