Search code examples
reactjsreact-hooksreact-router-domuse-effect

React useEffect to fetch data not finished yet when try to sort data using React Router useSearchParams


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?


Solution

  • 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]);