React useEffect请求未完成时useSearchParams排序异常问题
问题根源
你的代码存在两个核心问题,导致携带排序参数刷新页面时排序失效:
- 两个
useEffect的执行时序没有绑定:带query参数刷新页面时,监听queryParams的effect会在组件挂载后立刻执行,此时接口请求还未返回,movies还是初始空数组,排序操作等于对空数组执行,自然不会有效果。 - 违反React状态不可变原则:
Array.sort()是原地修改数组的方法,直接对movies状态调用sort会直接修改状态原值,很容易触发渲染不更新的隐性bug。
推荐解决方案
不要把排序后的结果存入movies状态,将接口返回的原始电影数据单独存储,渲染前根据当前query参数的排序规则做派生计算即可,从根源上避免两个effect互相等待的时序问题,后续扩展筛选、分页功能时也不会出现状态同步bug。
修改后的可运行代码如下:
import { Box, Button } from '@mui/material'; import { useEffect, useMemo, 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 [originalMovies, setOriginalMovies] = useState([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { const fetchMovies = async () => { setIsLoading(true); try { const fetchedMovies = await tmdb.get("trending/movie/week"); setOriginalMovies(fetchedMovies.data.results); } catch (error) { console.log(error); } finally { setIsLoading(false); } } fetchMovies(); }, []); // 通过useMemo派生排序后的列表,依赖原始数据和排序参数,任意一个变化都会自动重算 const sortedMovies = useMemo(() => { const sortType = queryParams.get('sort'); // 无排序参数或数据未加载完成时,直接返回原始数据 if (!sortType || originalMovies.length === 0) { return originalMovies; } // 先拷贝原数组再排序,避免原地修改状态 const moviesToSort = [...originalMovies]; if (sortType === 'asc') { return moviesToSort.sort((a, b) => a.vote_average - b.vote_average); } if (sortType === 'desc') { return moviesToSort.sort((a, b) => b.vote_average - a.vote_average); } return originalMovies; }, [originalMovies, queryParams]); const setSortParam = (type) => { // 创建新的URLSearchParams实例,避免原地修改原对象导致更新不触发 const newParams = new URLSearchParams(queryParams); newParams.set("sort", type); setQueryParams(newParams); } if (isLoading) { return <Box sx={{ mt:5 }}>电影数据加载中...</Box> } 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', }}> { sortedMovies.map(movie => ( // 用接口返回的唯一id作为key,避免重名电影导致的渲染错误 <MovieCard key={movie.id} movie={movie}></MovieCard> )) } </Box> </Box> ); } export default MovieList;
其他可选方案(不推荐)
如果你一定要保留双useEffect的串行执行逻辑,只需要给排序逻辑加非空判断,同时把movies加入effect依赖数组即可:
useEffect(() => { // 数据未加载完成时直接跳过,等movies更新后effect会自动重跑 if (movies.length === 0) return; const sortType = queryParams.get('sort'); if (!sortType) return; const sorted = [...movies].sort((a,b) => sortType === 'asc' ? a.vote_average - b.vote_average : b.vote_average - a.vote_average ); setMovies(sorted); }, [queryParams, movies]);
这种方式需要反复同步原始数据和排序后的数据,后续加筛选、分页等功能时很容易出现状态不同步的bug,不建议长期使用。
内容的提问来源于stack exchange,提问作者depimomo
相关产品推荐
相关产品推荐

