Skip to content

Proposal: background refresh — revalidate async sources without marking reads pending #2844

Description

@brenelz

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions