DEV Community

Cover image for Pagination: Your Guide to Not Breaking the Internet (Or Your Users' Patience)
The Witcher
The Witcher

Posted on

Pagination: Your Guide to Not Breaking the Internet (Or Your Users' Patience)

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...
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 πŸ†

  1. Always validate page parameters - Don't trust user input!
  2. Cache strategically - Popular pages should load instantly
  3. Handle edge cases - What happens when there's no data?
  4. Consider mobile users - Big pagination controls suck on small screens
  5. Add loading states - Users hate wondering if something broke
  6. Optimize your queries - Database indexes are your friends
  7. 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)