Open In App

How to implement pagination in React Redux Applications?

Pagination is a common requirement in web applications. It is very helpful when we wish to display or manage a large dataset on the website in an aesthetic manner. Whenever it comes to displaying a large number of data, state handling comes into the picture. The idea of pagination has a good binding with redux. In this article, we will have a look at how to implement pagination using the redux state management library.

Approach to Implement Pagination in React Redux

In the code, we will make a fetch request to a fake REST API (JSON Placeholder) that serves todos. We will display 10 todos per page. On every page change, 10 more todos will be displayed. To implement this pagination with redux, we will set the initial state of the redux slice with pagination parameters. We will also create an async thunk to fetch todos from the API based on pagiantion state. Along the way, we will include the reducer to update the current page.

Steps to Create React App and Installing Modules

Step 1: Create a react app and enter into the directory created by running the following commands.

npx create-react-app redux-pagination
cd redux-pagination

Step 2: Install the required dependencies

npm install @reduxjs/toolkit react-redux redux-thunk

Project Structure:

red

project structure

Updated Dependencies: Open package.json to verify the following dependencies.

"dependencies": {
"@reduxjs/toolkit": "^2.2.1",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-scripts": "5.0.1",
"redux-thunk": "^3.1.0",
"web-vitals": "^2.1.4"
}

Example: Here is the working example of pagination using redux in react app.

/* src/App.css */

/* App.css */

.container {
    max-width: 600px;
    margin: 0 auto;
    padding: 20px;
}

.top-text {
    font-size: 20px;
    font-weight: 500;
    color: rgb(28, 158, 37);

}

.t-2-text {
    font-size: 12px;
    text-decoration: underline;
}

.fetch-button {
    padding: 10px 20px;
    margin-bottom: 20px;
    font-size: 16px;
    background-color: #f2176b;
    color: #fff;
    border: none;
    border-radius: 3px;
    cursor: pointer;
    transition: background-color 0.3s;
}

.fetch-button:hover {
    background-color: #bf2419;
}

.loading {
    margin-top: 20px;
    font-size: 60px;
}

.todos-list {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 12px;
    font-size: 8px;
}

.cards {
    padding: 8px;
    border-radius: 5px;
}

.top {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 10px;
}

.loader {
    width: 20px;
    --b: 3px;
    aspect-ratio: 1;
    border-radius: 50%;
    padding: 1px;
    background: conic-gradient(#0000 10%, #f03355) content-box;
    -webkit-mask:
        repeating-conic-gradient(#0000 0deg, #000 1deg 20deg, #0000 21deg 36deg),
        radial-gradient(farthest-side, #0000 calc(100% - var(--b) - 1px), #000 calc(100% - var(--b)));
    -webkit-mask-composite: destination-in;
    mask-composite: intersect;
    animation: l4 1s infinite steps(10);
}

@keyframes l4 {
    to {
        transform: rotate(1turn)
    }
}


.pagination {
    display: flex;
    gap: 24px;
    justify-content: center;
    align-items: center;
}

.pagination button {
    background-color: rgb(56, 122, 228);
    color: white;
    padding: 6px 12px;
    border-radius: 4px;
    border: none;
}
// src/redux/store.js
 
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './todoSlice';
 
const store = configureStore({
    reducer: {
        todos: todosReducer,
    },
});
 
export default store;
// src/redux/userSlice.js

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// Define the number of items per page
const ITEMS_PER_PAGE = 10; 

// Define an initial state for pagination
const initialPaginationState = {
    currentPage: 1,
    totalPages: 1,
    totalItems: 0
};

export const fetchTodos = createAsyncThunk(
    'todos/fetchTodos',
    // Adjust the thunk to use pagination parameters
    async (_, { getState }) => { 
        try {
            const { currentPage } = getState().todos.pagination;
            const start = (currentPage - 1) * ITEMS_PER_PAGE;
            const response = await fetch(
`https://jsonplaceholder.typicode.com/todos?_start=${start}&_limit=${ITEMS_PER_PAGE}`);
            if (!response.ok) {
                throw new Error('Failed to fetch todos');
            }
            const totalCountHeader = response.headers.get('x-total-count');
            const totalItems = 
                totalCountHeader ? 
                    parseInt(totalCountHeader, 10) : 0;
            const todos = await response.json();
            return { todos, totalItems };
        } catch (error) {
            throw new Error('Error fetching todos');
        }
    }
);

const todosSlice = createSlice({
    name: 'todos',
    initialState: {
        data: [],
        loading: false,
        error: null,
        pagination: initialPaginationState 
        // Include pagination state in initial state
    },
    reducers: {
        // Add a reducer to update current page
        setCurrentPage: (state, action) => { 
            state.pagination.currentPage = action.payload;
        }
    },
    extraReducers: (builder) => {
        builder
            .addCase(fetchTodos.pending, (state) => {
                state.loading = true;
                state.error = null;
            })
            .addCase(fetchTodos.fulfilled, (state, action) => {
                state.loading = false;
                state.data = action.payload.todos;
                state.pagination.totalItems = action.payload.totalItems;
                state.pagination.totalPages = 
                    Math.ceil(action.payload.totalItems / ITEMS_PER_PAGE);
            })
            .addCase(fetchTodos.rejected, (state, action) => {
                state.loading = false;
                state.error = action.error.message;
            });
    },
});

// Export setCurrentPage action
export const { setCurrentPage } = todosSlice.actions; 

export default todosSlice.reducer;
// src/App.js

import React,
{
    useEffect
} from 'react';
import {
    useDispatch,
    useSelector
} from 'react-redux';
import { fetchTodos, setCurrentPage } from './redux/todoSlice';
import './App.css';
import Loader from './components/Loader';

function App() {
    const dispatch = useDispatch();
    const todos = useSelector((state) => state.todos);

    const randomLightColor = () => {
        const randomColor =
            () =>
                Math.floor(Math.random() * 200) + 56;
        return `rgb(${randomColor()}, ${randomColor()}, ${randomColor()})`;
    };

    useEffect(() => {
        dispatch(fetchTodos());
    }, [dispatch]);
    // Fetch todos on component mount

    const handlePrevPage = () => {
        if (todos.pagination.currentPage > 1) {
            dispatch(setCurrentPage(todos.pagination.currentPage - 1));
            dispatch(fetchTodos());
        }
    };

    const handleNextPage = () => {
        if (todos.pagination.currentPage < todos.pagination.totalPages) {
            dispatch(setCurrentPage(todos.pagination.currentPage + 1));
            dispatch(fetchTodos());
        }
    };

    return (
        <div className="container">
            <div className='top'>
                <h1 className='top-text'>GFG Todo Display App</h1>
            </div>
            <div className='top'>
                {todos.loading && <p className="loading"><Loader /></p>}
                <div className="todos-list">
                    {todos.data?.map((todo) => (
                        <div className='cards'
                            style={{backgroundColor:randomLightColor()}}
                            key={todo.id}>{todo.title}</div>
                    ))}
                </div>
                <div className="pagination">
                    <button className="pagination-button"
                        onClick={handlePrevPage}
                        disabled={todos.pagination.currentPage === 1}>
                        prev
                    </button>
                    <span className="pagination-info">
                        Page {todos.pagination.currentPage}
                        of {todos.pagination.totalPages}
                    </span>
                    <button className="pagination-button"
                        onClick={handleNextPage}
                        disabled={todos.pagination
                                       .currentPage === todos.pagination
                                                                .totalPages}>
                        next
                    </button>
                </div>
            </div>
        </div>
    );
}

export default App;
// src/index.js
 
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store';
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
        <App />
    </Provider>
);
// src/components/Loader.js

import React from 'react'

const Loader = () => {
    return (
        <div className='loader'></div>
    )
}

export default Loader

Output:

npm start

2024-03-1109-13-33-ezgifcom-video-to-gif-converter


Article Tags :