In this lesson, we will add pagination to our table. It won't be an easy one-minute process.
Modifying Controller for Pagination
app/Http/Controllers/TaskController.php
// ... public function index(){ return Inertia::render('Tasks/Index', [ 'tasks' => Task::all() 'tasks' => Task::paginate(5) ]);} // ...
Now, our Task List page will stop working because of a TypeScript structure mismatch. We return paginate()
from Controller, which is no longer the array of the Task
type. Let's fix this.
Creating Paginated Response Type
We return tasks
from the Controller not as a list but as a paginated object.
So, we need to create a specific new type for it, corresponding to the structure returned by Laravel pagination.
resources/js/types/index.d.ts
// ... export interface PaginatedResponse<T = Task | null> { current_page: number; data: T[]; first_page_url: string; from: number; last_page: number; last_page_url: string; links: { url: string | null; label: string; active: boolean; }[]; next_page_url: string | null; path: string; per_page: number; prev_page_url: string | null; to: number; total: number;}
Accepting Paginated Response in React
In three places, we need to change the type of data we get.
- We import our new
PaginatedResponse
type - We use it in the
export function
instead of the old list type - We change
tasks.map()
totasks.data.map()
because Laravel returns the data intasks.data
now
resources/js/pages/Tasks/Index.tsx
import { type BreadcrumbItem, type Task } from '@/types';import { type BreadcrumbItem, type PaginatedResponse, type Task } from '@/types'; // ... export default function Index({ tasks }: { tasks: Task[] }) {export default function Index({ tasks }: { tasks: PaginatedResponse<Task> }) { // ... return ( // ... {tasks.map((task: Task) => ( {tasks.data.map((task: Task) => ( <TableRow key={task.id}> // ...
Ok, now our page should load successfully again.
But wait... we still don't have pagination?
Pagination Component from Shadcn
Shadcn has a component of Pagination.
So, we install it with a regular npx
command.
npx shadcn@latest add pagination
We could use it right away, but I suggest considering the future when we will use the same pagination on other pages. So, we will build our own reusable pagination component suitable for our design.
Creating a "Simple" Pagination Component
Shadcn Pagination gives us the components like <Pagination>
and others. Here's an example of Usage from the docs:
Let's use those <Pagination>
, <PaginationContent>
, <PaginationItem>
and other components in our own new React component we'll call <TablePagination>
, custom to our own design.
For now, we will add only "Next" and "Previous" links.
resources/js/components/table-pagination.tsx
import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from '@/components/ui/pagination';import { type PaginatedResponse } from '@/types'; export function TablePagination({ resource }: { resource: PaginatedResponse }) { if (resource.last_page === 1) { return ( <div className={'mt-4 text-center text-gray-500'}> No more items to show. </div> ); } return ( <Pagination className='mt-4'> <PaginationContent> <PaginationItem> {resource.prev_page_url ? <PaginationPrevious href={resource.prev_page_url} /> : null } </PaginationItem> <PaginationItem> {resource.next_page_url ? <PaginationNext href={resource.next_page_url} /> : null } </PaginationItem> </PaginationContent> </Pagination> );}
Then, in our main component:
resources/js/pages/Tasks/Index.tsx
// Import firstimport { TablePagination } from '@/components/table-pagination'; // ... // Then use below <TablePagination resource={tasks} /> </div></AppLayout>
Here's the visual result:
If we click "Next", the URL changes, and we see the "Previous" button instead.
Now, if we want to build pagination with numbers, it's more complicated.
Pagination with Numbers
Shadcn is a CSS component library. It's for the presentation layer. It doesn't contain the logic for calculating and showing the page numbers. We need to handle this logic ourselves in custom JavaScript.
It's a pretty complex logic. Luckily, I've found a ready-made Gist called Shadcn UI implementation of DataTable and Pagination components working together. So, from that, we've created our own React component "helper". It doesn't return the visual HTML, so I put it in a resources/js/lib
subfolder, not resources/js/components
.
It will accept:
- Current page to mark the active page
- Total pages to determine the logic
- Path to generate the links for (e.g.
/tasks
) - Page query to append to the path (e.g.,
?page=
)
resources/js/lib/generate-pagination-links.tsx
import { PaginationEllipsis, PaginationItem, PaginationLink } from '@/components/ui/pagination';import { JSX } from 'react'; type PaginationLink = { url: string; label: string;} export const generatePaginationLinks = (currentPage: number, totalPages: number, path: string, links: PaginationLink[], pageQuery: string = '?page=') => { const pages: JSX.Element[] = []; console.log(links) if (totalPages <= 6) { for (let i = 1; i <= totalPages; i++) { pages.push( <PaginationItem key={i}> <PaginationLink href={links[i].url} isActive={i === currentPage}> {i} </PaginationLink> </PaginationItem>, ); } } else { for (let i = 1; i <= 2; i++) { pages.push( <PaginationItem key={i}> <PaginationLink href={links[i].url} isActive={i === currentPage}> {i} </PaginationLink> </PaginationItem>, ); } if (2 < currentPage && currentPage < totalPages - 1) { pages.push(<PaginationEllipsis />); pages.push( <PaginationItem key={currentPage}> <PaginationLink href="" isActive={true}> {currentPage} </PaginationLink> </PaginationItem>, ); } pages.push(<PaginationEllipsis />); for (let i = totalPages - 1; i <= totalPages; i++) { pages.push( <PaginationItem key={i}> <PaginationLink href={links[i].url} isActive={i === currentPage}> {i} </PaginationLink> </PaginationItem>, ); } } return pages;};
As you can see, it returns pages
. Now, how do we use those pages in the table?
Using Pagination Lib Helper in Component
Now we can use the pagination component in our Index
page:
resources/js/components/table-pagination.tsx
import { generatePaginationLinks } from '@/lib/generate-pagination-links'; // ... return ( <Pagination className='mt-4'> <PaginationContent> <PaginationItem> {resource.prev_page_url ? <PaginationPrevious href={resource.prev_page_url} /> : null } </PaginationItem> {generatePaginationLinks(resource.current_page, resource.last_page, resource.path, resource.links)}// [tl! ++] <PaginationItem> {resource.next_page_url ? <PaginationNext href={resource.next_page_url} /> : null } </PaginationItem> </PaginationContent> </Pagination>);
Now we have a proper numbered pagination:
Also, if we change the pagination to 10 records per page, we show the text on the bottom that there are no more records:
Here's the GitHub commit for the two lessons: this one about Pagination and the next one about Date Picker. (sorry, forgot to commit separately)
The shadcn pagination component uses a regular
<a>
tag for the links, so you'll notice that the pagination links cause a full page refresh. This can be remedied by updating the shadcn component to use the Inertia<Link>
component.The great thing about shadcn and the way that the Laravel Starter Kits are implemented is that you can modify the components in this way, so no messy extending. Just be careful not to overwrite the changes later!
Thanks for the valuable comment, Joe, great notice!
That's a great point Joe however I just tried to naively update resources/js/components/ui/pagination.tsx to use "Link" instead of "a" and got myself into all sorts of trouble. Anyone got some hints or a Gist?
Yeah, it's pretty easy to break. I ended up enlisting the help of AI to do it. Here's what we came up with...
First import the inertia
<Link>
at the top:Then redefine the
PaginationLinkProps
type slightly:Finally replace the
PaginationLink
function with this:Just an observation, after the custom
table-pagination.tsx
component is created, the step where the component is used is missing. This can be confusing to readers who are just following along. The following needs to be added to theTasks/Index.tsx
for the pagination to show;Wow, how could I miss this... fixed now! Thanks Steve for being a proofreader! Need to spend more time on double-checking everything in the future. Perhaps too much in a hurry to release everything related to Laravel 12, when people are asking so many question.
No worries my friend, happy to help! Hope to see you at Laravel Live London :)
I get error in console log. And I fixed it in resources/js/lib/generate-pagination-links.tsx by adding 2 keys to
PaginationEllipsis
key="ellipsis-before" and key="ellipsis-after"