React - Pagination (Part 1)

This video is available to view for members only.

Click here to Join!

Already a member?

Login


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.

Code For This Episode

Get the code for this episode.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# Title Duration
1 Intro, and Project Demo 02:27
2 Pagination with Twig and KnpPaginatorBundle 07:49
3 Sorting with Twig and KnpPaginatorBundle 02:15
4 Simple Filtering in Twig 05:00
5 API - Pagination, (Basic) Filtering, and Sorting 10:25
6 Angular - Pagination 10:14
7 Angular - Sorting 06:11
8 Angular - Limiting 05:43
9 Angular - Filtering 03:47
10 React - Pagination (Part 1) 10:45
11 React - Pagination (Part 2) 04:14
12 React - Sorting 11:11
13 React - Limiting 03:43
14 React - Filtering 03:41