React pagination component number of item displayed issue

Viewed 62

I simply don't know why I have 10 Items displayed instead of 8? I'd like A hint as I'm not formidable on react.

On my demo I have 10 movies displaying on the page they should be 8. Not sure why the page PAGE_SIZE = 8 doesn't do the job.

Any help is much appreciated.

Thanks in advance demo

const PAGE_SIZE = 8;
2 Answers

You didn't put page size into query params so response has default 10 items. That is reason why there are 10 item that displayed See image below enter image description here

In your API documents, they don't support page size enter image description here

From what I can tell it seems the default page size for https://www.omdbapi.com/ is 10 instead of 8, and I don't see any overt way to specify a page size to request. This is why you are rendering 10 results per "page".

What I suggest to do is to update your logic to fetch all the movies and handle the paging on the app side.

const getMovies = (term='marvel') => {
  setLoading(true);
  axios.get(`https://www.omdbapi.com/?apikey=cbb48c03&s=${encodeURIComponent(term)}&plot=full`)
    .then(response => {
    setMovies(response.data.Search);
  }).catch(error => {
    setMovies([]);
  }).finally(() => setLoading(false));
}

Use the current page number and PAGE_SIZE to slice up the current page to render.

const genCards = () => {
  const allCards = [];
  if (movies) {
    movies.slice(pageNo * PAGE_SIZE, pageNo * PAGE_SIZE + PAGE_SIZE)
      .map(movie => {
        allCards.push(<Card {...movie} cardClicked={handleCardClicked} />);
      });
  }
  return allCards;
}

const allCards = genCards();

The total is derived state from the movies array length, so it shouldn't be stored in state. The initial pageNo should also be 0.

const [loading, setLoading] = useState(true);
const [showPop, setShowPop] = useState(false);
const [movies, setMovies] = useState([]);
const [movie, setMovie] = useState({});
const [keyword, setKeyword] = useState('marvel');
const [pages, setPages] = useState(1);
const [pageNo, setPageNo] = useState(0);

const maxPages = Math.floor(movies.length / PAGE_SIZE);

The next/previous page handlers should account for page numbers starting at 0.

const onNext = () => pageNo < maxPages && setPageNo(pageNo + 1);
const onPrev = () => pageNo > 0 && setPageNo(pageNo - 1);

And the same for the page count and current page, and button disabling:

<span>{movies.length} results found</span>
...
Page {pageNo + 1} of {maxPages + 1}
...

<button onClick={onPrev} className="pagination-button" disabled={pageNo === 0}>
  &lt;
</button>
...
<button onClick={onNext} className="pagination-button" disabled={pageNo === maxPages}>
  &gt;
</button>
...

Update

I've noticed that the search now is broken something from const maxPages.

I incorrectly used the length of the movies array which only accounts for what you've already fetched and not the total aggregate movie count. For this we will save the totalResults value from the response into local state.

const [totalResults, setTotalResults] = useState(0);

const maxPages = Math.floor(totalResults / PAGE_SIZE);

const getMovies = (term = "marvel", page = 1) => {
  ...
  axios
    .get(....)
    .then((response) => {
      ...
      setTotalResults(response.data.totalResults);
    })
    .catch((error) => {
      ...
    })
    .finally(() => setLoading(false));
};

also the search is not showing less results

The issue here is the API returns a message that there are too many matches, so it's not returning a Search array and the UI falls over when attempting to access the length property.

I just added a check for this error response and throw an error so the .catch can handle it.

const getMovies = (term = "marvel", page = 1) => {
  ...
  axios
    .get(....)
    .then((response) => {
      if (response.data.Error) throw new Error(response.data.Error);

      ...
      setTotalResults(response.data.totalResults);
    })
    .catch((error) => {
      // handle error here
    })
    .finally(() => setLoading(false));
};

I noticed also that this API returns only 10 results no matter what, so requesting per page is still required. To resolve this I worked out a relationship between paging by 8's and paging by 10's.

const PAGE_SIZE = 8;
// API page size is 10, we display 8 per page
const PAGE_SIZE_TO_PAGE_REQUEST_RATIO = 4 / 5; // 8:10 == 4:5

const getApiRequestPage = (page) =>
  Math.ceil(page * PAGE_SIZE_TO_PAGE_REQUEST_RATIO) + 1;
pageNo page by 8's results by 8's page by 10's results by 10's
0 1 8 1 10
1 2 16 2 20
2 3 24 3 30
3 4 32 4 40
4 5 40 5 50
5 6 48 5 50
6 7 56 6 60
7 8 64 7 70
8 9 72 8 80
9 10 80 9 90
10 11 88 9 90
11 12 96 10 100
... ... ... ... ...

Use a React ref to store the maximum page fetched from the API and only fetch the next page when pageNo has been incremented higher.

const maxPageFetched = useRef(-1);

useEffect(() => {
  const nextPage = getApiRequestPage(pageNo);
  if (nextPage > maxPageFetched.current) {
    getMovies(keyword, nextPage);
  }
}, [keyword, pageNo]);

When the search param changes then assume this begins a new series of fetches and paging, reset the state.

useEffect(() => {
  setMovies([]);
  setPageNo(0);
  setTotalResults(0);
  maxPageFetched.current = -1;
}, [keyword]);

The final issue to resolve had to do with the slicing and mapping of the movies array. The imdbID property seemed like a great candidate for use as a React key, but for some reason the API you are using doesn't filter very well and returns duplicate data. The absolutely correct solution is to file a bug with the repo. The slightly less correct solution is to filter out these duplicate entries in the front end, accounting for new total results length, etc... The quick & dirty solution is to simply generate your own unique GUIDs for each movie element fetched.

import { v4 as uuidV4 } from "uuid";

...

const getMovies = (term = "marvel", page = 1) => {
  setLoading(true);
  axios
    .get(
      `https://www.omdbapi.com/?apikey=cbb48c03&s=${encodeURIComponent(
        term
      )}&plot=full&page=${page}` // <-- fetch by page
    )
    .then((response) => {
      if (response.data.Error) throw new Error(response.data.Error);

      maxPageFetched.current = page; // <-- update last page fetched
      setMovies((movies) =>
        movies.concat( // <-- concat the next page
          response.data.Search.map((movie) => ({
            id: uuidV4(), // <-- map page results and add id property
            ...movie
          }))
        )
      );
      setTotalResults(response.data.totalResults);
    })
    .catch((error) => {
      // error
    })
    .finally(() => setLoading(false));
};

...

const allCards = movies
  .slice(pageNo * PAGE_SIZE, pageNo * PAGE_SIZE + PAGE_SIZE)
  .map((movie) => (
    <Card
      key={movie.id} // <-- use id as React key
      {...movie}
      cardClicked={handleCardClicked}
    />
  ));

Edit react-pagination-component-number-of-item-displayed-issue

Related