The TTL is the obvious half. The in-flight deduplication is the half that matters: two components mounting in the same tick ask for one key once, so a rate limit sees one request instead of five. The TTL then keeps the answer around long enough that a remount paints from cache instead of flashing a skeleton over data the app already has.
Nothing yet.
Ask five times at once: five reads are served and ONE request is made. Ask again inside the 8s TTL and none is.
Installation
Adds the package and prints the import to use. Append --copy to write the source into your project instead.
enigma add cacheInstalls with your project's own package manager - npm, pnpm, yarn or bun - read from its packageManager field or its lockfile.
1 Install the dependencies
2 Use it
Import from @enigmax/utils/react.
State only - no rendering, so there is no recipe to copy. defaultCache is a
shared instance; createCache gives you your own.
Vanilla
import { createCache } from "@enigmax/utils";
const cache = createCache({ namespace: "app", ttl: 30_000, storage: "session" });
const user = await cache.read(`user:${id}`, () => api.getUser(id));
cache.invalidate("user:*"); // after a write
storage is "memory", "local" or "session". A trailing * invalidates a whole prefix.
React
import { useCached } from "@enigmax/utils/react";
const { data, loading, validating, refresh } = useCached(`user:${id}`, () => api.getUser(id));
loading is true only when there is nothing to render yet. validating is true while a value already on screen is being revalidated - that is the flag a spinner in the corner should use, not the one that blanks the view. Rendering a cached value immediately is the whole point: the shell never waits on data it already has.
API
| Method | Meaning |
|---|---|
get(key) / set(key, value, ttl?) |
Direct access. get returns undefined once expired. |
has(key) |
Fresh entry present. |
read(key, loader, ttl?) |
Read through the cache; concurrent calls share one loader call. |
invalidate(pattern) |
One key, or a whole prefix with a trailing *. |
clear() |
Everything. |
subscribe(listener) |
Told which key changed. Returns an unsubscribe. |
Details that are easy to get wrong
- A rejection is never cached. A failed load must not poison the key for the rest of the TTL.
- A full storage quota never takes the read path down. Private-mode Safari exposes
localStorageand throws on write, so the backing store is probed once and dropped if it is not usable. - A corrupt persisted entry is discarded, not thrown. Stale JSON from an older shape is removed and reloaded.
- Memory is bounded. Past
maxEntries(200) the oldest entry is evicted.