@@ -40,6 +40,12 @@ type Store<T> = {
4040 /** Store-level visibilitychange handler, installed only while this store polls. */
4141 visibilityListener : ( ( ) => void ) | null ;
4242 generation : number ;
43+ /**
44+ * Set when `setClientResourceData` publishes while nobody is subscribed (session-cache
45+ * seed). The first subscribe must quiet-revalidate; otherwise mount never fetches and
46+ * seeded rows stay indefinitely stale with `lastAttemptOk: true`.
47+ */
48+ seedNeedsRevalidate : boolean ;
4349} ;
4450
4551/**
@@ -80,6 +86,7 @@ function getStore<T>(key: string): Store<T> {
8086 inflightOwner : null ,
8187 visibilityListener : null ,
8288 generation : 0 ,
89+ seedNeedsRevalidate : false ,
8390 } ;
8491 stores . set ( key , store ) ;
8592 }
@@ -298,9 +305,13 @@ function subscribeResource<T>(
298305 store . fetcherByListener . set ( onStoreChange , fetcher ) ;
299306 store . subscriberCount ++ ;
300307
301- // Cold start only — keep cached data across transient 0→1 resubscribe gaps.
302- if ( store . subscriberCount === 1 && store . snapshot . data === undefined ) {
303- void runFetch ( store , fetcher , { replaceInflight : true , owner : onStoreChange } ) ;
308+ // Cold start, or a pre-subscribe seed that still needs a network check. Keep
309+ // cached data across transient 0→1 resubscribe gaps when neither applies.
310+ if ( store . subscriberCount === 1 ) {
311+ if ( store . snapshot . data === undefined || store . seedNeedsRevalidate ) {
312+ store . seedNeedsRevalidate = false ;
313+ void runFetch ( store , fetcher , { replaceInflight : true , owner : onStoreChange } ) ;
314+ }
304315 }
305316 recomputePoll ( store ) ;
306317
@@ -328,7 +339,7 @@ function subscribeResource<T>(
328339}
329340
330341/** Module-level fetch cache with useSyncExternalStore subscriptions (no fetch in useEffect). */
331- export interface ClientResourceOptions {
342+ export interface ClientResourceOptions < T = unknown > {
332343 pollMs ?: number ;
333344 enabled ?: boolean ;
334345 /**
@@ -337,18 +348,34 @@ export interface ClientResourceOptions {
337348 * happening off-screen — a restarting server, for instance.
338349 */
339350 pauseWhenHidden ?: boolean ;
351+ /**
352+ * Optional seed applied once before the first subscribe (session-cache revisit).
353+ * Lives in the store — not a render-time ref — so React Compiler / react-hooks/refs
354+ * stay quiet while the mount fetch still quiet-revalidates via `seedNeedsRevalidate`.
355+ */
356+ initialData ?: T ;
357+ }
358+
359+ /** Seed an empty, unsubscribed store. No-ops when data already exists or someone is listening. */
360+ function seedClientResourceIfEmpty < T > ( key : string , data : T ) : void {
361+ const store = getStore < T > ( key ) ;
362+ if ( store . subscriberCount !== 0 || store . snapshot . data !== undefined ) return ;
363+ setClientResourceData ( key , data ) ;
340364}
341365
342366export function useClientResource < T > (
343367 key : string ,
344368 fetcher : ( signal : AbortSignal ) => Promise < T > ,
345- options ?: ClientResourceOptions ,
369+ options ?: ClientResourceOptions < T > ,
346370) : ResourceSnapshot < T > & { refresh : ( opts ?: { forceLoading ?: boolean } ) => void } {
347371 const enabled = options ?. enabled !== false ;
348372 const pollMs = options ?. pollMs ;
349373 // Default true: a background tab has nobody reading the paint. Opt out for polls that
350374 // must keep running while hidden, such as waiting for a restarted server to answer.
351375 const pauseWhenHidden = options ?. pauseWhenHidden !== false ;
376+ if ( enabled && options ?. initialData !== undefined ) {
377+ seedClientResourceIfEmpty ( key , options . initialData ) ;
378+ }
352379 const fetcherRef = useRef ( fetcher ) ;
353380 // Sync latest fetcher every commit. No dep array on purpose: inline fetchers are
354381 // reallocated every render; listing them would re-subscribe forever.
@@ -411,7 +438,7 @@ export function useKeyedClientResource<T>(
411438 key : string ,
412439 deps : readonly unknown [ ] ,
413440 load : ( signal : AbortSignal ) => Promise < T > ,
414- options ?: ClientResourceOptions ,
441+ options ?: ClientResourceOptions < T > ,
415442) : ResourceSnapshot < T > & { refresh : ( opts ?: { forceLoading ?: boolean } ) => void } {
416443 const resource = useClientResource ( key , load , options ) ;
417444 const prevDepsRef = useRef < readonly unknown [ ] | null > ( null ) ;
@@ -447,6 +474,9 @@ export function setClientResourceData<T>(key: string, data: T) {
447474 hasSucceeded : true ,
448475 lastAttemptOk : true ,
449476 } ;
477+ // Only pre-subscribe seeds need a follow-up fetch. Live publishers (mutation
478+ // results) already hold the fresh value and must not schedule a redundant GET.
479+ store . seedNeedsRevalidate = store . subscriberCount === 0 ;
450480 emit ( store ) ;
451481}
452482
0 commit comments