Skip to main content

Shared Stores

By default, stores live with the component that creates them. This is the right default — no stale cache, no global state to manage, simple data lifetime.

But sometimes multiple unrelated components need the same data. A navigation bar shows the current user. A settings panel shows the current user. A comment widget shows the current user. With useRemoteData, you'd lift the store to a shared parent and pass it down — which works, but can mean prop-drilling through many layers.

useSharedRemoteData solves this with name-based deduplication. Two components that call useSharedRemoteData("currentUser", fetchUser) share the same store. One fetch, shared state, no prop-drilling.

Setup

Wrap your app (or a subtree) in <SharedStoreProvider>:

import { SharedStoreProvider } from 'use-remote-data';

function App() {
return (
<SharedStoreProvider>
<Router />
</SharedStoreProvider>
);
}

Usage

import { Await, useSharedRemoteData } from 'use-remote-data';

function NavBar() {
const store = useSharedRemoteData('currentUser', (signal) => fetch('/api/me', { signal }).then((r) => r.json()));
return <Await store={store}>{(user) => <span>{user.name}</span>}</Await>;
}

function SettingsPanel() {
// Same name, same data. No second fetch.
const store = useSharedRemoteData('currentUser', (signal) => fetch('/api/me', { signal }).then((r) => r.json()));
return <Await store={store}>{(user) => <h1>Settings for {user.name}</h1>}</Await>;
}

The first component to mount with a given name starts the fetch. Subsequent components with the same name immediately receive the current state — no duplicate request.

Try it

import { Await } from 'use-remote-data';
import { SharedStoreProvider, useSharedRemoteData } from 'use-remote-data';

let fetchCount = 0;

function fetchUser(): Promise<{ name: string; fetchNumber: number }> {
fetchCount++;
const n = fetchCount;
return new Promise((resolve) =>
setTimeout(() => resolve({ name: 'Alice', fetchNumber: n }), 800)
);
}

function UserBadge() {
const store = useSharedRemoteData('currentUser', fetchUser);
return (
<Await store={store}>
{(user) => (
<div>
Badge: <strong>{user.name}</strong> (fetch #
{user.fetchNumber})
</div>
)}
</Await>
);
}

function UserGreeting() {
const store = useSharedRemoteData('currentUser', fetchUser);
return (
<Await store={store}>
{(user) => (
<div>
Hello, <strong>{user.name}</strong>! (fetch #
{user.fetchNumber})
</div>
)}
</Await>
);
}

export function Component() {
return (
<SharedStoreProvider>
<UserBadge />
<UserGreeting />
<p style={{ fontSize: '0.85em', color: 'gray' }}>
Both components show the same fetch number — one fetch, shared
state.
</p>
</SharedStoreProvider>
);
}

How deduplication works

  • The name is the cache key. Same name = same store, same data.
  • The first registrant's fetcher wins. If two components register "currentUser" with different fetcher functions, the first one is used and a dev-mode warning is logged.
  • When the last subscriber unmounts, the entry is cleaned up — in-flight requests aborted, refresh timers cancelled, state dropped.
Define fetchers outside components

Since the first registrant's fetcher wins, avoid defining fetchers inline (which creates a new reference each render). Instead, define them outside the component or wrap them in useCallback:

// Good — stable reference
const fetchUser = (signal: AbortSignal) => fetch('/api/me', { signal }).then((r) => r.json());

function NavBar() {
const store = useSharedRemoteData('currentUser', fetchUser);
// ...
}

gcTime — grace period before cleanup

By default, when the last subscriber unmounts, the entry is immediately deleted. This can cause unnecessary re-fetches during route transitions where a component unmounts and remounts quickly.

The gcTime option keeps the entry alive for a grace period:

const store = useSharedRemoteData('currentUser', fetchUser, {
gcTime: 5000, // keep for 5 seconds after last unmount
});

If a new subscriber mounts within the grace period, it picks up the existing state without a re-fetch. If the grace period expires with no subscribers, the entry is cleaned up.

This is analogous to react-query's gcTime (formerly cacheTime).

Refresh strategies

Works identically to useRemoteData. Pass a refresh option:

const store = useSharedRemoteData('prices', fetchPrices, {
refresh: RefreshStrategy.afterMillis(30_000),
});

One timer per entry, not per subscriber. When the refresh fires, all subscribers see the new data.

refresh() — manual re-fetch

Calling store.refresh() from any subscriber re-fetches for all of them:

function RefreshButton() {
const store = useSharedRemoteData('posts', fetchPosts);
return <button onClick={() => store.refresh()}>Refresh</button>;
}

SSR with initialData

Pass server-rendered data to the provider. Stores with matching names start in Success — no loading spinner, no hydration mismatch:

// Server component fetches data
const user = await db.users.findFirst();
const posts = await db.posts.findMany();

// Client wrapper
<SharedStoreProvider initialData={{ currentUser: user, posts: posts }}>
<App />
</SharedStoreProvider>;

Components that call useSharedRemoteData("currentUser", ...) render immediately with the server data. If a refresh strategy is configured, it kicks in after hydration.

Full compatibility

useSharedRemoteData returns a standard RemoteDataStore<T>. Everything works:

// Combine with local stores
const sharedUser = useSharedRemoteData('currentUser', fetchUser);
const localPosts = useRemoteData(() => fetchPosts(id), { dependencies: [id] });
const combined = RemoteDataStore.all(sharedUser, localPosts);

// Map
const userName = sharedUser.map((u) => u.name);

// orNull
const userOrNull = sharedUser.orNull;

When to use useRemoteData vs useSharedRemoteData

useRemoteDatauseSharedRemoteData
Store lifetimeComponent that called the hookWhile any subscriber is mounted (+ gcTime)
DeduplicationNone — each call creates its own storeBy name — same name = same store
Requires providerNoYes (<SharedStoreProvider>)
Prop-drilling neededYes, for sharingNo — same name = same store
Best forData owned by one component treeData shared across unrelated components

Start with useRemoteData. It's simpler, has no provider, and the component-scoped lifetime prevents stale cache bugs. Reach for useSharedRemoteData when you find yourself prop-drilling the same store through 3+ levels, or when truly unrelated components need the same data.

Options

OptionTypeDescription
refreshRefreshStrategy<T>Auto-refresh strategy (same as useRemoteData)
gcTimenumberMilliseconds to keep the entry alive after the last subscriber unmounts
debugConsole['warn']Log state transitions and warnings