Files
zooqueenandHanzo Dev 3f893d4538 recoil -> jotai: retire the dead state library the whole client sat on
recoil 0.7.7 is recoil's LAST release and its final act is to break React 19.
`currentRendererSupportsUseSyncExternalStore()` destructures
`ReactCurrentDispatcher` out of
`React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`, which React 19
renamed. The read returns undefined and throws before the first paint — the
green-build/blank-page that 7d4adc71af measured and reverted. There is no
version to bump to, so the library goes.

The client already had TWO state libraries. This leaves ONE.

  249 files: 197 source + 52 test + the store itself
  client/src is now recoil-free; `recoil` is out of client/package.json,
  out of the lockfile, and out of node_modules (129 packages dropped).

Store rewritten on jotai (client/src/store):
  atom({key, default})            -> atom(default)
  selector / selectorFamily       -> derived atom / atomFamily of one
  atomFamily + reset              -> `family()` = atomFamily of atomWithReset
  useRecoilCallback               -> useAtomCallback
  RecoilRoot                      -> jotai Provider
  SetterOrUpdater / RecoilState   -> Setter<T> / WritableAtom (common/types)

Three semantic differences recoil hid, each fixed deliberately rather than
transcribed — a recoil `snapshot` is IMMUTABLE, so a read after a reset still
sees the pre-reset value, while jotai's `get` is live:

  - useClearConvoState read the conversation AFTER resetting it, to decide
    whether to also reset its latest message. Reordered to read first.
  - useClearStates read conversationKeysAtom AFTER awaiting the clears that
    reset it; live reads would have returned [] and skipped every per-key
    reset. The keys are now read before the clears.
  - useLatestMessage keyed a selectorFamily by an ARRAY of parent ids. recoil
    hashes params by value; jotai's atomFamily compares by identity and would
    have leaked one atom per render. Now a useMemo'd derived atom keyed by
    content.

Pure-logging recoil `effects` are deleted, not ported — they only called
logger.log with a key re-parsed out of `node.key`. The one effect that did
real work (conversationByIndex: localStorage write-through + history
pushState) is a write-wrapper over the stored atom, and gets the index
directly instead of splitting it back out of a serialized node key.

Deletes two duplicate helper modules' worth of overlap: store/jotai-utils.ts
folds into store/utils.ts, so there is one home and one name for each atom
helper. ToggleSwitch stops shipping two implementations behind an
`isRecoilState()` sniff and is now one component.

Also drops the recoil pnpm patch that appeared in the tree alongside this
work: it exists to keep recoil alive under React 19, and pnpm fails a patch
entry for a package nothing depends on.

MEASURED: `npm run frontend` exit 0 (23.8s). React is still 18.3.1 here —
this commit changes state management ONLY, so it is separately revertable
from the React 19 bump that follows.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 22:25:45 -07:00

16 lines
437 B
TypeScript

import { createStore } from 'jotai';
export type Store = ReturnType<typeof createStore>;
/**
* A fresh jotai store, optionally seeded — the equivalent of the
* `initializeState` prop tests used to hand to Recoil's root.
*
* render(<Provider store={seed((s) => s.set(myAtom, 1))}>{ui}</Provider>)
*/
export function seed(init?: (store: Store) => void): Store {
const store = createStore();
init?.(store);
return store;
}