use-lane
v0.2.0
Published
Transition-native data fetching for React 19 — Lane caches the promise behind each key and re-reads it inside transitions, while React owns loading, errors, and optimistic UI.
Downloads
373
Maintainers
Readme
use-lane
Transition-native data fetching for React 19. Refetches run inside React transitions — invalidate after a mutation, revalidate on focus, or defer a filter change, and the screen you're looking at stays live while the next data loads. No spinner flash, no torn UI.
React 19 ships the primitives to render async data — use(promise) for data,
Suspense for loading, Error Boundaries for errors, transitions for non-blocking
updates, and useOptimistic / useActionState for mutations. It doesn't ship
the layer underneath: something to cache those promises by key, share one request
across components, and re-fetch after a change. Lane is exactly that layer — and
nothing else.
const { promise } = useLane(["user", id], ({ signal }) => fetchUser(id, signal));
const user = use(promise); // Suspense owns loading, Error Boundaries own errorsWhy Lane
Libraries like SWR and TanStack Query own a resolved-value cache plus their own loading/error/status objects, optimistic patches, and mutation helpers. Lane takes the opposite split: it owns only the promise identity behind each key and lets React own the UI state it was designed to own in React 19.
- Every update is a transition. SWR and React Query keep the previous screen
during a refetch with a library flag (
keepPreviousData/placeholderData). Lane keeps each key's promise in React state, so wrapping a key change or aninvalidateinstartTransition— oruseDeferredValue— just works: the same transition you use everywhere else, interruptible, with a real pending flag. - One mental model. Mutate the source, invalidate the read, render from the next promise — the same model you already use next to Server Components.
- No parallel state machine. No
data/error/isLoadingresult object. You read withuse(promise); Suspense and Error Boundaries do the rest. - No mutation helper, by design. Mutations stay in React primitives, so optimistic UI lives next to the action that triggered it instead of in a global cache that needs rollback semantics.
Requirements
React 19.2+ (Lane uses useEffectEvent). React is a peer dependency.
Install
npm install use-lane
# or: pnpm add use-lane
# or: yarn add use-laneQuick start
1. Wrap your client tree in a LaneProvider.
"use client";
import { LaneProvider } from "use-lane";
export function Providers({ children }: { children: React.ReactNode }) {
return <LaneProvider>{children}</LaneProvider>;
}2. Read with useLane and unwrap with use. Lane returns the promise; a
Suspense boundary owns the loading state and an Error Boundary owns the
initial-load failure.
"use client";
import { Suspense, use } from "react";
import { useLane } from "use-lane";
function Profile({ userId }: { userId: string }) {
const { promise } = useLane(["user", userId], async ({ signal }) => {
const res = await fetch(`/api/users/${userId}`, { signal });
if (!res.ok) throw new Error("Failed to load user");
return (await res.json()) as User;
});
const user = use(promise);
return <h1>{user.name}</h1>;
}
export function UserProfile({ userId }: { userId: string }) {
return (
<Suspense fallback={<p>Loading…</p>}>
<Profile userId={userId} />
</Suspense>
);
}3. Converge after a mutation by invalidating the source. Mounted readers
re-read through a transition; isTransitionPending tells you it is happening.
"use client";
import { useLaneInstance } from "use-lane";
function RenameButton({ userId }: { userId: string }) {
const lane = useLaneInstance();
async function rename(name: string) {
await fetch(`/api/users/${userId}`, {
method: "PATCH",
body: JSON.stringify({ name }),
});
// Source changed → re-read the affected key. React renders the next promise.
lane.invalidate(["user", userId]);
}
return <button onClick={() => rename("Ada")}>Rename</button>;
}Core concepts
- Transition-native re-reads. Updates run through
useTransition, so the previous UI stays mounted and interactive while the next promise resolves —isTransitionPendingandisBackgroundPendingtell you which is in flight. Pair a key withuseDeferredValuefor search and filter UIs. (Initial loads with no prior data still suspend to a Suspense fallback.) - Keys are structural arrays (
["task", id]). They are matched exactly, or byprefix/ predicate for scoped operations.Datesegments are supported. - Invalidation-driven re-reads.
invalidateclears the cached promise and notifies mounted readers, which create the next promise from their current loader. Explicit (transition) and automatic (focus/mount/ polling, reported asisBackgroundPending) re-reads are kept separate. - Stale-on-error. A failed refresh keeps serving the last fulfilled value
and reports the failure through
refreshError. Only an initial load (no previous value) rejects the promise and reaches the Error Boundary. - Authoritative publication.
set/updatepublish server-confirmed data to exact keys;LaneHydrationseeds promises from RSC-loaded data and overwrites authoritatively on navigation. - Lifecycle built in. Garbage collection (
gcTime, default 5 min),retry/retryDelay,refetchIntervalpolling, andrefetchOnFocus/refetchOnMount/refetchOnReconnectrevalidation. - Optimistic UI stays local. Lane ships no mutation helper; use
useOptimistic/useActionStatein the component that owns the action.
API at a glance
| Export | Purpose |
| --- | --- |
| LaneProvider | Provides a Lane instance to the tree; wires focus / reconnect revalidation. |
| useLane(key, loader, options?) | Read a key. Returns { promise, refreshError, isTransitionPending, isBackgroundPending, invalidate }. |
| useLanePromise(key, loader, options?) | Thin wrapper returning just promise. |
| useLaneInstance() | The current Lane instance, for invalidate / set / update / remove from event handlers. |
| createLane() | Create a Lane instance manually (e.g. to share one across providers or seed on the server). |
| LaneHydration | Apply RSC-loaded snapshots as authoritative seed values. |
Lane instance methods: invalidate / invalidateAll, set, update /
updateAll, remove / removeAll. useLane options: staleTime, gcTime,
retry, retryDelay, refetchInterval, refetchOnFocus, refetchOnMount,
refetchOnReconnect.
See the API reference for full signatures and semantics.
Documentation
- API reference — every export, option, and behavior.
- Supported architectures — RSC-first and RSC-seeded client ownership.
- Design notes — why Lane is shaped this way.
License
MIT © Kento Moriwaki
