Having trouble paginating search results using useState, Material-UI and custom hook

I am having an issue with pagination on a page in my React application. On the page, search results are rendered when one types into the search bar (naturally). I think my issue arises from how pagination is set up on this page.

Pagination works fine as long as the user clicks back to the first page before searching for anything else. For example, if the user is on page 3 and then types something new into the search bar, the new search will not display without the user clicking ‘page 1’ again on the pagination bar. However if they returned to page 1 of their initial search before doing the new search, page 1 of the new search displays properly. Hopefully this makes sense. Here is the page where the issue occurs:

import React, { useState } from "react";
import Pagination from "@material-ui/core/Pagination";
import usePagination from "./usePagination.js";

export default function Main({reviews, web3}) {

    const [search, setSearch] = useState("");
    const [page, setPage] = useState(1);
    
    const updateSearch = (event) => {
        setSearch(event.target.value.substr(0, 20));
    }

    let filteredReviews = reviews.filter(
        (review) => {
            return review.restaurantName.indexOf(web3.utils.toHex(search)) !== -1;
    });

    let paginatedReviews = usePagination(filteredReviews, 2);

    const handleChange = (e, p) => {
        setPage(p);
        paginatedReviews.jumpPage(p);
    }

    return (
        <div className="container-fluid mt-5" style={{ minHeight: "100vh" }}>
        <div className="row">
            <main role="main" className="col-lg-12 ml-auto mr-auto" style={{ maxWidth: '500px' }}>
                <div className="content mr-auto ml-auto">
                <input type="text" className="form-control" value={search} onChange={updateSearch} />
                {filteredReviews.length > 0 ? paginatedReviews.pageData().map((review, key) => {
                    return (
                    <>
                        <div key={key}>
                            // search result item
                        </div>
                    </>
                    )
                })
                {filteredReviews.length > 1
                ? <Pagination
                      count={paginatedReviews.maxPage}
                      page={page}
                      onChange={handleChange}
                  />
                : null
                )
                </div>
            </main>
        </div>
        </div>
    );
}

and here is usePagination:

import { useState } from "react";

export default function usePagination(allReviews, perPage) {
    
    const [currentPage, setCurrentPage] = useState(1);

    const maxPage = Math.ceil(allReviews.length / perPage);

    function pageData() {
        const start = (currentPage - 1) * perPage;
        const end = start + perPage
    
        return allReviews.slice(start, end);
    }

    function jumpPage(page) {
        const pageNumber = Math.max(1, page);
        setCurrentPage((currentPage) => Math.min(pageNumber, maxPage));
    }

    return { jumpPage, pageData, currentPage, maxPage }
}

I thought I could resolve the issue I’m having by adding setPage(1) to updateSearch in order to have the page automatically move to page 1 for each new search, but that didn’t work, as you still had to click page 1 on the actual pagination bar for the results to show up.

Edit: I tried renaming currentPage and setCurrentPage in the hook so that they shared the same names as on my page, but that also did not work.

Thanks in advance for any help you can offer. If you need me to elaborate on anything I will happily do so.

Answer

How about updating the page in a useEffect? That way you’ll make sure all hooks have run and their return values are up-to-date (useEffect runs after render). If you reset the page too early, at the same time as the search query, jumpPage might rely on stale data: your search results and the internal usePagination values like maxPage will not have had a chance to recalculate yet.

Here is a working example based off your codesandbox: https://codesandbox.io/s/restless-dust-28351 Note that to make sure useEffect runs on search change only, you need to wrap jumpPage in a useCallback so that the jumpPage function reference remains the same. In general, I’d recommend you do that to methods returned from custom hooks. This way those methods are safer to consume anywhere, including as dependencies to useEffect, useCallback etc.

Also I’d recommend destructuring the custom hook return values, so that each of them can be used on its own as a hook dependency, like jumpPage in my example above.

I’ve also removed the page state from App, as it’s already tracked in usePagination and returned from there. Having usePagination as a single source of truth that encapsulates all your pagination stuff makes things simpler. Simplicity is a great ideal to strive for:)

Lastly, a small side note: it’s best not use <br /> purely as a spacer. It clutters up the markup without contributing any useful semantics, and it’s better to leave the spacing concern to CSS.

And good luck with your React endeavors, you’re doing great!