使用startTransition仍触发Suspense同步输入报错问题求助
React Native中Suspense + startTransition + React Query报错排查
问题场景
在React Native中实现菜单/详情示例,使用<Suspense>和startTransition结合react-query做数据请求,运行时触发以下错误:
Error: A component suspended while responding to synchronous input. This will cause the UI to be replaced with a loading indicator. To fix, updates that suspend should be wrapped with startTransition. This error is located at: in Screen (created by StarWarsSuspenseApp) in QueryClientProvider (created by StarWarsSuspenseApp) in StarWarsSuspenseApp (created by Routes) in RenderedRoute (created by Routes) in Routes (created by App) in Router (created by MemoryRouter) in MemoryRouter (created by NativeRouter) in NativeRouter (created by App) in RCTSafeAreaView (created by SafeAreaView) in Styled(RCTSafeAreaView) (created by App) in App in RCTView (created by View) in View (created by AppContainer) in RCTView (created by View) in View (created by AppContainer) in AppContainer in Toy(RootComponent), js engine: hermes
尽管已用startTransition包裹状态更新,错误仍触发。
实现代码
import React, {useState, Suspense, startTransition} from 'react'; import { Button, Details, Heading, Menu, Text, Wrapper, } from './StarWarsSuspenseApp.styled'; import {QueryClient, QueryClientProvider, useQuery} from 'react-query'; const api = (entity: 'planets') => `https://swapi.dev/api/${entity}`; const EMPTY = ''; type Planet = { name: string; rotation_period: string; orbital_period: string; diameter: string; climate: string; gravity: string; terrain: string; surface_water: string; population: string; residents: string[]; films: string[]; created: string; edited: string; url: string; }; type ApiResponse<K> = { count: number; next: string; previous: null; results: K; }; const queryClient = new QueryClient({ defaultOptions: { queries: { suspense: true, }, }, }); const fetchWithErrors = async <T,>(request: Promise<Response>): Promise<T> => { const response = await request; if (!response.ok) { throw new Error('Network response was not ok'); } return response.json() as Promise<T>; }; const fetchPlanets = () => fetchWithErrors<ApiResponse<Planet[]>>(fetch(api('planets'))); const usePlanets = () => useQuery('planets', fetchPlanets); const fetchPlanetDetails = (url: string) => fetchWithErrors<ApiResponse<Planet>>(fetch(url)); const usePlanetDetails = (url: string) => useQuery(['planet', url], () => fetchPlanetDetails(url), { enabled: url !== EMPTY, }); const Screen = () => { const [selectedUrl, setSelectedUrl] = useState(EMPTY); const planets = usePlanets(); const planetDetails = usePlanetDetails(selectedUrl); const handlePlanetSelect = (url: string) => { startTransition(() => { setSelectedUrl(url); }); }; return ( <Wrapper> <Suspense fallback={<Heading>Loading Planets...</Heading>}> {planets.isLoading ? ( <Heading>Loading...</Heading> ) : planets.isError ? ( <Heading>{`${planets.error}`}</Heading> ) : ( <Menu> {planets?.data?.results.map(planet => ( <Button key={planet.name} title={planet.name} onPress={() => handlePlanetSelect(planet.url)} /> ))} </Menu> )} </Suspense> {selectedUrl !== EMPTY && ( <Suspense fallback={<Heading>Loading Planet Details...</Heading>}> {planetDetails.isLoading ? ( <Heading>Loading Details...</Heading> ) : planetDetails.isError ? ( <Heading>{`${planetDetails.error}`}</Heading> ) : ( <Details> <Text>{JSON.stringify(planetDetails.data, null, 2)}</Text> </Details> )} </Suspense> )} </Wrapper> ); }; const StarWarsSuspenseApp = () => ( <QueryClientProvider client={queryClient}> <Screen /> </QueryClientProvider> ); export default StarWarsSuspenseApp;
依赖版本
"react": "18.2.0", "react-native": "0.72.7", "react-query": "3.39.3",
问题原因及解决方案
核心问题
- Suspense模式与手动状态判断冲突:开启
query.suspense: true后,仍手动判断isLoading/isError,破坏了Suspense自动捕获组件挂起的逻辑,导致组件在同步渲染流程中挂起。 - 查询逻辑未隔离:在
Screen组件中直接执行详情查询,状态更新后查询立刻触发挂起,且该挂起发生在同步输入响应流程中,即使包裹了startTransition也无法避免错误。
修复步骤
移除手动加载/错误状态判断
Suspense模式下,React Query会自动通过Suspense fallback展示加载状态,无需手动判断isLoading/isError,直接访问data即可,未就绪时组件会自动挂起。拆分详情查询到独立组件
将星球详情的查询和渲染逻辑抽离为单独组件,让Suspense精准捕获该组件的挂起状态,避免在父组件中同步触发挂起。保留startTransition包裹状态更新
确保状态更新被标记为过渡更新,让React优先处理用户交互,再处理数据加载的渲染更新。
修复后的代码示例
// 拆分独立的详情组件 const PlanetDetail = ({ url }: { url: string }) => { const planetDetails = usePlanetDetails(url); // Suspense模式下直接访问data,未就绪时组件自动挂起 return ( <Details> <Text>{JSON.stringify(planetDetails.data, null, 2)}</Text> </Details> ); }; const Screen = () => { const [selectedUrl, setSelectedUrl] = useState(EMPTY); const planets = usePlanets(); const handlePlanetSelect = (url: string) => { startTransition(() => { setSelectedUrl(url); }); }; return ( <Wrapper> <Suspense fallback={<Heading>Loading Planets...</Heading>}> <Menu> {planets.data.results.map(planet => ( <Button key={planet.name} title={planet.name} onPress={() => handlePlanetSelect(planet.url)} /> ))} </Menu> </Suspense> {selectedUrl !== EMPTY && ( <Suspense fallback={<Heading>Loading Planet Details...</Heading>}> <PlanetDetail url={selectedUrl} /> </Suspense> )} </Wrapper> ); };
额外说明
- React Query的Suspense模式下,查询会在组件挂载且
enabled为true时自动执行,数据未就绪时组件挂起,由上层Suspense处理加载状态。 - 非Suspense模式才需要手动判断
isLoading/isError,两种模式不可混用,否则会导致Suspense无法正确工作。
内容的提问来源于stack exchange,提问作者Ashley Coolman
相关产品推荐
相关产品推荐

