React.js, part 17: startTransition with async functions and the useEffectEvent escape hatch
Part 17from the React.js series · 20 parts in all
Two ergonomic gaps closed by the mid-2020s: transitions that wrap asynchronous work, and a principled way for an effect to read the latest value without re-subscribing. Both look small and both remove a category of stale-closure bugs.
Async transitions
A transition used to accept only synchronous work — an await inside it, and
React had already left the transition by the time the promise settled. React now keeps the
transition open across awaits, so pending state and the "keep showing the old UI" behavior
cover the whole operation:
const [isPending, startTransition] = useTransition();
function selectTab(tabId) {
// The whole async operation stays inside one transition: a spinner for the
// duration, and the old tab's content kept on screen while the data loads.
startTransition(async () => {
const data = await loadTab(tabId);
setTab(tabId);
setData(data);
});
}
The practical effect is that in-place navigation stops flashing a fallback: the previous
screen stays painted, dimmed by your own isPending styling, until the new one is
ready.
useEffectEvent: the latest value without a dependency
The perennial effect problem: a long-lived subscription needs to report analytics with the current user id, but adding it to the dependency array re-subscribes on every change. The old workarounds were a ref or turning the whole thing off in a linter. The proper tool makes the intent explicit:
'use client';
import { useEffect, useEffectEvent } from 'react';
function Chat({ roomId, userId }) {
// Not a dependency: the event always sees the latest props, but changing
// them does not re-run the effect.
const onMessage = useEffectEvent((msg) => {
log('message', { roomId, userId, len: msg.length });
});
useEffect(() => {
const socket = openSocket(roomId); // reconnects only when roomId changes
socket.on('message', onMessage);
return () => socket.close();
}, [roomId]); // userId deliberately absent
return <Room roomId={roomId} />;
}
The rule that keeps it honest
An effect event is for code that responds to an event — a socket message, a timer tick, a resize. It must not be called during render, and it must not be used to smuggle a changing value into a computation that should genuinely re-run. If the effect's work depends on the value at the moment it starts, that value belongs in the dependency array, not in an effect event. Next: testing this whole stack without writing brittle tests.