Hey there, fellow developers! π
Ever tried loading a million records at once and watched your app crash harder than a Windows 95 machine? Yeah, we've all been there. That's where pagination comes to save the day (and your server bills).
What the Heck is Pagination Anyway?
Think of pagination like reading a book. You don't try to read all 500 pages at once β you go page by page. Same concept applies to data. Instead of loading thousands of records and making your users wait forever (or worse, crashing their browser), you serve data in bite-sized chunks.
Why Should You Care?
- Performance: Your app won't feel like it's running on a potato
- User Experience: Nobody likes staring at loading spinners for eternity
- Server Resources: Your poor database will thank you
- Mobile Friendly: Less data = happier mobile users = lower data bills
Types of Pagination (Choose Your Fighter)
1. Offset-Based Pagination (The Classic)
This is your bread-and-butter pagination. Think "Page 1, 2, 3..." like Google search results.
Pros:
- Easy to implement
- Users can jump to any page
- Shows total pages/results
Cons:
- Gets slower as you go deeper (page 1000 anyone?)
- Data inconsistency issues (someone adds a record while you're browsing)
2. Cursor-Based Pagination (The Modern Choice)
Uses a "cursor" (usually an ID or timestamp) to know where to continue from.
Pros:
- Consistent performance regardless of page depth
- No duplicate results when data changes
- Perfect for real-time feeds
Cons:
- Can't jump to random pages
- Slightly more complex to implement
3. Infinite Scroll (The Instagram Way)
Automatically loads more content as you scroll down.
Pros:
- Smooth user experience
- Great for mobile
- Feels modern and sleek
Cons:
- Can be memory-intensive
- Harder to find specific items
- SEO challenges
Show Me the Code! π
Java Backend (Spring Boot) - Offset-Based
Let's start with a simple REST API that serves paginated user data:
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public ResponseEntity<PagedResponse<User>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy,
@RequestParam(defaultValue = "asc") String sortDir) {
PagedResponse<User> users = userService.getUsers(page, size, sortBy, sortDir);
return ResponseEntity.ok(users);
}
}
// Service layer
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public PagedResponse<User> getUsers(int page, int size, String sortBy, String sortDir) {
Sort sort = sortDir.equalsIgnoreCase("desc") ?
Sort.by(sortBy).descending() :
Sort.by(sortBy).ascending();
Pageable pageable = PageRequest.of(page, size, sort);
Page<User> userPage = userRepository.findAll(pageable);
return new PagedResponse<>(
userPage.getContent(),
userPage.getNumber(),
userPage.getSize(),
userPage.getTotalElements(),
userPage.getTotalPages(),
userPage.isLast()
);
}
}
// Response wrapper
public class PagedResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
private int totalPages;
private boolean last;
// constructors, getters, setters...
}
Java Backend - Cursor-Based (For the Cool Kids)
@GetMapping("/cursor")
public ResponseEntity<CursorPagedResponse<User>> getUsersWithCursor(
@RequestParam(required = false) String cursor,
@RequestParam(defaultValue = "10") int size) {
CursorPagedResponse<User> users = userService.getUsersWithCursor(cursor, size);
return ResponseEntity.ok(users);
}
// In service
public CursorPagedResponse<User> getUsersWithCursor(String cursor, int size) {
List<User> users;
if (cursor == null) {
// First page
users = userRepository.findTopNOrderByIdAsc(size + 1);
} else {
Long cursorId = Long.parseLong(cursor);
users = userRepository.findByIdGreaterThanOrderByIdAsc(cursorId, size + 1);
}
boolean hasNext = users.size() > size;
if (hasNext) {
users.remove(users.size() - 1); // Remove extra item
}
String nextCursor = hasNext && !users.isEmpty() ?
users.get(users.size() - 1).getId().toString() : null;
return new CursorPagedResponse<>(users, nextCursor, hasNext);
}
React Frontend - The Fun Part! βοΈ
Here's a complete pagination component that works with our Java backend:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const UserList = () => {
const [users, setUsers] = useState([]);
const [currentPage, setCurrentPage] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [loading, setLoading] = useState(false);
const [pageSize, setPageSize] = useState(10);
useEffect(() => {
fetchUsers();
}, [currentPage, pageSize]);
const fetchUsers = async () => {
setLoading(true);
try {
const response = await axios.get('/api/users', {
params: {
page: currentPage,
size: pageSize,
sortBy: 'name',
sortDir: 'asc'
}
});
setUsers(response.data.content);
setTotalPages(response.data.totalPages);
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setLoading(false);
}
};
const handlePageChange = (newPage) => {
if (newPage >= 0 && newPage < totalPages) {
setCurrentPage(newPage);
}
};
const renderPaginationButtons = () => {
const buttons = [];
const maxVisiblePages = 5;
// Previous button
buttons.push(
<button
key="prev"
onClick={() => handlePageChange(currentPage - 1)}
disabled={currentPage === 0}
className="px-3 py-1 mx-1 bg-blue-500 text-white rounded disabled:bg-gray-300"
>
Previous
</button>
);
// Page numbers
let startPage = Math.max(0, currentPage - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(totalPages - 1, startPage + maxVisiblePages - 1);
for (let i = startPage; i <= endPage; i++) {
buttons.push(
<button
key={i}
onClick={() => handlePageChange(i)}
className={`px-3 py-1 mx-1 rounded ${
i === currentPage
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{i + 1}
</button>
);
}
// Next button
buttons.push(
<button
key="next"
onClick={() => handlePageChange(currentPage + 1)}
disabled={currentPage === totalPages - 1}
className="px-3 py-1 mx-1 bg-blue-500 text-white rounded disabled:bg-gray-300"
>
Next
</button>
);
return buttons;
};
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">User List</h1>
{/* Page size selector */}
<div className="mb-4">
<label className="mr-2">Items per page:</label>
<select
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setCurrentPage(0); // Reset to first page
}}
className="border p-1 rounded"
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
{/* Loading state */}
{loading ? (
<div className="text-center py-4">Loading... π</div>
) : (
<>
{/* User list */}
<div className="grid gap-4">
{users.map(user => (
<div key={user.id} className="border p-4 rounded shadow">
<h3 className="font-semibold">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</div>
))}
</div>
{/* Pagination controls */}
<div className="mt-6 flex justify-center items-center">
{renderPaginationButtons()}
</div>
{/* Page info */}
<div className="mt-4 text-center text-gray-600">
Page {currentPage + 1} of {totalPages}
</div>
</>
)}
</div>
);
};
export default UserList;
Infinite Scroll Component (Because Why Not?)
import React, { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
const InfiniteUserList = () => {
const [users, setUsers] = useState([]);
const [cursor, setCursor] = useState(null);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const fetchUsers = useCallback(async (reset = false) => {
if (loading) return;
setLoading(true);
try {
const response = await axios.get('/api/users/cursor', {
params: {
cursor: reset ? null : cursor,
size: 20
}
});
const newUsers = response.data.content;
setUsers(prev => reset ? newUsers : [...prev, ...newUsers]);
setCursor(response.data.nextCursor);
setHasMore(response.data.hasNext);
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setLoading(false);
}
}, [cursor, loading]);
useEffect(() => {
fetchUsers(true);
}, []);
useEffect(() => {
const handleScroll = () => {
if (window.innerHeight + document.documentElement.scrollTop
!== document.documentElement.offsetHeight || loading || !hasMore) {
return;
}
fetchUsers();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [fetchUsers, loading, hasMore]);
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Infinite User List</h1>
<div className="grid gap-4">
{users.map((user, index) => (
<div key={`${user.id}-${index}`} className="border p-4 rounded shadow">
<h3 className="font-semibold">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</div>
))}
</div>
{loading && (
<div className="text-center py-4">
Loading more users... π
</div>
)}
{!hasMore && (
<div className="text-center py-4 text-gray-500">
You've reached the end! π
</div>
)}
</div>
);
};
export default InfiniteUserList;
When to Use What?
Use Offset-Based When:
- Users need to jump to specific pages
- You need to show total count/pages
- Data doesn't change frequently
- Building admin panels or search results
Use Cursor-Based When:
- You have large datasets
- Data changes frequently (like social feeds)
- Performance is critical
- Building real-time applications
Use Infinite Scroll When:
- Building mobile-first apps
- Content is consumed sequentially
- You want that modern social media feel
- Users are browsing rather than searching
Pro Tips for Pagination Success π
- Always validate page parameters - Don't trust user input!
- Cache strategically - Popular pages should load instantly
- Handle edge cases - What happens when there's no data?
- Consider mobile users - Big pagination controls suck on small screens
- Add loading states - Users hate wondering if something broke
- Optimize your queries - Database indexes are your friends
- Think about SEO - Search engines need to crawl your paginated content
Common Pitfalls (Learn from My Mistakes!)
- The "Page 1 of β" Problem: Always handle cases where you can't calculate total pages
- The Disappearing Data: When someone deletes a record while you're paginating
- The Memory Leak: Infinite scroll without cleanup will eat RAM for breakfast
- The Mobile Nightmare: Tiny pagination buttons that nobody can tap
- The Database Killer: Not using proper indexes on your sort columns
Wrapping Up
Pagination might seem simple, but there's definitely more to it than meets the eye. The key is understanding your use case and picking the right approach. Need users to jump around? Go offset-based. Building the next TikTok? Cursor-based is your friend. Want that smooth scrolling experience? Infinite scroll it is!
Remember, good pagination is invisible to users β they just know your app feels fast and responsive. Bad pagination? Well, let's just say you'll hear about it in the reviews. π
Happy coding, and may your pages load swiftly! π
Got questions or suggestions? Drop a comment below! And if this helped you out, consider sharing it with your fellow developers who are still loading everything at once like it's 2005.
Top comments (0)