DEV Community

Cover image for A pagination state management with useReducer
Yuko
Yuko

Posted on

A pagination state management with useReducer

This is my memo when I implemented a pagination feature with useReducer.

What is useReducer and when to use it

According to the official document, useReducer is an alternative to useState. It says that there are mainly two situations which useReducer is better than useState:

when you have complex state logic that involves multiple sub-values

and

when the next state depends on the previous one

I decided to use useReducer because it is necessary to simultaneously manage page numbers and an array whose elements are rendered in the page as well as other pagination related states.

Basic Syntax

The basic syntax of useReducer is the below.

const [state, dispatch] = useReducer(reducer, initialState)
Enter fullscreen mode Exit fullscreen mode

It accepts a **(state, action) => newState **type reducer and returns a pair of current state and dispatch. This is syntax is quite similar to Redux.

Step by step procedure with my actual code

This is the actual code.

1️) Set initial state

    const initialState = {
      results: [],
      query: "",
      totalItems: 0,
      startIndex: 0,
      page: 1,
    };
Enter fullscreen mode Exit fullscreen mode

2) Define a reducer

    const searchReducer = (currntState, action) => {
      if (action.type === "UPDATE") {
        return {
          ...currntState,
          results: action.results,
          totalItems: action.totalItems,
        };
      }

    if (action.type === "QUERY") {
        return {
          ...currntState,
          query: action.query,
          startIndex: 0,
          page: 1,
        };
      }

    if (action.type === "PAGINATION") {
        return {
          ...currntState,
          page: action.page,
          startIndex: (action.page - 1) * 20 - 1,
        };
      }
    };
Enter fullscreen mode Exit fullscreen mode

You can use switch statement instead of if statement.

3) Call it inside of component function

    const [searchState, dispatchSearch] = useReducer(searchReducer, initialState);
Enter fullscreen mode Exit fullscreen mode

4) Call dispatch method with each type respectively

type:”PAGINATION”

    const pageChangeHandler = (event, newValue) => {
        dispatchSearch({ type: "PAGINATION", page: newValue });
      };
Enter fullscreen mode Exit fullscreen mode

type: “QUERY”

    const searchHandler = () => {
        setInit(false);
        sendGetRequest(searchInputRef.current.value);
        dispatchSearch({
          type: "QUERY",
          query: searchInputRef.current.value,
        });
        searchInputRef.current.value = "";
      };
Enter fullscreen mode Exit fullscreen mode

type:”UPDATE”

    useEffect(() => {
        if (loadedBooksData && !init) {
          dispatchSearch({
            type: "UPDATE",
            results: loadedBooksData.results,
            totalItems: loadedBooksData.totalItems,
          });
        }
      }, [loadedBooksData, init]);
Enter fullscreen mode Exit fullscreen mode

Here is the whole code snippet of SearchBooksComponents.

    import { Fragment, useRef, useReducer, useEffect, useState } from "react";
    import { Route, Routes } from "react-router-dom";
    import SearchIcon from "[@mui/icons-material](http://twitter.com/mui/icons-material)/Search";
    import Pagination from "[@mui/material](http://twitter.com/mui/material)/Pagination";
    import useHttp from "../hooks/use-http";
    import BookList from "./BookList";
    import LoadingSpinner from "../UI/LoadingSpinner";
    import ErrorMessage from "../UI/ErrorMessage";
    import BookDetail from "./BookDetail";

    import { getSearchBooks } from "../lib/api";
    import classes from "./SearchBooks.module.css";

    const initialState = {
      results: [],
      query: "",
      totalItems: 0,
      startIndex: 0,
      page: 1,
    };
    const searchReducer = (currntState, action) => {
      if (action.type === "UPDATE") {
        return {
          ...currntState,
          results: action.results,
          totalItems: action.totalItems,
        };
      }

    if (action.type === "QUERY") {
        return {
          ...currntState,
          query: action.query,
          startIndex: 0,
          page: 1,
        };
      }

    if (action.type === "PAGINATION") {
        return {
          ...currntState,
          page: action.page,
          startIndex: (action.page - 1) * 20 - 1,
        };
      }
    };

    const SearchBooks = () => {
      return (
        <Routes>
          <Route path="" element={<SearchBooksComponents />} />
          <Route path=":bookId/*" element={<BookDetail />} />
        </Routes>
      );
    };
    export default SearchBooks;

    const SearchBooksComponents = () => {
      const [init, setInit] = useState(true);
      const [searchState, dispatchSearch] = useReducer(searchReducer, initialState);
      const searchInputRef = useRef("");
      const {
        sendRequest: sendGetRequest,
        status,
        data: loadedBooksData,
        error,
      } = useHttp(getSearchBooks, true);

    const searchHandler = () => {
        setInit(false);
        sendGetRequest(searchInputRef.current.value);
        dispatchSearch({
          type: "QUERY",
          query: searchInputRef.current.value,
        });
        searchInputRef.current.value = "";
      };

    const pageChangeHandler = (event, newValue) => {
        dispatchSearch({ type: "PAGINATION", page: newValue });
      };

    useEffect(() => {
        if (searchState.query && !init) {
          sendGetRequest(searchState.query, searchState.startIndex);
        }
      }, [searchState.query, searchState.startIndex, sendGetRequest, init]);

    useEffect(() => {
        if (loadedBooksData && !init) {
          dispatchSearch({
            type: "UPDATE",
            results: loadedBooksData.results,
            totalItems: loadedBooksData.totalItems,
          });
        }
      }, [loadedBooksData, init]);
      return (
        <Fragment>
          <div className={classes.search}>
            <input type="text" placeholder="search books" ref={searchInputRef} />
            <button type="submit" onClick={searchHandler}>
              <SearchIcon sx={{ fontSize: 35 }} />
            </button>
          </div>
          {status === "loading" && !init && <LoadingSpinner />}
          {status === "completed" && error && <ErrorMessage />}
          {status === "completed" && !error && (
            <Fragment>
              <BookList results={searchState.results} />
              <div className={classes.pagination}>
                <Pagination
                  count={Math.ceil(searchState.totalItems / 20)}
                  page={searchState.page}
                  onChange={pageChangeHandler}
                />
              </div>
            </Fragment>
          )}
        </Fragment>
      );
    };
Enter fullscreen mode Exit fullscreen mode

Thank you for reading :)
I'm happy if you give me some feedback!

The original article is here

Top comments (0)