Inside Await, data is never undefined.
The Await component narrows the type. Inside the callback, your data is the success type. No null checks, no optional chaining, no guessing.
<Await store={userStore}>{(user) => (// user is User. Never undefined,// never null, never loading.<div><h1>{user.name}</h1><p>{user.email}</p></div>)}</Await>
Errors are data, not exceptions.
When a request fails, the store moves to the Failed state, with the error and a retry callback. No try/catch. No error boundaries. Just a render prop.
<Await store={userStore}error={({ errors, retry }) => (<div><p>Something went wrong.</p><button onClick={retry}>Try again</button></div>)}>{(user) => <h1>{user.name}</h1>}</Await>
Three API calls. One typed tuple.
Combine multiple stores into one with RemoteDataStore.all(). Lazy, type-safe, with automatic retry.
const userStore = useRemoteData(() => fetchUser(id), { dependencies: [id] });const postsStore = useRemoteData(() => fetchPosts(id), { dependencies: [id] });const statsStore = useRemoteData(() => fetchStats(id), { dependencies: [id] });const allStore = RemoteDataStore.all(userStore, postsStore, statsStore);return (<Await store={allStore}>{([user, posts, stats]) => (<Dashboard user={user} posts={posts} stats={stats} />)}</Await>);
Retry only what broke.
When you combine three stores and one fails, retry() only re-fetches the failed request. The two successful stores keep their data. One button, one re-fetch.
// Combine three stores. One fails.const allStore = RemoteDataStore.all(userStore, postsStore, statsStore);// retry() only re-fetches the broken one.// The two successful stores keep their data.<Await store={allStore}error={({ retry }) => (<button onClick={retry}>Retry failed</button>)}>{([user, posts, stats]) => <Dashboard ... />}</Await>
Data stays fresh without a flicker.
Tell the store how long data should live. It re-fetches in the background when data goes stale, so your users keep seeing the old data while the new data loads.
const store = useRemoteData(() => fetchPrices(), {refresh: RefreshStrategy.afterMillis(30_000),});// isStale: true while background// refresh is in progress, old data stays visible<Await store={store}>{(prices, isStale) => (<div style={{ opacity: isStale ? 0.6 : 1 }}><PriceTable prices={prices} /></div>)}</Await>
Mutations refresh reads.
After a successful write, the stores you depend on re-fetch automatically. No manual cache busting. Declare what should refresh and it happens.
const todosStore = useRemoteData(() => fetchTodos());const addTodo = useRemoteUpdate((text) => api.addTodo(text), {refreshes: [todosStore],});// addTodo.run("Buy milk")// → mutation fires// → on success, todosStore re-fetches// → Await re-renders with fresh data
Test without mocking.
Stores are values. Pass one to your component and assert what renders. RemoteDataStore.of() creates a store in any state. The same approach works for Storybook.
import { RemoteData, RemoteDataStore, Failure } from "use-remote-data";// A store that's already loaded. No fetch, no mock.const store = RemoteDataStore.of(RemoteData.success({ name: "Alice", email: "alice@ex.com" }));render(<UserCard store={store} />);expect(screen.getByText("Alice")).toBeInTheDocument();// Test loading? Errors? Same idea.const loading = RemoteDataStore.of(RemoteData.Pending);const failed = RemoteDataStore.of(RemoteData.Failed([Failure.unexpected(new Error("timeout"))],async () => {}));
Data lifetime follows the component tree.
Stores live in components, not in a global cache. Mount a page, its data fetches. Unmount, it's gone. Pass a store as a prop and every child shares the same fetch.
function UserPage({ id }) {// Store created when UserPage mounts.// Fetches on first render. Caches while mounted.// Unmount UserPage: store is gone. No stale cache.const userStore = useRemoteData(() => fetchUser(id), { dependencies: [id] });// Pass the store down. Child components// share the same fetch, same cache.return (<Layout><UserHeader store={userStore} /><UserPosts store={userStore} /></Layout>);}
Six things you also get.
Zero dependencies
Just React. ~3.5kB gzipped, no runtime deps, no context providers, no bloat.
SSR ready
Pass server data as initial. The store starts in Success. No hydration boundaries to wire up.
Automatic cancellation
When deps change or a component unmounts, in-flight requests are aborted. Stale responses are always discarded.
Lazy by default
Stores only fetch when rendered. Define data dependencies upfront; only what mounts hits the network.
Mutations that refresh
First-class writes with useRemoteUpdate. After a successful mutation, dependent stores re-fetch automatically.
Typed errors
Separate domain errors from crashes. Validate with Zod, handle GraphQL unions; TypeScript knows which error you have.