aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/client/atoms/utils.ts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-01-04 17:43:59 +0900
committernsfisis <nsfisis@gmail.com>2026-01-04 19:09:58 +0900
commitf8e4be9b36a16969ac53bd9ce12ce8064be10196 (patch)
treeb2cf350d2e2e52803ff809311effb40da767d859 /src/client/atoms/utils.ts
parente1c9e5e89bb91bca2586470c786510c3e1c03826 (diff)
downloadkioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.tar.gz
kioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.tar.zst
kioku-f8e4be9b36a16969ac53bd9ce12ce8064be10196.zip
refactor(client): migrate state management from React Context to Jotai
Replace AuthProvider and SyncProvider with Jotai atoms for more granular state management and better performance. This migration: - Creates atoms for auth, sync, decks, cards, noteTypes, and study state - Uses atomFamily for parameterized state (e.g., cards by deckId) - Introduces StoreInitializer component for subscription initialization - Updates all components and pages to use useAtomValue/useSetAtom - Updates all tests to use Jotai Provider with createStore pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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();
+ }
+}