Når vi bruker apper i dag er ikke flaskehalsen lite datakraft. Det er heller variasjon i nettverk, enten vi må vente på systemer som snakker sammen eller vi tilfeldigvis befinner oss i en tunnel.
Derfor beveger React seg mot async som standard: vi bygger grensesnitt som antar at operasjoner kan være trege. Når noe går sakte, får brukeren feedback fra første tastetrykk til data lastes inn. Når nettverket er raskt vil appen føles umiddelbar.
Dette høres jo kjempebra ut. Så jeg testet det ut. Med nye React-funksjoner som use, useTransition, suspense og ViewTransition. Resultatet for brukeren er mer responsive grensesnitt, men som utvikler er det mange snublefeller på veien dit.
I denne posten får du bli med på frustrasjonene fra da jeg testet ut async React. Jeg viser hva fellene sier om hvordan React egentlig fungerer, og hva de avslører om hvordan React-teamet ser for seg at vi bygger apper fremover.
Tilbakemelding mens vi venter på data
Suspense er en av måtene vi gir brukeren tilbakemelding mens vi venter på at noe skjer. For å aktivere suspense, trenger vi å hente data med et verktøy som aktivererer suspense. For det kan vi bruke den nye use-funksjonen.
F ør jeg introduserer use, ta en titt på hvordan vi kan gjøre datahenting uten et bibliotek for datahenting — med useEffect:
// ActivitiesPage
export function ActivitiesPage() {
const [activities, setActivities] = useState<Activity[]>([]);
// 👇 Holder rede på laste-tilstand
const [loading, setLoading] = useState(true);
useEffect(() => {
// 👇 Ved mount henter vi data
const initialFetch = async () => {
setLoading(true);
// 👇 Setter responsdata til useState
const response = await fetchActivities();
setActivities(response);
setLoading(false);
};
initialFetch();
}, []);
...
return (
// 👇 Betinget viser fallback med loading- tilstanden
{loading ? (
<ActivityListFallback />
) : (
<ListActivities
activities={activities}
onToggle={onToggle}
/>
)}
)
...Med useEffect henter vi data, setter loading-tilstand manuelt, og setter responsdataene til useState.
Med use-funksjonen ser ting litt annerledes ut:
// ActivitiesPage
export function ActivitiesPage() {
// 👇 state holder på promise - ikke responsdata
const [activitiesPromise, setActivitiesPromise] = useState<
Promise<Activity[]>
>(() => fetchActivities());
...
return (
/*
👇 Istedenfor manuell loading-tilstand,
vil fallback vises så lenge promise ikke er resolved
*/
<Suspense fallback={<ActivityListFallback />}>
<ListActivities
activitiesPromise={activitiesPromise}
onToggle={onToggle}
/>
</Suspense>
)
// ListActivities
function ListActivities({ activitiesPromise, onToggle }: Props) {
// 👇 Promise resolves i child
const activities = use(activitiesPromise);
...Her var det litt av hvert å pakke ut.
For å starte med slutten, use-funksjonen. use brukes til å konsumere dataene.
Litt som når du awaiter et promise:
// const activities = await fetchActivities();
const activities = use(activitiesPromise);Forskjellen er at use-funksjonen vil suspende promiset frem til det er ferdig. Det betyr at fallback- verdien i Suspense vil vises:
// ActivitiesPage
<Suspense fallback={<ActivityListFallback />}>
<ListActivities
activitiesPromise={activitiesPromise}
onToggle={onToggle}
/>
</Suspense>Det kan se slik ut:

Når promiset til slutt resolves (altså har suksess), vil barna i Suspense bli vist, og dataene fra promiset kan leses av. Om promiset skulle bli rejecta (altså ved feil), blir nærmeste ErrorBoundary aktivert.
At promise-et blir kasta av use-funksjonen gjør at disse laste-tilstandene og feil-tilstandene koordineres automatisk. Så må du selv legge opp til Suspense- og Error- grenser der det passer seg.
Hente nye data etter endring
For å mutere data, kan vi sette tilstand med ny fetch:
// ActivitiesPage
async function onToggle(id: string) {
// 👇 API-kall for å endre complete-status på aktivitet
await toggleActivity(id);
// 👇 Hente ny aktiviteter og sette promise-et med setState
setActivitiesPromise(fetchActivities());
}Det fungerte! Men du ser kanskje noen problemer?

Selv om jeg bare endrer én ting, vises hele fallback-lista for aktivitetene.
Det kan være rett i noen tilfeller, men jeg ville at dataene lastes inn i bakgrunnen ved toggling. Det løste jeg med useDeferredValue:
// ActivitiesPage
const deferredPromise = useDeferredValue(activitiesPromise);useDeferredValue sier at oppdateringen av verdien er i lavprioritet, så det kan skje ved neste render. Det leder til at gammel data vises mens promise-et for ny data kan laste, uten å trigge ny fallbackverdi.
Og da fikk jeg kun fallback-verdi ved første innlastning:

Respons mens vi endrer
Vi klarte å endre data etter brukeren sjekket av en handling, men det tar lang tid før brukeren forstår at noe skjer. Det kan vi løse med transitions.
Ved å bruke useTransition kan vi starte en handling, og mens den handlingen skjer, har vi en laste-status:
// ActivityListItem
export function ActivityListItem({
activity,
onToggle,
}: Props) {
/*
👇 isPending er lastestatus mens transition pågår,
som vi så kan bruke for å vise laste-tilstand i UI-et
*/
const [isPending, startTransition] = useTransition();
async function handleToggle() {
// 👇 startTransition gjør om en handling til en transition
startTransition(async () => {
await onToggle(activity.id);
});
}Det ser slik ut:

Optimistisk visning
Brukeren får litt feedback, med laste-indikator (lavere opacity), men det føles ikke veldig responsivt når check-merket kommer par sekunder senere.
Det kan vi løse med optimistisk oppdatering, via useOptimistic-hooken:
// ActivityListItem
export function ActivityListItem({
activity,
onToggle,
}: Props) {
/*
👇 useOptimistic lar deg enklere lage optimistiske oppdateringer,
hvor du antar verdien før API-kallet responderer.
Verdien rulles tilbake til start-verdi (her activity.completed)
etter transition er ferdig.
*/
const [optimisticCompleted, setOptimisticCompleted] = useOptimistic(
activity.completed
);
const [isPending, startTransition] = useTransition();
async function handleToggle() {
startTransition(async () => {
//👇 Ved toggle setter vi ny verdi med en gang.
setOptimisticCompleted(!optimisticCompleted);
await onToggle(activity.id);
});
}
return (
...
<input
type="checkbox"
checked={optimisticCompleted}
onChange={handleToggle}
/>Nå har vi umiddelbar respons!

Men her ser vi flikring. Vi får umiddelbar respons, så flikrer det mellom check-status, så blir det rett igjen.
Årsaken er:
- først setter vi optimistisk verdi (checked)
- Så rulles verdien tilbake idet transition i item er ferdig til startverdi (un-checked)
- Så blir ny datahenting ferdig og settes til rett verdi (checked)
Grunnen til at ny datahenting ikke inkluderes i transition, er at enn så lenge vil tilstands-oppdateringer etter await i en transition ikke markeres som transition. Derfor må vi legge til en transition på setActivitiesPromise:
// ActivitiesPage
// 👇 Vi sender ned funksjon til child
async function onToggle(id: string) {
// 👇 Muterer activity
await toggleActivity(id);
/*
👇 må starte ny transition her,
siden tilstands-oppdatering etter await
ikke blir merket som transitions.
Uten transition her, får vi flikring.
*/
startTransition(() => {
setActivitiesPromise(fetchActivities());
});
}
// ActivityListItem
async function handleToggle() {
startTransition(async () => {
// 👇 Optimistisk verdi bevares frem til transitions er ferdig
setOptimisticCompleted(!optimisticCompleted);
await onToggle(activity.id);
});
}Og resultatet blir:

Veiled brukeren med animasjoner
Men én siste ting. La oss få inn animasjoner. For med den nye ViewTransition- komponenten kan vi pakke inn elementer som er i transition, og få en jevn overgang fra det ene snapshotet til det andre:
/*
👇 ViewTransition animerer fra snapshotet før en transition
til etter.
*/
<ViewTransition>
<Suspense fallback={<ActivityListFallback />}>
<ListActivities
activitiesPromise={deferredPromise}
onToggle={onToggle}
/>
</Suspense>
</ViewTransition>Og resultatet blir slik:

Fjooo! Ingen flikring, umiddelbar respons oooog animasjoner!
Async React i tre lag
Det kan hende du nå stiller meg samme spørsmål som jeg stilte Chattern:

Tja.
Ricky Hanlon sier at vi i fremtiden ikke kommer til å skrive all transition-funksjonalitet selv. I stedet kan vi forvente at data- og routing bibliotek håndterer det for oss.
Det var Ricky Hanlons foredrag som pirret nysgjerrigheten min på async React. Han forklarer hvordan de har jobbet med dette lenge, og hvordan de ser for seg fretiden blir.
Foredraget er oppstykket, på grunn av noen utfordringer i demoen (async React er vanskelig!).
Se Ricky Hanlons foredrag fra React Conf her:

Mye av async React er støttet allerede. For datahenting kan vi bytte ut use med useSuspenseQuery fra TanStack Query, som Petter viste i en tidligere kalenderluke. For routing har flere router-bibliotek transition-støttet routing, som routingen i Next.js.
Fra designbibliotek, som shadcn, kan vi etter hvert forvente at transitions bare funker. Da vil vi som konsumere bare sende en handling ned i en prop som benytter seg av action-mønsteret — som vi gjorde med handleToggle — og forvente at propen blir wrappa i en transition.
Ettersom vi utviklere sjelden implementerer egne router- eller dataløsninger, tror jeg det er på designlaget vi kommer til å forholde oss til async React en god stund fremover.
React-teamet har startet en arbeidsgruppe for å gjøre transitions enklere — og å lære det bort. Allerede har de drodlet på noen ideer, som å bytte ut datahentings-eksempelet i React docs fra å bruke useEffect til å bruke use.
Fremtiden til React er async
Async React er komplekst, men i fremtiden vil vi få mye gratis. Der raske apper føles umiddelbare og trege apper føles forståelige. Inntil da er det verdt å kjenne til hvordan de underliggende funksjonene fungerer — og de tilhørende snublefellene.