Problem
refresh(source) currently has exactly one behavior: it invalidates the source and every read derived from it reports isPending() === true until the refetch settles. For a store, that's every property — the pending state propagates from the parent source (computePendingState's _parentSource check), so there's no fine-grained middle ground.
This makes the canonical optimistic-mutation pattern degrade at the last step:
const [books, setBooks] = createOptimisticStore(() => searchBooks(""), []);
const editBookTitle = action(function* (id: number) {
setBooks(s => {
const book = s.find(b => b.id === id);
if (book) book.title = "New Title";
});
yield api.updateBook(id);
refresh(books); // sync server truth back into the store
});
// view
{isPending(() => book.title) && "Saving..."}
During the optimistic write, isPending is beautifully scoped: only the edited book's property node has an override, so only that row shows "Saving...". The moment refresh(books) runs, the whole source goes STATUS_PENDING and every row flips to pending for the duration of the refetch — even though the user only touched one.
Proposal
refresh(target, options?)
interface RefreshOptions {
/** Revalidate without marking dependent reads as pending. Default: false. */
background?: boolean;
}
function refresh<T>(target: Refreshable<T>, options?: RefreshOptions): Promise<void>;
Semantics
The framing that keeps this coherent with isPending's "value is not yet settled" contract: a background refresh reclassifies the refetch from a pending resolution into a future write. The current value remains the settled value; when new data arrives it applies like any other write (reconciled for stores).
Alternatives considered
- App-level workaround (works today): write an optimistic
pending flag into the data (book.pending = true) or track a createOptimistic saving-id, and key indicators off that instead of isPending. Viable, but it's boilerplate for what SWR-style libraries treat as the default refetch mode, and it doesn't help polling/focus-revalidation at all.
- Making
isPending ignore source refetches (e.g. isPending(fn, { overridesOnly: true })): rejected — during a refetch every value genuinely might change, so reads are unsettled; the fix belongs on the producer side (declaring the refetch non-blocking) rather than weakening the read-side contract.
Problem
refresh(source)currently has exactly one behavior: it invalidates the source and every read derived from it reportsisPending() === trueuntil the refetch settles. For a store, that's every property — the pending state propagates from the parent source (computePendingState's_parentSourcecheck), so there's no fine-grained middle ground.This makes the canonical optimistic-mutation pattern degrade at the last step:
During the optimistic write,
isPendingis beautifully scoped: only the edited book's property node has an override, so only that row shows "Saving...". The momentrefresh(books)runs, the whole source goesSTATUS_PENDINGand every row flips to pending for the duration of the refetch — even though the user only touched one.Proposal
refresh(target, options?)Semantics
The framing that keeps this coherent with
isPending's "value is not yet settled" contract: a background refresh reclassifies the refetch from a pending resolution into a future write. The current value remains the settled value; when new data arrives it applies like any other write (reconciled for stores).Alternatives considered
pendingflag into the data (book.pending = true) or track acreateOptimisticsaving-id, and key indicators off that instead ofisPending. Viable, but it's boilerplate for what SWR-style libraries treat as the default refetch mode, and it doesn't help polling/focus-revalidation at all.isPendingignore source refetches (e.g.isPending(fn, { overridesOnly: true })): rejected — during a refetch every value genuinely might change, so reads are unsettled; the fix belongs on the producer side (declaring the refetch non-blocking) rather than weakening the read-side contract.