feat!: add staleWhileMounted global cache - #22
Open
timBm10c wants to merge 2 commits into
Open
Conversation
The jest config referenced a `react-native` preset, but react-native is not a dependency of this package, so `yarn test` failed to start at all. babel-jest picks up the metro preset from babel.config.js instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Responses are held in a path-keyed cache owned by the ApiReadProvider, so a hook mounting on a path already in the cache returns that response immediately (stale, with a 'mounted' reason) while it refetches. This removes the loading state when returning to an already-visited screen. The cache is kept coherent with the existing controls: invalidateExact and invalidateMatching drop cached entries as well as notifying mounted hooks, mutateExact and mutateMatching apply the mutator to cached entries, and a hook's own invalidate() drops its path. Otherwise data already known to be wrong could be served to a later mount. Reducer state gains a dataPath field. A path change re-renders before the request effect resets the state, so without it the write-back effect would file the previous path's data under the newly requested path. BREAKING CHANGE: READ_FAILURE now reports a staleReason of 'error' rather than 'invalidated', matching what the README already documented, and StaleReason gains a 'mounted' member which will fail exhaustive switches in consuming code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the
staleWhileMountedoption that was stubbed out intypes.ts, backed by the global cache listed as a TODO in the README.What it does
Responses are held in a path-keyed cache owned by the
ApiReadProvider. A hook mounting on a path that's already cached returns that response immediately — marked stale, with a new'mounted'reason — while the fresh response is fetched in the background. This removes the loading state when a user returns to a screen they've already visited.Opt-in per instance or via provider config, and inert (with a dev warning) when there's no
ApiReadProvider, since the provider owns the cache.Keeping the cache coherent
The bulk of the work, and the part worth reviewing closely. The existing controls only ever reached mounted hooks, which isn't sufficient once responses outlive their component:
invalidateExact/invalidateMatchingnow drop cached entries as well as notifying mounted hooks.mutateExact/mutateMatchingapply the mutator to cached entries too.invalidate()drops its path.Without this, data already known to be wrong could be served to a later mount. The four functions were refactored onto shared
invalidateWhere/mutateWhereappliers taking a path predicate, collapsing four near-identical loops into two.Where a path is both mounted and cached, the direct cache write and the mounted hook's write-back derive from the same base, so the mutator applies once.
Why reducer state gained
dataPathWhen
pathchanges, the component re-renders holding the previous path's data before the request effect resets state. Without tracking which path the current data belongs to, the write-back effect would file path A's data under key B. It also means mutations andreadMoreresults reach the cache without plumbingsetCachedthroughuse-read-more.Breaking changes
Flagged as
feat!— both are small, but they affect integrators, so I'd rather surface them than bury them. Happy to drop the!if you'd rather not take a version bump for these:READ_FAILUREreported astaleReasonof'invalidated'; it now reports'error'. This was a plain bug — the README already documented'error'as the value for this case. Covered by a regression test.StaleReasongains a'mounted'member, which will fail exhaustive switches in consuming code.Unrelated fix included
The jest config specified
preset: "react-native", butreact-nativeisn't a dependency of this package, soyarn testhas been failing to start entirely — the loneit.todonever ran. Removed in its own commit; babel-jest picks up the metro preset frombabel.config.js.Testing
The seeding and precedence logic lives in the reducer, which is pure, so it's unit-tested directly — 11 tests covering cache seeding, the own-data-beats-cache precedence rule, path changes, and the
staleReasonfix.Hook-level mount/unmount/remount tests would need
react-test-rendererand@testing-library/react-hooksas new devDependencies. That felt like a maintainer's call, so I left a specificit.todomarking the gap rather than adding them unasked.yarn lint,yarn typescript,yarn testandyarn prepareall pass.Known limitation
The cache has no eviction policy — one entry per path, for the provider's lifetime. Documented as a caveat in the README; worth a follow-up adding an LRU cap or max age before it's recommended for apps reading an unbounded number of distinct paths.
🤖 Generated with Claude Code