aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/atoms/utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/atoms/utils.ts')
-rw-r--r--src/client/atoms/utils.ts81
1 files changed, 81 insertions, 0 deletions
diff --git a/src/client/atoms/utils.ts b/src/client/atoms/utils.ts
new file mode 100644
index 0000000..e7af288
--- /dev/null
+++ b/src/client/atoms/utils.ts
@@ -0,0 +1,81 @@
+import { atom, type Getter, type WritableAtom } from "jotai";
+
+// Symbol to identify reload action
+const RELOAD = Symbol("reload");
+
+/**
+ * A WritableAtom that returns T (or Promise<T> before hydration) and accepts
+ * an optional T value for hydration, or undefined to trigger reload.
+ */
+export type ReloadableAtom<T> = WritableAtom<T | Promise<T>, [T?], void>;
+
+/**
+ * Creates an async atom that can be reloaded by calling its setter.
+ * Read the atom to get the data (suspends while loading).
+ * Set the atom with no args to trigger a reload.
+ * Set the atom with a value to hydrate (useful for testing).
+ */
+export function createReloadableAtom<T>(
+ getter: (get: Getter) => Promise<T>,
+): ReloadableAtom<T> {
+ const refetchKeyAtom = atom(0);
+ // Stores hydrated value - undefined means not hydrated
+ const hydratedValueAtom = atom<{ value: T } | undefined>(undefined);
+
+ return atom(
+ // Not using async here - returns T synchronously when hydrated, Promise<T> when fetching
+ (get): T | Promise<T> => {
+ // Check for hydrated value first (sync path - avoids Suspense)
+ const hydrated = get(hydratedValueAtom);
+ if (hydrated !== undefined) {
+ return hydrated.value;
+ }
+ // Async path - will trigger Suspense
+ get(refetchKeyAtom);
+ return getter(get);
+ },
+ (_get, set, action?: T | typeof RELOAD) => {
+ if (action === undefined || action === RELOAD) {
+ // Trigger reload: clear hydrated value and bump refetch key
+ set(hydratedValueAtom, undefined);
+ set(refetchKeyAtom, (k) => k + 1);
+ } else {
+ // Hydrate with value
+ set(hydratedValueAtom, { value: action });
+ }
+ },
+ );
+}
+
+// Track all atom family caches for test cleanup
+const atomFamilyCaches: Map<unknown, unknown>[] = [];
+
+/**
+ * Creates a reloadable atom family for parameterized async data.
+ * Each unique parameter gets its own cached atom with reload capability.
+ */
+export function createReloadableAtomFamily<T, P extends string | number>(
+ getter: (param: P, get: Getter) => Promise<T>,
+): (param: P) => ReloadableAtom<T> {
+ const cache = new Map<P, ReloadableAtom<T>>();
+ atomFamilyCaches.push(cache);
+
+ return (param: P): ReloadableAtom<T> => {
+ let reloadableAtom = cache.get(param);
+ if (!reloadableAtom) {
+ reloadableAtom = createReloadableAtom((get) => getter(param, get));
+ cache.set(param, reloadableAtom);
+ }
+ return reloadableAtom;
+ };
+}
+
+/**
+ * Clears all atom family caches. Call this in test beforeEach/afterEach
+ * to ensure tests don't share cached atoms.
+ */
+export function clearAtomFamilyCaches() {
+ for (const cache of atomFamilyCaches) {
+ cache.clear();
+ }
+}