diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-01-04 17:43:59 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-01-04 19:09:58 +0900 |
| commit | f8e4be9b36a16969ac53bd9ce12ce8064be10196 (patch) | |
| tree | b2cf350d2e2e52803ff809311effb40da767d859 /src/client/atoms/utils.ts | |
| parent | e1c9e5e89bb91bca2586470c786510c3e1c03826 (diff) | |
| download | kioku-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.ts | 81 |
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(); + } +} |
