≡ Menu

When building modern web applications, handling large datasets efficiently is a must-have skill. If you try to load 1,000 items at once, your page will lag, and your users will leave. The solution is Server-Side Pagination.

In this tutorial, we’ll build a robust pagination system that fetches data from a live API, handles dynamic button generation, and includes “smart” ellipsis (...) for a professional UI.


1. The Strategy: “The Sliding Window”

Instead of showing every page button, we use a “Sliding Window” logic. This keeps the UI clean by only showing:

  1. The First and Last pages.

  2. A small Range (2 pages) around the current page.

  3. Ellipses (…) to bridge the gaps.


2. The Full Implementation

Here is the complete, single-file solution. You can save this as index.html and run it immediately.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Professional API Pagination</title>
    <style>
        :root { --primary: #007bff; --bg: #f4f7f6; --text: #333; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: var(--bg); color: var(--text); padding: 40px 20px; max-width: 700px; margin: auto; }
        
        h2 { text-align: center; color: #444; }
        
        /* List Styling */
        #list { list-style: none; padding: 0; min-height: 400px; }
        .post-item { background: #fff; border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); transition: transform 0.2s; }
        .post-item:hover { transform: translateY(-2px); }
        .post-item h3 { margin: 0 0 8px 0; font-size: 1.1rem; color: var(--primary); }
        
        /* Controls Styling */
        .controls { display: flex; gap: 8px; justify-content: center; align-items: center; margin-top: 30px; flex-wrap: wrap; }
        button { padding: 8px 16px; border: 1px solid var(--primary); background: #fff; color: var(--primary); cursor: pointer; border-radius: 4px; font-weight: 600; }
        button:hover:not(:disabled) { background: var(--primary); color: white; }
        button.active { background: var(--primary); color: white; }
        button:disabled { border-color: #ccc; color: #ccc; cursor: not-allowed; }
        .dots { color: #888; padding: 0 5px; font-weight: bold; }
        
        .loading { text-align: center; font-weight: bold; color: var(--primary); display: none; }
    </style>
</head>
<body>

    <h2>Published Posts</h2>
    <div id="loading" class="loading">Fetching Data...</div>
    <ul id="list"></ul>

    <div class="controls">
        <button id="prevBtn">Previous</button>
        <div id="page-numbers"></div>
        <button id="nextBtn">Next</button>
    </div>

    <script>
        // State Management
        let currentPage = 1;
        const itemsPerPage = 6;
        let totalPages = 0;

        // DOM Elements
        const listEl = document.getElementById('list');
        const pageNumbersEl = document.getElementById('page-numbers');
        const loadingEl = document.getElementById('loading');

        /**
         * Fetches data from API using page and limit parameters
         */
        async function fetchPosts(page) {
            loadingEl.style.display = 'block';
            listEl.style.opacity = '0.5';
            
            try {
                const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_page=${page}&_limit=${itemsPerPage}`);
                const data = await response.json();

                // Calculate total pages from the API header
                const totalCount = response.headers.get('x-total-count');
                totalPages = Math.ceil(totalCount / itemsPerPage);

                renderPosts(data);
                renderButtons();
            } catch (error) {
                listEl.innerHTML = `<p style="color:red">Error loading data. Please try again.</p>`;
            } finally {
                loadingEl.style.display = 'none';
                listEl.style.opacity = '1';
            }
        }

        /**
         * Renders the list of posts
         */
        function renderPosts(posts) {
            listEl.innerHTML = posts.map(post => `
                <li class="post-item">
                    <h3>${post.id}. ${post.title.substring(0, 40)}...</h3>
                    <p>${post.body.substring(0, 80)}...</p>
                </li>
            `).join('');
        }

        /**
         * Renders the smart pagination buttons with ellipses
         */
        function renderButtons() {
            pageNumbersEl.innerHTML = '';
            const range = 1; // How many pages to show around the current page
            let lastRenderedPage = 0;

            for (let i = 1; i <= totalPages; i++) {
                // Logic: Always show first, last, and the range around current page
                const isFirstOrLast = i === 1 || i === totalPages;
                const isWithinRange = i >= currentPage - range && i <= currentPage + range;

                if (isFirstOrLast || isWithinRange) {
                    
                    // If there's a gap, add dots
                    if (lastRenderedPage && i - lastRenderedPage > 1) {
                        const dots = document.createElement('span');
                        dots.innerText = '...';
                        dots.className = 'dots';
                        pageNumbersEl.appendChild(dots);
                    }

                    const btn = document.createElement('button');
                    btn.innerText = i;
                    btn.className = i === currentPage ? 'active' : '';
                    btn.onclick = () => {
                        currentPage = i;
                        fetchPosts(i);
                        window.scrollTo({ top: 0, behavior: 'smooth' });
                    };

                    pageNumbersEl.appendChild(btn);
                    lastRenderedPage = i;
                }
            }

            // Update disabled state for arrows
            document.getElementById('prevBtn').disabled = currentPage === 1;
            document.getElementById('nextBtn').disabled = currentPage === totalPages;
        }

        // Arrow Button Listeners
        document.getElementById('prevBtn').onclick = () => {
            if (currentPage > 1) fetchPosts(--currentPage);
        };

        document.getElementById('nextBtn').onclick = () => {
            if (currentPage < totalPages) fetchPosts(++currentPage);
        };

        // Initial Load
        fetchPosts(currentPage);
    </script>
</body>
</html>

3. How the “Smart Dots” Logic Works

The magic happens in the renderButtons function. We track the lastRenderedPage. If the current page i we are about to draw is more than 1 step away from the lastRenderedPage, we know there is a gap.

For example, if we just drew page 1 and the next page the logic allows is page 8, the script sees that 8 - 1 > 1 and injects the ... span automatically.


4. Key Takeaways

  • Performance: We only fetch 6 items at a time, making the app incredibly light.

  • Scalability: This code works just as well for 1,000 pages as it does for 5.

  • User Experience: We included window.scrollTo so that when a user clicks a page, they are taken back to the top of the list.

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

{ 0 comments }

In the world of JavaScript, there are two ways to solve a problem.

You can tell the computer exactly how to do something (Imperative), or you can describe what the result should look like using math-like logic (Functional).

As applications grow in complexity, the “Imperative” way often leads to “spaghetti code”—where changing a variable on line 10 breaks a feature on line 500.

Functional Programming (FP) is the antidote.

By mastering three core patterns—Pure Functions, Immutability, and Currying—you can write code that is predictable, testable, and incredibly easy to reason about.


1. The Foundation: Pure Functions

A function is “pure” when it behaves like a reliable machine: you put the same raw materials in, and you always get the exact same product out.

The Two Rules of Purity:

  1. Predictability: Given the same arguments, it always returns the same result.

  2. No Side Effects: It does not modify any state outside its own scope (no changing global variables, no fetching data, no console.log).

The Code:

// ❌ Impure: Depends on external state and modifies it
let tax = 0.5;
const calculateTotal = (price) => {
  tax = tax + 0.1; // Side effect!
  return price + (price * tax);
};

// ✅ Pure: Self-contained and predictable
const calculateTotalPure = (price, taxRate) => {
  return price + (price * taxRate);
};

Why use it? Pure functions are a dream for testing. You don’t need to “set up” a complex environment to test them; you just pass an input and check the output.


2. The Safety Net: Immutability

In JavaScript, objects and arrays are “mutable.” This means if you pass an array to a function, that function can accidentally change the original array.

Immutability is the practice of never changing data. Instead of modifying an existing object, you create a copy with the changes applied.

The Code:

const state = { user: 'Alex', points: 10 };

// ❌ Mutable: Changes the original object
const updatePoints = (player) => {
  player.points = 20; 
};

// ✅ Immutable: Returns a new object using the Spread Operator (...)
const updatePointsPure = (player) => {
  return { ...player, points: 20 };
};

Why use it? Immutability makes “Time Travel Debugging” possible. Since you never destroy old state, you can keep a history of every change in your app, making it nearly impossible to lose data to “stealth” bugs.


3. The Specialized Tool: Currying

Currying sounds intimidating, but it’s actually a simple trick: it’s the process of taking a function that takes multiple arguments and turning it into a series of functions that each take one argument.

The Code:

// The standard way
const multiply = (a, b) => a * b;

// The Curried way
const curriedMultiply = (a) => (b) => a * b;

// Why bother? Because you can create "Pre-configured" functions!
const double = curriedMultiply(2); 
const triple = curriedMultiply(3);

console.log(double(10)); // 20
console.log(triple(10)); // 30

Why use it? Currying allows you to create highly reusable “specialty” functions from a single general-purpose function. It’s perfect for handling configuration or repetitive logic.


Putting it All Together: The Pipeline

When you combine these three, you get Composition. You can pipe your data through a series of small, pure, curried functions like an assembly line.

// A simple pipeline
const getName = (user) => user.name;
const uppercase = (str) => str.toUpperCase();
const getGreeting = (name) => `HELLO, ${name}!`;

const user = { name: 'emily' };

// Functional Composition
const welcomeMessage = getGreeting(uppercase(getName(user)));
// "HELLO, EMILY!"

Summary

Functional Programming isn’t about being “smarter” than other developers; it’s about being safer.

  • Pure Functions remove surprises.

  • Immutability preserves your data’s integrity.

  • Currying lets you build a library of reusable tools.

The next time you’re about to update a global variable or write a massive 5-argument function, ask yourself: “How would a functional programmer do this?”

Useful links below:

Let me & my team build you a money making website/blog for your business https://bit.ly/tnrwebsite_service

Get Bluehost hosting for as little as $1.99/month (save 75%)…https://bit.ly/3C1fZd2

Best email marketing automation solution on the market! http://www.aweber.com/?373860

Build high converting sales funnels with a few simple clicks of your mouse! https://bit.ly/484YV29

Join my Patreon for one-on-one coaching and help with your coding…https://www.patreon.com/c/TyronneRatcliff

Buy me a coffee ☕️https://buymeacoffee.com/tyronneratcliff

 

{ 0 comments }