Adding pagination into Next.js blog
Tags: javascript, nextjs
Published: 2021-11-11 20:14:57


I recently redid my blog with Next.js. I used the amazing Next.js tutorial and I was very happy with it. But as time went on and I wrote more and more articles it became apparent that I need to add paging. I am not an expert on Next and it turns out that adding paging will not be that easy. I used static generation for my listing page and generating all the pages is not an option. I decided to switch to server-side rendering for SEO reasons but also I wanted to switch pages on the fly.

## Adding API

First thing I needed to add an API call that would provide paging info and list posts.
I created a posts directory in a root api folder and in there I created a [page].js file. This file will be my api handler.
```javascript
// api/posts/[page].js

import {getSortedPostsData} from "../../lib/posts";


export default function (req, res) {
    const { page } = req.query
    const allPostsData = getSortedPostsData()
    const perPage = 9
    const totalPosts = allPostsData.length
    const totalPages = totalPosts / perPage
    const start = (page - 1) * perPage
    let end = start + perPage
    if (end > totalPosts) {
        end = totalPosts
    }

    res.status(200).json({
        currentPage: page,
        perPage: perPage,
        totalCount: totalPosts,
        pageCount: totalPages,
        start: start,
        end: end,
        posts: allPostsData.slice(start, end)
    })
}
```

This is pretty straightforward code. It's doing some stats from an array of all posts.
Side note here, if you are deploying to Vercel, your api calls are deployed as serverless functions and you need to tell Vercel to add your markdown files to the serverless deploy. This is done via root vercel.json file.

```json
{
  "functions": {
    "api/posts/[page].js": {
      "includeFiles": "posts/**"
    }
  }
}
```

The root posts directory is the place where I have all the markdown files.

## Modifying blog listing page

I used the blog listing page pretty much out of the next.js tutorial. I was using static page generation. So the first thing I have done was to change it to server-side rendering.

```javascript
Blog.getInitialProps = async ({ query }) => {
    const page = query.page || 1; //if page empty we request the first page
    const response = await fetch(`${server}/api/posts/${page}`)
    const posts = await response.json()
    return {
        totalCount: posts.totalCount,
        pageCount: posts.pageCount,
        currentPage: posts.currentPage,
        perPage: posts.perPage,
        posts: posts.posts,
    }
}
```

It fetches our new api call and returns it as our component properties.
The server variable is different for localhost and for prod. We need to specify the full path as this will be called from the server.

```javascript
const dev = process.env.NODE_ENV !== 'production';
export const server = dev ? 'http://localhost:3000' : 'https://ppolivka.com';
```

I am using next/router to navigate between pages. And to make all the things more user-friendly I added a loading animation on route changes.

```javascript
const [isLoading, setLoading] = useState(false);
const startLoading = () => setLoading(true);
const stopLoading = () => setLoading(false);

useEffect(() => {
    Router.events.on('routeChangeStart', startLoading);
    Router.events.on('routeChangeComplete', stopLoading);

    return () => {
        Router.events.off('routeChangeStart', startLoading);
        Router.events.off('routeChangeComplete', stopLoading);
    }
}, [])
```

To render the posts or the loading I have a if in this style.

```javascript
let content;
if (isLoading) {
    content = (
        <div className={styles.loadWrapper}>
            <Spinner animation="border" role="status">
                <span className="visually-hidden">Loading...</span>
            </Spinner>
        </div>
    )
} else {
    //Generating posts list
    content = (
        <>
            {props.posts.map(({ id, date, title, image, description }) => (
                <Card className={styles.item}>
                    <Card.Img variant="top" src={image} width={360} height={215} />
                    <Card.Body>
                        <Card.Title>
                            <Link href={`/posts/${id}`}>
                                <a>
                                    {title}
                                </a>
                            </Link>
                        </Card.Title>
                        <Card.Subtitle className="mb-2 text-muted"><Date dateString={date} /></Card.Subtitle>
                        <Card.Text>
                            {description}
                        </Card.Text>
                    </Card.Body>
                </Card>
            ))}
        </>
    );
}
```

For the actual pagination navigation I used awesome component react-paginate.

```javascript
<ReactPaginate
    previousLabel={'<'}
    nextLabel={'>'}
    breakLabel={'...'}
    breakClassName={'break-me'}
    activeClassName={'active'}
    containerClassName={'pagination'}
    subContainerClassName={'pages pagination'}
    initialPage={props.currentPage - 1}
    pageCount={props.pageCount}
    marginPagesDisplayed={2}
    pageRangeDisplayed={5}
    onPageChange={paginationHandler}
/>
```

It's referring the pagination handler function, that has the actual navigation logic. 

```javascript
const paginationHandler = (page) => {
    const currentPath = props.router.pathname;
    const currentQuery = props.router.query;
    currentQuery.page = page.selected + 1;

    props.router.push({
        pathname: currentPath,
        query: currentQuery,
    })

}
```

You can see the whole blog page in this [Gist](https://gist.github.com/PavlikPolivka/a3125f354757a498f769ed28a2840991).


---

If you like this article you can follow me on [Twitter](https://twitter.com/pavel_polivka).