I have a React component that try to load movies first from tmdb API. Then, I create a button to sort the movies using query params. I want to use query params because I want to preserve the URL so that I can share the URL with sort value. (This is for my learning purpose also).
When the page is already rendered and I click the button, it's work fine.
But when I try to reload the page with query params, the query params useEffect is run first before the movies finished loaded.
Here is my code:
import { Box, Button } from '@mui/material';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import tmdb from '../apis/tmdb';
import MovieCard from '../components/MovieCard';
const MovieList = () => {
const [queryParams, setQueryParams] = useSearchParams();
const [movies, setMovies] = useState([]);
useEffect(() => {
const fetchMovies = async () => {
try {
const fetchedMovies = await tmdb.get("trending/movie/week");
setMovies(fetchedMovies.data.results);
} catch (error) {
console.log(error);
}
}
fetchMovies();
}, []);
useEffect(() => {
const sortMovies = (type) => {
if (type === 'asc') {
const sorted = movies.sort((a, b) => a.vote_average - b.vote_average);
setMovies(sorted);
}
if (type === 'desc') {
const sorted = movies.sort((a, b) => b.vote_average - a.vote_average);
setMovies(sorted);
}
}
sortMovies(queryParams.get('sort'));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [queryParams]);
const setSortParam = (type) => {
queryParams.set("sort", type);
setQueryParams(queryParams);
}
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
mt: 5,
}}>
<Box sx={{
mt: 5,
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center',
}}>
Sort by Rating
<Button
variant="contained"
sx={{ ml: 2 }}
onClick={() => setSortParam("asc")}
>
Asc
</Button>
<Button
variant="contained"
sx={{ ml: 2, mr: 2 }}
onClick={() => setSortParam("desc")}
>
Desc
</Button>
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
}}>
{
movies.map(movie => (
<MovieCard key={movie.title} movie={movie}></MovieCard>
))
}
</Box>
</Box>
);
}
export default MovieList;
I try to create new state to track if the movies length > 0, but sometimes it's buggy. I also try to useRef to track if the movies length > 0 but it's not working.
Is there anyway to wait for first useEffect to finish? Or should I use other approach than useEffect to read query params and sort?
I think you are trying to merge the movies
state (i.e. the source of truth) with the derived state (i.e. the sorting order of movies). Derived state generally doesn't belong in state. You can easily derive the sorted movies
array from the movies
state and the current sort
queryString parameter value.
Example:
import { Box, Button } from "@mui/material";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import tmdb from "../apis/tmdb";
import MovieCard from "../components/MovieCard";
// Sort comparators
const sortVoteAverageAsc = (a, b) => a.vote_average - b.vote_average;
const sortVoteAverageDesc = (a, b) => sortVoteAverageAsc(b, a);
// Sorting utility function
const sortVoteAverage = (sort = "asc") =>
sort === 'asc' ? sortVoteAverageAsc : sortVoteAverageDesc;
const MovieList = () => {
const [queryParams, setQueryParams] = useSearchParams();
const [movies, setMovies] = useState([]);
useEffect(() => {
const fetchMovies = async () => {
try {
const fetchedMovies = await tmdb.get("trending/movie/week");
setMovies(fetchedMovies.data.results);
} catch (error) {
console.log(error);
}
};
fetchMovies();
}, []);
const setSortParam = (type) => {
queryParams.set("sort", type);
setQueryParams(queryParams);
};
// create shallow copy of movies state, then sort by sort query parameter
const sortedMovies = movies
.slice()
.sort(sortVoteAverage(queryParams.get("sort")));
return (
<Box
...
>
<Box
...
>
Sort by Rating
<Button
variant="contained"
sx={{ ml: 2 }}
onClick={() => setSortParam("asc")}
>
Asc
</Button>
<Button
variant="contained"
sx={{ ml: 2, mr: 2 }}
onClick={() => setSortParam("desc")}
>
Desc
</Button>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between"
}}
>
{sortedMovies.map((movie) => (
<MovieCard key={movie.title} movie={movie} />
))}
</Box>
</Box>
);
};
If needed, you can memoize the sortedMovies
value using the useMemo
hook with a dependency on the movies
state and the current sort
queryString parameter value.
Example:
const sort = queryParams.get("sort");
const sortedMovies = useMemo(() => {
return movies
.slice()
.sort(sortVoteAverage(sort))
}, [movies, sort]);