<ForrigeUke uke="44" år="2025" />
Alt om overganger og hvorfor fremtidens React er async — i denne ukas ForrigeUke.
Dette var uka for pai og imperativ programmering — og 3713 ting skjedde i frontendverdenen!
«<ForrigeUke /> er en artikkelserie som oppsummerer hva som skjedde i frontend-verden i uka som var.»
Jeg har lenge slitt med å forstå hva funksjoner som useTransition, useDeferredValue og ikke minst hva use() gjør. Sistnevnte har jo et helt tullete navn — men forrige uke forstod jeg endelig hva den var godt for.
Jack Herrington slapp en ti minutters video som oppsummerer hva use gjør — og hvordan det henger sammen med Suspense, Transitions og ViewTransitions. Dette er verktøy for å støtte "Async React". Kort sagt innebærer det å la arbeid som å laste inn et element skje på lavprioritet, så andre viktige ting som brukerinput blir prioritert og gjennomført raskere.
Motivasjonen min for å dykke ordentlig ned i transitions kom etter Ricky Hanlons foredrag "Async React" på React Conf (video del 1, slutter før demo. Video del 2, starter demo). Han snakket om hvordan vi bør tenke på React som asynkront som standard, selv for funksjoner som egentlig ikke er asynkrone.

Dette implementeres ved å bruke transitions for å koordinere arbeid.
For en treg enhet, vil transitions hjelpe å gi respons når brukeren trykker, venter på svar eller får en fin overgang til nytt innhold. Mens på en rask enhet vil appen fortsatt føles umiddelbar.
Med nyvunnet motivasjon gikk jeg løs på å lære meg disse nye funksjonene. Selvfølgelig via en dårlig forkledd todo-app.
Hva gjør use()?
Appen jeg lekte med så slik ut, hvor jeg kan huke av jeg du har gjennomført noen ukomfortable aktiviteter:

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:
// ListActivities
const activities = use(activitiesPromise);use() brukes til å konsumere dataene. Litt som når du awaiter et promise:
const activities = await fetchActivities();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.
Mutering og use()
Da jeg selv lekte meg frem med use(), var det deilig å se hvor mye kode som kunne droppes ved henting av data. Men da jeg ville endre dataene, her huke av en aktivitet, var det verre.
I Herringtons video, var ny datahenting automatisk med parameteren som endret seg, trigget en re-render nå med ny paramater, så skjedde en ny datahenting i initiell verdi med useState-en. Han endret ikke dataene direkte med setState. Mens jeg ville mutere en aktivitet, så hente dataene på nytt. Jeg leita gjennom mange bloggposter, mange videoer og måtte til slutt ty til Chattern. Den ga meg mange løsninger, men ingen av dem hadde jeg helt troa på:

Til slutt prøvde jeg min naive tilnærming ved å kjøre setState igjen med ny fetchActivities:
// 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 ble løst 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:

Dette var fremgang. Men brukeren får lite respons når hen trykker på en toggle. 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:

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:
// 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)
Det vi ønsker er at optimistisk verdi bevares frem til ny datahenting er ferdig. Det kan vi koordinere ved å bruke transition:
// ActivitiesPage
// 👇 Vi sender ned funksjon til child
async function onToggle(id: string) {
// 👇 Muterer activity
await toggleActivity(id);
// 👇 Vi venter til ny datahenting er ferdig
const nextData = await fetchActivities();
/*
👇 Vi wrapper setState i transition,
så tilstands-setting ikke skjer med engang.
Uten transition her, får vi flikring.
*/
startTransition(() => {
setActivitiesPromise(Promise.resolve(nextData));
});
}
// ActivityListItem
async function handleToggle() {
startTransition(async () => {
// 👇 Optimistisk verdi bevares frem til transitions er ferdig
setOptimisticCompleted(!optimisticCompleted);
await onToggle(activity.id);
});
}Og resultatet blir:

Men én siste ting. La oss få inn animasjoner. For med den nye ViewTransition- komponenten kan vi wrappe 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!
Fremtiden til React er async
Det kan hende du nå stiller meg samme spørsmål som jeg stilte Chattern:

Tja.
I Hanlons foredrag sier han 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.

En del av dette er støttet allerede. For eksempel kan vi bytte ut use() med useSuspenseQuery fra TanStack Query. Og Next.js har transition-støttet routing.
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.
React-teamet, med Hanlon, 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().
For det ser ut som at transitions er fremtiden til React. Det er bygd for å koordinere arbeid i React — både for enklere kode for deg, og for en bedre brukeropplevelse for alle.
👋 Det var alt for denne gang. Ha en riktig fin uke!
