Search justacoding.blog. [Enter] to search. Click anywhere to close.

August 12th, 2022

Create A React Pagination Component

In this brief tutorial, we’ll go over the basics steps required to create your very own pagination component in React.

Here’s the finished component for you to follow along with: JSFiddle containing the React Pagination Bar Component.

The pagination mechanism your app uses is very important. Without an effective component to handle this aspect for you, users may find it more awkward and cumbersome to navigate through your application or website!

So with that said, let’s go over this simple pagination solution in detail.

What this React pagination component actually does

In its most basic form, a typical pagination component would simply present a set of clickable tiles or cells that navigates the user off to a specific page:

A simple pagination bar

We’re all familiar with this procedure.

However, there are some intricacies to consider when implementing your own solution.

The main one being: what should we do when there are many (100+, for instance) pages?

Surely we don’t want to always shows 100+ clickable links to the user each time? What if there are 1000 pages?

No, we don’t!

That would take up far too much of the screen’s real-estate (unnecessarily), and it’s simply not a scalable solution long-term.

A better way to handle pagination

So with that said, the main objective of our React pagination component is to effectively condense this pagination bar down — whilst still providing a component and UI element that actually makes sense to the end user.

We’ll show clickable page links as per above (and as is standard) — but we’ll also be ensuring that the element doesn’t become too long and is always neatly truncated down as required, no matter how many (or how few) pages exist.

Let’s get into it!

Implementing the “grouping” pagination logic

The main heavy-lifting in this React pagination component can be seen within the buildOutput function:

const buildOutput = (pageCount, activePage, rangeSize) => {
    const startRange = buildRange(1, rangeSize, 1, pageCount)
    const selectedRange = buildRange(
        activePage - (rangeSize - 1),
        activePage + (rangeSize - 1),
        1,
        pageCount
    )
    const endRange = buildRange(
        pageCount - (rangeSize - 1),
        pageCount,
        1,
        pageCount
    )

    const output = [...startRange]

    if (startRange.at(-1) < selectedRange.at(0) - 1) {
        output.push(FIRST_ELLIPSIS)
    }

    output.push(...selectedRange)

    if (selectedRange.at(-1) < endRange.at(0) - 1) {
        output.push(LAST_ELLIPSIS)
    }

    output.push(...endRange)
    return [...new Set(output)]
}

As you can see here, I’ve defined 3 concepts:

  • startRange
  • endRange
  • selectedRange

The members are simply just arrays of numbers (or page indexes), basically.

startRange represents the first few pages that are always visible in the pagination bar, regardless of which page is the “active” page.

Similarly, endRange defines the last few pages. These are always visible, too.

The selectedRange sits in the middle of these two. The idea here is that the selectedRange is composed of the active page as well as a few pages either side of this page.

To present this explanation more visually, you can consider the following:

// How our ranges would look based on a pageCount of 25
// with the activePage being page 12

const firstRange = [1, 2, 3, 4, 5]
const selectedRange = [10, 11, 12, 13, 14]
const lastRange = [21, 22, 23, 24, 25]

By defining these concepts above, it’s now much easier to implement the truncation logic that we require within our pagination component.

Note: The rangeSize prop determines how big these individual groups are. In the example above, a rangeSize of 5 would have been specified!

In fact, a good portion of that mechanism already (kind of) exists at this point, so let’s go over the remaining steps required to finish it off!

Further developing the truncation functionality

As you may have noticed, we’ve got some simple logic that also inserts some ELLIPSIS items into the generated output array, like so:

if (startRange.at(-1) < selectedRange.at(0) - 1) {
    output.push(FIRST_ELLIPSIS)
}

Basically, if the groups either side of the selectedRange aren’t immediately conjoining – we should insert these elements. These are an indicator to the user that there are more pages here, and that those pages are currently hidden from sight.

Consider these examples.

Ellipsis not required:

const firstRange = [1, 2, 3]
const selectedRange = [4, 5, 6]
const lastRange = [7, 8, 9]

Ellipsis required:

const firstRange = [1, 2, 3]
// Gap in pages here...
const selectedRange = [10, 11, 12]
// And here..
const lastRange = [18, 19, 20]

So there are two possible places these items can be inserted during the output of our pagination items. Either before the selectedRange, and/or after the selectedRange.

As to how these ellipsis characters are used and what they actually do, specifically, within our pagination component – we’ll cover that shortly.

For now, it’s necessary to go over what the output of our main pagination component actually looks like first.

Outputting the Pagination bar

As you can see, outputting the pagination bar is as simple as mapping over our output list.

The output list is generated by the function we’ve just discussed (buildOutput), and it’s invoked whenever activePage changes. When the user navigates to a different page – we clearly need our pagination logic (that builds up the pagination bar itself) to come into effect once again.

The pagination bar itself looks different depending on which page the user is on, whether they are at the very end of the range, the very start of the range and so on.

That’s handled here via a simple useEffect hook:

useEffect(() => {
    setOutput(buildOutput(pageCount, activePage, rangeSize))
}, [activePage])

Once we have the correct data returned via the buildOutput function – 95% of the work is already done. It’s just a matter of rendering or outputting the items in a suitable manner to make our React pagination bar component aesthetically pleasing and easy to use.

<div className="bar">
    {output.map((outputItem) => {
        const isEllipsisItem = [
            FIRST_ELLIPSIS,
            LAST_ELLIPSIS
        ].includes(outputItem)
            return (
                <div
                    key={outputItem}
                    className={`cell 
                        ${outputItem === activePage && "active"} 
                        ${isEllipsisItem && "ellipsis"}`}
                    onClick={() => onCellClick(outputItem)}
                    >
                        {isEllipsisItem ? "..." : outputItem}
                    </div>
            )
    })}
</div>

A class is conditionally applied for the currently active page, and we also show some simple back/forward navigation elements to assist with navigation. These just cycle through the pages either to the left or to the right.

The main thing to note here is the onCellClick function:

const onCellClick = (outputItem) => {
    if (outputItem === FIRST_ELLIPSIS) {
        setOutput(
            appendHiddenPagesToOutput(
                output,
                output.indexOf(FIRST_ELLIPSIS)
            )
        )
        return
    }
    if (outputItem === LAST_ELLIPSIS) {
        setOutput(
            appendHiddenPagesToOutput(
                output, 
                output.indexOf(LAST_ELLIPSIS)
            )
        )
        return
    }
    onNavigate(outputItem)
}

This is where our ellipsis characters come in handy.

Upon clicking a “regular” cell (or page), ie. an item that’s not one of our ellipsis characters (a number, or index, representing the page in question) — we’ll navigate using whatever function was passed in to the Pagination component via the onNavigate prop.

Note: in this case, I’m just handling a fake scenario in the parent App component above.

In the real world, you’d be using something like React Router to actually navigate to a different page. I’m just mimicking that scenario here for the sake of ease.

However, upon clicking one of the ellipsis characters, we don’t want to do that.

Instead, we want to insert all of the “hidden” pages into the output array, and thus re-render the component accordingly:

const appendHiddenPagesToOutput = (output, index) => {
    const newOutput = [...output]
    const firstPart = newOutput.slice(0, index)
    const lastPart = newOutput.slice(index + 1)

    const items = []
    let arrayFromCursor = firstPart.at(-1) + 1
    const arrayTo = lastPart.at(0)
    while (arrayFromCursor < lastPart.at(0)) {
        items.push(arrayFromCursor)
        arrayFromCursor++
    }

    return [...firstPart, ...items, ...lastPart]
}

This provides us with the following behaviour:

  • The user is presented with a pagination bar
  • They are on page 30
  • They want to navigate to page 22, but this is currently hidden
  • They expand out the pagination bar options using the ellipsis character – page 22 is now visible and the user can select it to navigate away

With these dynamic elements, that effectively wraps up the truncation aspect of this simple React pagination bar component, and in fact, the pagination solution as a whole.

Potential developments

I think the component is built in a manner which (hopefully) makes it easy to extend or improve.

Here are a few ideas regarding new features that can be tagged on to what we already have:

A “jump-to-page” input

Provide an input field whereby the user can type in a number. ie 10 or 15, and then navigate them off to that specific page.

You’ll need to ensure that they can only enter “valid” pages, you don’t want to send them to a page that doesn’t exist!

More granular truncation logic

Our current pagination component reveals “all” hidden pages once one of the ellipsis characters are clicked.

That’s fine for now.

But it may be nicer to be able to show only some of the hidden pages. So the “next” 10 pages, for instance. Working in an incremental fashion.

This would perhaps provide a nicer experience for the end user, and it’ll also solve the problem of showing too many hidden pages (and hence, reverting back to the problem we were initially trying to solve – the element becoming too large!)

In summary

I hope you’ve found this simple React pagination component example easy to understand and I hope you’ve gained something from reading this brief guide!

There are many more code examples like it, feel free to check those out in our Code Examples section!

Thanks for reading!

Have any questions? Ping me over at @justacodingblog
← Back to blog