useReducer によるページネーションの状態管理


useReducerでページネーション機能を実装した時のメモです.

useReducer とは何か、いつ使用するか



the official document によると、useReducer は useState の代替です. useReducer が useState よりも優れている主な状況が 2 つあります.

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





when the next state depends on the previous one



ページ番号とページに要素を描画する配列、その他のページネーション関連の状態を同時に管理する必要があるため、useReducer を使用することにしました.

基本構文



useReducer の基本的な構文は次のとおりです.

const [state, dispatch] = useReducer(reducer, initialState)


**(state, action) => newState ** タイプのレデューサーを受け取り、現在の状態とディスパッチのペアを返します.これは Redux と非常によく似た構文です.

実際のコードを使用した段階的な手順



これが実際のコードです.

1️) 初期状態を設定

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


2) レデューサーを定義する

    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,
        };
      }
    };


if ステートメントの代わりに switch ステートメントを使用できます.

3) コンポーネント関数内で呼び出す

    const [searchState, dispatchSearch] = useReducer(searchReducer, initialState);


4) タイプごとにディスパッチメソッドを呼び出す

タイプ:「ページネーション」

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


タイプ: 「クエリ」

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


タイプ:「更新」

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


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>
      );
    };


読んでくれてありがとう :)
感想など頂けたら嬉しいです!

元の記事は here です