author face photo

How I Accidentally Built a 22× Faster Pagination Hook

Published October 5, 2025

How It All Started

For me, this turned into a spontaneous action because I was playing around with Next.js, and when it came to pagination, I didn't plan on using external dependencies. So I checked out what Next offers in its examples. Finding a link to an example in the official documentation, I was disappointed—I think that's not up to the level of a giant. In their array, strings and numbers coexist at the same time, and overall, the solution looks simple and "hardcoded."

That's when the idea hit me to write my own hook that could be used without tying it to a framework. It had to be simple and as clean as possible 💡.

Tip: If you're like me and looking for inspiration, start by reviewing existing solutions—but don't be afraid to surpass them with your own version.


What Inspired Me

To find inspiration, I started browsing existing implementations, but they were all overloaded with logic. So I decided to find a mathematical foundation for building the hook, a conditionally pure function.

In my searches, I stumbled upon an interesting article on GeeksForGeeks about the window sliding technique. It wasn't related to pagination, but I borrowed a few ideas from it. It also reminded me of a previous project where I calculated the radius of a circle to evenly place triangles inside it. 🔺

  • Window sliding helped me understand how to "slide" through pages without extra conditions.
  • Radius math gave me the idea for centering—just divide the window in half.
  • Function purity became a priority: avoid hardcoded if-statements everywhere.

How My Pagination Works?

(parameters: current page, total, number of buttons to display, shift from center)

  • Find the window length from the total number of buttons. For example, if there are 7 buttons, subtract 2—that's the first and last button.
  • Calculate the offset from the center (middle) by dividing the window in half. This gives the number of buttons from the center, similar to a radius.
  • Calculate the page number from which the window build should start: current page minus the window middle.
  • Apply limits to not go beyond pagination boundaries: total minus window length. Find the minimum starting point for pagination. Create the pagination itself.

That's all—simple math, no extra logic!


Why Did I Decide to Build It This Way?

I wanted to create something maximally simple and universal. Though it took a bit of time, the concept is very straightforward. We get the desired number of visible buttons, cut off the static ones, find the middle with simple math, and then calculate the starting point.

In the end, we have almost a pure function that returns an array of numbers without any dots. I wanted to separate UI and logic, and I succeeded. I also tried to avoid hardcoded stuff with a ton of conditions as much as possible.


Why Did I Decide to Publish the Package?

After covering the hook with tests, I started measuring performance and saw that it ran a bit slower than the Vercel example. That wouldn't let me sleep 😅.

So I got to optimizing: replaced Array.from with a regular loop and applied a hack that prevents unnecessary array allocation. It was simple because I know the exact array length and can fill it by index.

As a result, I got a 22x performance boost compared to the Vercel example, and that's what pushed me to publish the package—because I couldn't find anything like it.

Thanks for your attention 🙌

Here's the hook code itself—simple, but powerful:

/** * Generates a page number array for pagination controls. * * - Always includes first and last page. * - Keeps the current page approximately centered. * - Supports optional manual shift to bias the window. * - Auto-adjusts if `visibleButtons < 5` and warns in console. * * @param currentPage Current page number (1-based, e.g. 1 for first page, 2 for second) * @param totalPages Total number of pages * @param visibleButtons Desired total number of buttons (including first & last). Minimum: 5 * @param shift Manual centering offset (positive = more buttons left, negative = more buttons right) * @note If currentPage > totalPages, the range will shift to show the last available pages. * @returns Array of 1-based page numbers for display, e.g. [1, 5, 6, 7, 50] * @throws {Error} If parameters are invalid * * @example * // Page 1 of 125 * useSimplePagination(1, 125, 7) * // → [1, 2, 3, 4, 5, 6, 125] * * @example * // Page 61 of 125 * useSimplePagination(61, 125, 7) * // → [1, 59, 60, 61, 62, 63, 125] * * @example * // With shift * useSimplePagination(6, 20, 7, 1) * // → [1, 3, 4, 5, 6, 7, 20] */ const validateInputs = ( currentPage: number, totalPages: number, visibleButtons: number, shift: number ): void => { if (!Number.isInteger(currentPage) || currentPage < 1) throw new Error("currentPage must be a positive integer (1, 2, 3, ...)"); if (!Number.isInteger(totalPages) || totalPages < 0) throw new Error("totalPages must be a non-negative integer"); if (!Number.isInteger(visibleButtons) || visibleButtons < 1) throw new Error("visibleButtons must be a positive integer"); if (!Number.isFinite(shift)) throw new Error("shift must be a finite number"); }; const renderPages = (length: number, start: number, last: number): number[] => { const paginationList = new Array(length + 2); paginationList[0] = 1; for (let i = 0; i < length; i++) paginationList[i + 1] = start + i; paginationList[paginationList.length - 1] = last; return paginationList; }; export const useSimplePagination = ( currentPage: number, totalPages: number, visibleButtons: number, shift: number = 0 ): number[] => { validateInputs(currentPage, totalPages, visibleButtons, shift); const MIN_BTN = 5; const fixedButtons = 2; const isMinimalCase = totalPages <= 1 || visibleButtons <= 1; const warnInfo = `Pagination: visibleButtons=${visibleButtons} is too small. Auto-adjusted to 5 for proper UX. Please use at least ${MIN_BTN} buttons.`; let adjustedVisibleButtons = visibleButtons; if (isMinimalCase) return totalPages >= 1 ? [currentPage] : []; if (visibleButtons < MIN_BTN) { console.warn(warnInfo); adjustedVisibleButtons = MIN_BTN; } const excludedStatic = Math.min(adjustedVisibleButtons, totalPages) - fixedButtons; const length = Math.max(0, excludedStatic); const halfWindow = Math.floor(length / 2) + shift; const getIdealStart = Math.max(currentPage - halfWindow, fixedButtons); const startPosition = Math.min(getIdealStart, totalPages - length); const paginationList = renderPages(length, startPosition, totalPages); return paginationList; };

What's Next?

Now this hook is ready for use in any project—from Next.js to plain React. Try it out yourself, and if you have ideas for improvements, drop them in the comments! Experiment with pagination—it's one of those moments when math makes code truly beautiful.

Dmytro notes