I've done a simple menu/detail example in ReactNative with Suspense
and 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;
But I get this error
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
Packages
"react": "18.2.0",
"react-native": "0.72.7",
"react-query": "3.39.3",
Can anyone see the problem?
The
handlePlanetSelect
function is called in response to a user action and should be wrapped instartTransition
: