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}>
<
</button>
...
<button onClick={onNext} className="pagination-button" disabled={pageNo === maxPages}>
>
</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}
/>
));
