Skip to main content

Configuration

Every behavior is set through one config object, grouped by concern. The same shape is accepted in three places:

<CupleProvider store={store} config={appConfig}> // the app's defaults
<Boundary config={{ cache: { enabled: false } }}> // everything inside this region
<Report /> // useGet(client.report, args, { config: { ... } })
</Boundary>
</CupleProvider>

Each setting cascades on its own: the request's wins over the Boundary's, over the provider's, over the built-in default. Setting cache.freshMs on a Boundary never resets cache.storeStaleMs from the provider.

Defaults​

export const defaultConfig: ResolvedConfig = {
cache: {
enabled: true,
freshMs: 0,
storeStaleMs: 5 * 60_000,
maxStoredCalls: 3,
keepOnKeyChange: false,
},
errors: { onError: "boundary", fallbackMessage: "Something went wrong." },
loading: { blocking: false },
};
  • enabled: true: responses are cached, so a screen you come back to shows its data at once.
  • freshMs: 0: cached data counts as stale as soon as it lands, so coming back to it shows it at once and also refetches it in the background. The cache makes the screen fast, and what you see still ends up current.
  • storeStaleMs: 5 minutes: data nobody reads stays in memory for 5 minutes, then it's dropped. Going back and forth between screens is instant, without keeping everything for good.
  • maxStoredCalls: 3: of one endpoint, at most the 3 most recent entries nobody reads are kept (one per set of arguments). A search shouldn't keep every keystroke in memory, and a limit is a safer default than none.
  • keepOnKeyChange: false: when the client's key changes (another account signs in), the previous key's data is deleted at once. It can be large, and must not show to the next user.
  • onError: "boundary": an error in an action that nobody handled replaces the nearest <Boundary>'s content with its error view. It should be visible, not only a line in the console.
  • fallbackMessage: "Something went wrong.": the message an error gets when there's nothing more readable (a network failure, a bug in production). Set your own, in your app's language.
  • blocking: false: nothing makes useIsFetching({ blocking: true }) true unless it asks to. Only the few steps the user must wait for should block the page.

cache​

SettingDefaultWhat it does
enabledtruefalse keeps nothing once it's unread: data is dropped as soon as the last reader unmounts, and fetched again when read again. Readers mounted at the same time still share one request. The same as freshMs: 0, storeStaleMs: 0: for search boxes, and large data.
freshMs0How long data counts as fresh after it lands. A component that starts reading cached data (another tab, a reopened panel) shows it at once, and refetches it in the background only if it's stale. 0 always refetches, Infinity never. Fresh data nobody reads is never dropped. A first load is never fetched twice: "coming back" means some component has read this data before.
storeStaleMs5 minutesHow long data nobody reads stays in memory once it's stale. The clock starts when it is both stale and unread, whichever comes last. With several readers, the longest wins. The entries a combined read makes through ctx.get are kept as long as the combined read itself, whatever their own settings.
maxStoredCalls3How many entries of one endpoint (or one combined read) nobody reads are kept, whatever freshMs and storeStaleMs say. Past it, the one unread the longest is dropped. Entries something reads never count, and neither do the entries a cached combined read made. Different args are different entries, so a search keeps its last few queries, not every keystroke. With several readers, the largest wins.
keepOnKeyChangefalseWhether store.refreshKeys() keeps data whose key changed: false deletes it at once, true keeps it like any unread data, so switching back is instant: only when one person owns every account, since the old account's data shows again at once. Read from the provider.

With { freshMs: 30_000, storeStaleMs: 60_000 }, data that lands at 0s and loses its last reader at 3s is still fresh: it stays, and a reader coming back at 20s gets it without a request. It goes stale at 30s, and is dropped at 90s.

freshMs decides only what coming back does: polls, store.refresh() and an action's refresh refetch fresh data too. With Infinity, data nobody reads stays for good: right for reference data, wrong for args that keep changing, like a search, which pile up.

errors​

SettingDefaultWhat it does
onError"boundary"Actions: where an error nobody handled goes. "boundary", "notify", null (handled: the action's error holds it, nothing else shows it), or a function choosing per CupleError. See Errors.
notifynoneHow the app shows an error without replacing the page, given a CupleError. Used by "notify" and <Boundary error="notify">.
fallbackMessage"Something went wrong."The CupleError message when there's nothing more readable: a network failure, a server result without a message, a bug in production.

loading​

SettingDefaultWhat it does
blockingfalseCounts for useIsFetching({ blocking: true }): actions while they run, reads while their data refetches. For a full-page overlay. See The store.
everyMsnoneReads only: refetch every this many ms while something reads it.
debounceMsnoneuseGet only: when its args change, keep showing the current ones until the new ones have stayed the same this long, then load them as a transition, with the current data on screen meanwhile. One request for a burst of typing. The first args load at once. A first load the component moved on from (or unmounted during), and nothing else asked for, is aborted. See Reading.

Where each is used​

  • useGet, useGetWrapped: cache, loading.
  • useAction: errors, loading.blocking.
  • <Boundary error>: errors.fallbackMessage; with "notify", errors.notify.
  • Plain code on the store (store.preload, store.refreshKeys): the provider's config.