React状态异常问题:保存文件或setTimeout可临时修复?
React状态更新异常与API异步处理问题
问题描述
我在Google和ChatGPT上搜遍了都没找到解决方案,实在头疼。没法理解React状态为啥会有这种异常表现:
我做了个新闻应用,从多个API拉取数据合并成数组,用列表展示,还支持筛选和筛选偏好持久化。
应用结构:App -> Home(Router-dom) -> News组件 -> List组件 + Filter组件,Home只放基础HTML和News组件,News负责维护新闻列表和筛选条件状态。
News组件代码
import React, {useEffect, useState} from 'react' import List from '../../components/List'; import SearchBar from '../../components/SearchBar'; import { FilterOptions, filter, getNews } from '../../utils/data'; import Filter from '../Filter'; import Cookies from 'js-cookie'; export interface NewsItem { id : string apiSource : "news_api" | "guardian_news" | "nyt_api"; author : string; description : string; publishedAt : string; source : string; image: string; title : string; url : string; } export const sourcingFrom = { news_api: "News API", guardian_news: "The Guardian Open Platform", nyt_api: "New York Times Article Search" } export type ApiSource = keyof typeof sourcingFrom; function News() { const [newsItems, setNewsItems] = useState([]) const [filters, setFilters] = useState<FilterOptions>({}) const updateFilters = (newFilters : FilterOptions) => { const updateFiltersQuery :FilterOptions = { ...filters, ...newFilters, } setFilters(prev => updateFiltersQuery) } const updateNews = (filterData: FilterOptions = {}) => { const filterRequest = Object.keys(filterData).length <= 0 ? filterData : filters console.log(filterRequest) filter(filterRequest).then(newNewsItems => { setTimeout(() => { setNewsItems(prev => newNewsItems); }, 500); }); }; const updateQuery = (e : string) => { const updateFiltersQuery :FilterOptions = { ...filters, query : e, } setFilters(prev => updateFiltersQuery) } const resetFilters = () => {} const resetPreferences = () => { Cookies.remove('filterPreferences'); } const savePreferences = () => { Cookies.set('filterPreferences', JSON.stringify(filters), { expires: 30 }); } useEffect(() => { const savedPreferences = Cookies.get('filterPreferences'); if(savedPreferences){ const filterData : FilterOptions = JSON.parse(savedPreferences) updateFilters(filterData); updateNews(filterData); } else { getNews().then(res => setNewsItems(res)); } }, []); return ( <section className='newsItems'> <SearchBar updateQuery={updateQuery} updateNews={updateNews} resetFilters={resetFilters}/> <Filter filter={updateFilters} savePreferences={savePreferences} resetPreferences={resetPreferences}/> <List newsItems={newsItems}/> </section> ) }
异常表现
- 必须在
updateNews里加setTimeout才能更新状态,这明显不合理。 - Cookie加载筛选条件后,完全触发不了请求,除非修改文件保存后才生效,加
setTimeout也没用。 - 去掉
setTimeout后,能拿到过滤后的newsItems,但状态就是不更新。
API调用代码(filter和getNews)
export const getNews = async (newAPIParameters :DefaultAPIParas ={}, guardianNewsParameters :DefaultAPIParas ={}, nYTAPIParameters :DefaultAPIParas ={}, dontCallAPIs : DontCallAPIs = {}) => { const newsData: any = []; if(!dontCallAPIs?.news_api) { getNewsApiData(newAPIParameters).then((res: any) => { for (let item of res) { const modifiedRes = { id: generateRandomString(), apiSource: "news_api", author: item.author, title: item.title, url: item.url, image: item.urlToImage, publishedAt: item.publishedAt, source: item.source?.name ?? "", description: item.content ?? item.description, }; newsData.push(modifiedRes); } }); } if(!dontCallAPIs?.guardian_api) { getGuardianNewsAPIData(guardianNewsParameters).then((res: any) => { for (let item of res) { const modifiedRes = { id: generateRandomString(), apiSource: "guardian_news", author: item.tags[0]?.webTitle ?? 'Not Determined', title: item.webTitle, url: item.webUrl, image: item.fields.thumbnail, publishedAt: item.webPublicationDate, source: "The Guardian News", description: item.fields.trailText, }; newsData.push(modifiedRes); } }); } if(!dontCallAPIs?.nyt_api) { getNYTAPIData(nYTAPIParameters).then((res: any) => { for (let item of res) { const modifiedRes = { id: generateRandomString(), apiSource: "nyt_api", author: item.byline.original?.replace('By ', '') ?? 'Not Determined', title: item.headline.main, url: item.web_url, image: "https://www.nytimes.com/"+item.multimedia[0]?.url ?? null, publishedAt: item.pub_date, source: "New York Times", description: item.abstract, }; newsData.push(modifiedRes); } }); } return await newsData; }; export const filter = async (filterOptions : FilterOptions) => { const {query = null, category = '', from = null,to = null,sources = {}} = filterOptions const dontCallAPIs : DontCallAPIs = {} if (Object.keys(sources).length !== 0) { dontCallAPIs['news_api'] = sources.news_api && sources?.news_api?.length <= 0; dontCallAPIs['guardian_api'] = !sources.guardian_api; dontCallAPIs['nyt_api'] = !sources.nyt_api; } const newAPIParameters ={ query : query ?? '', from : from ?? '', to : to ?? '', category : category ?? '', sources : sources.news_api?.join(', ') } const guardianNewsParameters = { query : query ?? '', from : from ?? '', to : to ?? '', category : category ?? '', } const nYTAPIParameters = { query : query ?? '', from : from ?? '', to : to ?? '', category : category ?? '', } return getNews(newAPIParameters, guardianNewsParameters, nYTAPIParameters, dontCallAPIs) }
问题根源分析
- API异步处理完全错误:
getNews是async函数,但内部用.then处理API请求却没有等待这些请求完成,直接返回了空数组newsData。await newsData毫无意义,因为newsData是普通数组不是Promise。导致你拿到的newNewsItems是空数组,后续API返回数据后才往数组里加,但此时React状态已经更新过了,而且数组是引用类型,React检测不到变化。 - 状态更新依赖过时闭包:
updateFilters和updateQuery里直接用当前filters状态构建新对象,React状态更新是异步的,多次连续调用可能会拿到旧的filters值。 - Cookie加载后状态未同步:
useEffect里调用updateFilters后立刻调用updateNews,但setFilters是异步的,此时filters还没更新,updateNews用的还是旧的空状态。
修复方案
1. 修复API异步逻辑
把getNews里的.then改成await,确保所有API请求完成后再返回数据:
export const getNews = async (newAPIParameters :DefaultAPIParas ={}, guardianNewsParameters :DefaultAPIParas ={}, nYTAPIParameters :DefaultAPIParas ={}, dontCallAPIs : DontCallAPIs = {}) => { const newsData: any = []; if(!dontCallAPIs?.news_api) { const res = await getNewsApiData(newAPIParameters); for (let item of res) { const modifiedRes = { id: generateRandomString(), apiSource: "news_api", author: item.author, title: item.title, url: item.url, image: item.urlToImage, publishedAt: item.publishedAt, source: item.source?.name ?? "", description: item.content ?? item.description, }; newsData.push(modifiedRes); } } if(!dontCallAPIs?.guardian_api) { const res = await getGuardianNewsAPIData(guardianNewsParameters); for (let item of res) { const modifiedRes = { id: generateRandomString(), apiSource: "guardian_news", author: item.tags[0]?.webTitle ?? 'Not Determined', title: item.webTitle, url: item.webUrl, image: item.fields.thumbnail, publishedAt: item.webPublicationDate, source: "The Guardian News", description: item.fields.trailText, }; newsData.push(modifiedRes); } } if(!dontCallAPIs?.nyt_api) { const res = await getNYTAPIData(nYTAPIParameters); for (let item of res) { const modifiedRes = { id: generateRandomString(), apiSource: "nyt_api", author: item.byline.original?.replace('By ', '') ?? 'Not Determined', title: item.headline.main, url: item.web_url, image: "https://www.nytimes.com/"+item.multimedia[0]?.url ?? null, publishedAt: item.pub_date, source: "New York Times", description: item.abstract, }; newsData.push(modifiedRes); } } return newsData; };
2. 修复状态更新的闭包问题
使用函数式更新,确保拿到最新的状态:
const updateFilters = (newFilters : FilterOptions) => { setFilters(prev => ({ ...prev, ...newFilters, })) }; const updateQuery = (e : string) => { setFilters(prev => ({ ...prev, query: e, })) };
3. 修复Cookie加载后的请求触发
监听filters变化,当filters更新时自动调用updateNews;或者直接在Cookie加载时用filterData直接请求,跳过依赖状态:
// 方案1:监听filters变化自动更新新闻 useEffect(() => { const savedPreferences = Cookies.get('filterPreferences'); if(savedPreferences){ const filterData : FilterOptions = JSON.parse(savedPreferences) updateFilters(filterData); } else { getNews().then(res => setNewsItems(res)); } }, []); // 新增监听逻辑 useEffect(() => { if(Object.keys(filters).length > 0){ updateNews(filters); } }, [filters]); // 修改updateNews逻辑 const updateNews = (filterData: FilterOptions | undefined) => { const filterRequest = filterData ?? filters; console.log(filterRequest) filter(filterRequest).then(newNewsItems => { setNewsItems(newNewsItems); }); };
或直接在Cookie加载时直接请求:
useEffect(() => { const savedPreferences = Cookies.get('filterPreferences'); if(savedPreferences){ const filterData : FilterOptions = JSON.parse(savedPreferences) updateFilters(filterData); // 直接用filterData请求,无需等待状态更新 filter(filterData).then(newNewsItems => { setNewsItems(newNewsItems); }); } else { getNews().then(res => setNewsItems(res)); } }, []);
4. 移除无用的setTimeout
修复API异步逻辑后,setTimeout完全不需要,直接调用setNewsItems即可。
内容的提问来源于stack exchange,提问作者Osama Anwar
相关产品推荐
相关产品推荐

