@octanejs/jotai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # @octanejs/jotai
2
+
3
+ [jotai](https://github.com/pmndrs/jotai) for the [octane](https://github.com/octanejs/octane) UI framework.
4
+
5
+ jotai separates a framework-agnostic **vanilla core** (`atom`, `createStore`,
6
+ `getDefaultStore` + all of `vanilla/utils`) from a small **React binding**
7
+ (`Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom`). This package
8
+ reuses the vanilla core unchanged (re-exported verbatim from `jotai/vanilla`) and
9
+ reimplements only the binding on octane's hooks — deliberately preserving
10
+ upstream's implementation shape (a force-update `useReducer` + effect
11
+ subscription, not `useSyncExternalStore`), so re-render behavior matches jotai on
12
+ React. The public surface matches jotai 1:1 — existing jotai code works by
13
+ changing the import.
14
+
15
+ ```tsx
16
+ // before
17
+ import { atom, useAtom } from 'jotai';
18
+ // after
19
+ import { atom, useAtom } from '@octanejs/jotai';
20
+
21
+ const countAtom = atom(0);
22
+
23
+ function Counter() @{
24
+ const [count, setCount] = useAtom(countAtom);
25
+ <button onClick={() => setCount((c) => c + 1)}>count is {count as string}</button>
26
+ }
27
+ ```
28
+
29
+ ## Entry points
30
+
31
+ | import | what you get | notes |
32
+ | --- | --- | --- |
33
+ | `@octanejs/jotai` | `atom`, `createStore`, `getDefaultStore`, `Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom` | vanilla verbatim + the octane-bound binding |
34
+ | `@octanejs/jotai/vanilla` | `atom`, `createStore`, `getDefaultStore` + types | re-exported verbatim from jotai |
35
+ | `@octanejs/jotai/vanilla/utils` | `RESET`, `atomWithReset`, `atomWithStorage`, `atomWithReducer`, `atomFamily`, `selectAtom`, `splitAtom`, `loadable`, `unwrap`, … | re-exported verbatim (all framework-agnostic) |
36
+ | `@octanejs/jotai/vanilla/internals` | `INTERNAL_*` store building blocks | re-exported verbatim; unstable by upstream contract |
37
+ | `@octanejs/jotai/react` | `Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom` | the binding, ported to octane hooks |
38
+ | `@octanejs/jotai/react/utils` | `useResetAtom`, `useAtomCallback`, `useHydrateAtoms`, `useReducerAtom` | ported to octane hooks |
39
+ | `@octanejs/jotai/utils` | everything from `vanilla/utils` + `react/utils` | mirror of `jotai/utils` |
40
+
41
+ `jotai/babel/*` (React-specific compile-time plugins) is not shipped.
42
+
43
+ ## How it works
44
+
45
+ octane keys hooks by a compiler-injected per-call-site `Symbol`, appended as the
46
+ last argument of every `use*` call. The hooks here **forward** that slot to the
47
+ base hooks they compose (deriving a stable sub-slot per composed base hook), so
48
+ `useAtom(a)` and `useAtom(b)` in one component — or the same atom used twice —
49
+ stay independent, exactly like distinct call sites in React.
50
+
51
+ The binding is a line-for-line port of `jotai/react`: a reader holds a
52
+ `[value, store, atom]` tuple in a force-update reducer and subscribes to the
53
+ store in an effect. That means the same observable behavior as jotai on React,
54
+ including:
55
+
56
+ - **`useSetAtom` never re-renders the writer.** A component that only writes an
57
+ atom doesn't subscribe to it.
58
+ - **Derived atoms bail out.** A dependency write that recomputes to an
59
+ `Object.is`-equal value never notifies readers.
60
+ - **Readers mount with two renders** (the subscription effect re-checks the
61
+ value after subscribing) — same as upstream.
62
+
63
+ ## Async atoms + Suspense
64
+
65
+ An atom whose value is a promise suspends the reader through octane's `use()`
66
+ (React-19 parity) on jotai's identity-stable *continuable promise*. Use a
67
+ suspense boundary (`@try { } @pending { } @catch (e) { }` or `<Suspense>`), or
68
+ skip suspending entirely with the vanilla `loadable`/`unwrap` escape hatches:
69
+
70
+ ```tsx
71
+ const userAtom = atom(async () => (await fetch('/api/user')).json());
72
+
73
+ function Profile() @{
74
+ <div>
75
+ @try {
76
+ <UserName />
77
+ } @pending {
78
+ <span>loading…</span>
79
+ } @catch (e) {
80
+ <span>failed: {(e as Error).message}</span>
81
+ }
82
+ </div>
83
+ }
84
+ ```
85
+
86
+ ## Status
87
+
88
+ Current scope, known divergences, and verification status are tracked in the
89
+ generated [bindings status table](../../docs/bindings-status.md), sourced from
90
+ this package's [`status.json`](./status.json).
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@octanejs/jotai",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "octane": {
7
+ "hookSlots": {
8
+ "manual": [
9
+ "src"
10
+ ]
11
+ }
12
+ },
13
+ "description": "jotai bindings for the octane renderer — reuses jotai's framework-agnostic vanilla atoms/store and swaps the React binding for an octane port of jotai/react.",
14
+ "author": {
15
+ "name": "Dominic Gannaway",
16
+ "email": "dg@domgan.com"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/octanejs/octane.git",
24
+ "directory": "packages/jotai"
25
+ },
26
+ "main": "src/index.ts",
27
+ "module": "src/index.ts",
28
+ "types": "src/index.ts",
29
+ "files": [
30
+ "src",
31
+ "README.md"
32
+ ],
33
+ "exports": {
34
+ ".": "./src/index.ts",
35
+ "./vanilla": "./src/vanilla.ts",
36
+ "./vanilla/utils": "./src/vanilla/utils.ts",
37
+ "./vanilla/internals": "./src/vanilla/internals.ts",
38
+ "./react": "./src/react.ts",
39
+ "./react/utils": "./src/react/utils.ts",
40
+ "./utils": "./src/utils.ts"
41
+ },
42
+ "dependencies": {
43
+ "jotai": "^2.20.1",
44
+ "octane": "0.1.3"
45
+ },
46
+ "devDependencies": {
47
+ "@tsrx/react": "^0.2.37",
48
+ "esbuild": "^0.28.1",
49
+ "react": "^19.2.0",
50
+ "react-dom": "^19.2.0",
51
+ "vitest": "^4.1.9"
52
+ },
53
+ "scripts": {
54
+ "test": "vitest run"
55
+ }
56
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // @octanejs/jotai — jotai for the octane renderer.
2
+ //
3
+ // jotai cleanly separates a framework-agnostic vanilla core (`atom`,
4
+ // `createStore`, `getDefaultStore` + all of vanilla/utils) from a small React
5
+ // binding (`Provider`, `useStore`, `useAtom`, `useAtomValue`, `useSetAtom`).
6
+ // This package reuses the vanilla core UNCHANGED (re-exported verbatim from
7
+ // `jotai/vanilla`) and reimplements only the binding on octane's hooks —
8
+ // preserving upstream's useReducer-force-update implementation rather than
9
+ // rewriting it on useSyncExternalStore, so re-render behavior matches jotai on
10
+ // React. The public surface matches jotai 1:1: existing jotai code works by
11
+ // changing the import from `jotai` to `@octanejs/jotai`.
12
+ //
13
+ // The one octane-specific detail is hook slots: octane keys hooks by a
14
+ // compiler-injected per-call-site Symbol, appended as the LAST argument of
15
+ // every `use*` call. The hooks here FORWARD that slot (deriving stable
16
+ // sub-slots when one hook composes several base hooks — see internal.ts), so
17
+ // `useAtom(a)` and `useAtom(b)` in one component stay independent, just like
18
+ // in React.
19
+ export * from './vanilla';
20
+ export * from './react';
@@ -0,0 +1,37 @@
1
+ // Slot mechanics shared by the binding's plain-`.ts` hooks (same helper as
2
+ // @octanejs/tanstack-query). The octane compiler injects a per-call-site Symbol slot into
3
+ // every hook call in compiled files; these binding files are NOT compiled, so a
4
+ // hook here receives the caller's slot as its trailing argument and derives a
5
+ // distinct sub-slot for each base hook it composes.
6
+
7
+ // Memoized: subSlot runs on EVERY hook call every render; the cache returns the
8
+ // identical Symbol.for-interned value without the concat + registry lookup.
9
+ const subSlotCache = new Map<symbol, Map<string, symbol>>();
10
+ // Tag-only symbols for the slotless-caller case (see below).
11
+ const bareTagCache = new Map<string, symbol>();
12
+
13
+ export function subSlot(slot: symbol | undefined, tag: string): symbol {
14
+ // No inherited slot (the caller was NOT compiled — e.g. a vendored wrapper
15
+ // hook): return a stable TAG-ONLY symbol rather than undefined. The runtime
16
+ // combines it with the ambient withSlot path, so sibling base hooks inside
17
+ // one composed hook stay DISTINCT per tag. Returning undefined here made
18
+ // them all resolve to the bare path — one shared slot, state collision.
19
+ if (slot === undefined) {
20
+ let bare = bareTagCache.get(tag);
21
+ if (bare === undefined) bareTagCache.set(tag, (bare = Symbol.for(':' + tag)));
22
+ return bare;
23
+ }
24
+ let byTag = subSlotCache.get(slot);
25
+ if (byTag === undefined) subSlotCache.set(slot, (byTag = new Map()));
26
+ let sym = byTag.get(tag);
27
+ if (sym === undefined) byTag.set(tag, (sym = Symbol.for((slot.description ?? '') + ':' + tag)));
28
+ return sym;
29
+ }
30
+
31
+ // Split the compiler-injected trailing slot off a hook's runtime args, returning
32
+ // the user args (everything before it) and the slot.
33
+ export function splitSlot(args: any[]): [any[], symbol | undefined] {
34
+ const tail = args[args.length - 1];
35
+ const slot = typeof tail === 'symbol' ? (tail as symbol) : undefined;
36
+ return [slot !== undefined ? args.slice(0, -1) : args, slot];
37
+ }
@@ -0,0 +1,19 @@
1
+ // Provider — port of the component half of jotai's react/Provider.ts. Upstream
2
+ // only allocates a store when no `store` prop is given (the useRef stays null
3
+ // otherwise); the port keeps that laziness but renders through a SINGLE JSX
4
+ // path instead of upstream's two createElement returns — the children
5
+ // descriptor shape must stay stable across renders for octane's reconciler.
6
+ import { useRef } from 'octane';
7
+ import { createStore } from 'jotai/vanilla';
8
+ import { StoreContext } from './store.ts';
9
+
10
+ export function Provider(props) @{
11
+ const { children, store } = props;
12
+
13
+ const storeRef = useRef(null);
14
+ if (!store && storeRef.current === null) {
15
+ storeRef.current = createStore();
16
+ }
17
+
18
+ <StoreContext.Provider value={store || storeRef.current}>{children}</StoreContext.Provider>
19
+ }
@@ -0,0 +1,4 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { Store } from './store';
3
+
4
+ export declare const Provider: (props: { children?: unknown; store?: Store }) => unknown;
@@ -0,0 +1,41 @@
1
+ // The store context + `useStore` — port of the context half of jotai's
2
+ // react/Provider.ts. The context instance is deduped across duplicate module
3
+ // instances via a globalThis map keyed by octane's createContext (the same
4
+ // hazard exists under Vite dev — duplicate module instances — see
5
+ // @octanejs/redux's Context.ts, which this mirrors).
6
+ import { createContext, useContext } from 'octane';
7
+ import { createStore, getDefaultStore } from 'jotai/vanilla';
8
+ import { splitSlot } from '../internal';
9
+
10
+ export type Store = ReturnType<typeof createStore>;
11
+
12
+ const ContextKey = Symbol.for('octane-jotai-store-context');
13
+ const gT: any = typeof globalThis !== 'undefined' ? globalThis : {};
14
+
15
+ function getContext() {
16
+ const contextMap: Map<typeof createContext, any> = (gT[ContextKey] ??= new Map());
17
+ let realContext = contextMap.get(createContext);
18
+ if (!realContext) {
19
+ realContext = createContext<Store | undefined>(undefined);
20
+ contextMap.set(createContext, realContext);
21
+ }
22
+ return realContext as ReturnType<typeof createContext<Store | undefined>>;
23
+ }
24
+
25
+ export const StoreContext = getContext();
26
+
27
+ type Options = {
28
+ store?: Store;
29
+ };
30
+
31
+ // Resolution order (upstream parity): an explicit `options.store` wins, then
32
+ // the nearest <Provider>'s store, then the module-global default store. The
33
+ // context read is keyed by context identity in octane, so no slot is needed —
34
+ // the trailing compiler-injected slot is stripped and dropped.
35
+ export function useStore(options?: Options): Store;
36
+ export function useStore(...rest: [options?: Options, slot?: symbol]): Store {
37
+ const [user] = splitSlot(rest);
38
+ const options = user[0] as Options | undefined;
39
+ const store = useContext(StoreContext);
40
+ return options?.store || store || getDefaultStore();
41
+ }
@@ -0,0 +1,66 @@
1
+ // useAtom — port of jotai's react/useAtom.ts: a tuple of the two base hooks.
2
+ // Each half gets its own stable sub-slot so `useAtom(a)` and `useAtom(b)` in
3
+ // one component (or the same atom twice) keep independent reducer/effect/
4
+ // callback state, exactly like distinct call sites in React.
5
+ import type {
6
+ Atom,
7
+ ExtractAtomArgs,
8
+ ExtractAtomResult,
9
+ ExtractAtomValue,
10
+ PrimitiveAtom,
11
+ SetStateAction,
12
+ WritableAtom,
13
+ } from 'jotai/vanilla';
14
+ import { useAtomValue } from './useAtomValue';
15
+ import { useSetAtom } from './useSetAtom';
16
+ import { splitSlot, subSlot } from '../internal';
17
+
18
+ type SetAtom<Args extends unknown[], Result> = (...args: Args) => Result;
19
+
20
+ type Options = Parameters<typeof useAtomValue>[1];
21
+
22
+ export function useAtom<Value, Args extends unknown[], Result>(
23
+ atom: WritableAtom<Value, Args, Result>,
24
+ options?: Options,
25
+ ): [Awaited<Value>, SetAtom<Args, Result>];
26
+
27
+ export function useAtom<Value>(
28
+ atom: PrimitiveAtom<Value>,
29
+ options?: Options,
30
+ ): [Awaited<Value>, SetAtom<[SetStateAction<Value>], void>];
31
+
32
+ export function useAtom<Value>(atom: Atom<Value>, options?: Options): [Awaited<Value>, never];
33
+
34
+ export function useAtom<AtomType extends WritableAtom<unknown, never[], unknown>>(
35
+ atom: AtomType,
36
+ options?: Options,
37
+ ): [
38
+ Awaited<ExtractAtomValue<AtomType>>,
39
+ SetAtom<ExtractAtomArgs<AtomType>, ExtractAtomResult<AtomType>>,
40
+ ];
41
+
42
+ export function useAtom<AtomType extends Atom<unknown>>(
43
+ atom: AtomType,
44
+ options?: Options,
45
+ ): [Awaited<ExtractAtomValue<AtomType>>, never];
46
+
47
+ export function useAtom<Value, Args extends unknown[], Result>(
48
+ atom: Atom<Value> | WritableAtom<Value, Args, Result>,
49
+ ...rest: [options?: Options, slot?: symbol]
50
+ ) {
51
+ const [user, slot] = splitSlot(rest);
52
+ const options = user[0] as Options | undefined;
53
+ return [
54
+ (useAtomValue as (a: unknown, o?: unknown, s?: symbol) => unknown)(
55
+ atom,
56
+ options,
57
+ subSlot(slot, 'ua:v'),
58
+ ),
59
+ // We do wrong type assertion here, which results in throwing an error.
60
+ (useSetAtom as (a: unknown, o?: unknown, s?: symbol) => unknown)(
61
+ atom,
62
+ options,
63
+ subSlot(slot, 'ua:s'),
64
+ ),
65
+ ];
66
+ }
@@ -0,0 +1,183 @@
1
+ // useAtomValue — port of jotai's react/useAtomValue.ts. Upstream reads the
2
+ // atom through a force-update reducer holding a `[value, store, atom]` tuple
3
+ // and subscribes in an effect (re-render on store notify; an unconditional
4
+ // post-subscribe rerender catches updates that raced between render and
5
+ // effect — so mounting renders twice, in React and here alike). The port keeps
6
+ // that shape exactly: octane's `useReducer` has React's semantics (lazy init;
7
+ // a no-op dispatch still renders once with children bailing), and a
8
+ // render-phase `rerender()` on store/atom swap mutates the reducer state
9
+ // synchronously and schedules a self-quiescing re-render while THIS render
10
+ // uses the locally-computed value.
11
+ //
12
+ // Async atom values suspend through octane's `use()` on a "continuable"
13
+ // promise: a wrapper whose identity is WeakMap-stable across atom
14
+ // recomputations (aborted fetches chain into it via the store's abort-handler
15
+ // registry). That stability is what lets octane's thenable replay see the SAME
16
+ // promise on every re-render until it settles — do not simplify it away.
17
+ import { use, useDebugValue, useEffect, useReducer } from 'octane';
18
+ import { INTERNAL_getBuildingBlocksRev3 as INTERNAL_getBuildingBlocks } from 'jotai/vanilla/internals';
19
+ import type { Atom, ExtractAtomValue } from 'jotai/vanilla';
20
+ import { useStore, type Store } from './store';
21
+ import { splitSlot, subSlot } from '../internal';
22
+
23
+ const isPromiseLike = (x: unknown): x is PromiseLike<unknown> =>
24
+ typeof (x as PromiseLike<unknown>)?.then === 'function';
25
+
26
+ // Opt-in (`unstable_promiseStatus`) decoration of the suspended promise with
27
+ // React 19's `status`/`value`/`reason` convention. Upstream defaults this to
28
+ // "React.use is missing"; octane always has `use`, so it defaults to false.
29
+ const attachPromiseStatus = <T>(
30
+ promise: PromiseLike<T> & {
31
+ status?: 'pending' | 'fulfilled' | 'rejected';
32
+ value?: T;
33
+ reason?: unknown;
34
+ },
35
+ ) => {
36
+ if (!promise.status) {
37
+ promise.status = 'pending';
38
+ promise.then(
39
+ (v) => {
40
+ promise.status = 'fulfilled';
41
+ promise.value = v;
42
+ },
43
+ (e) => {
44
+ promise.status = 'rejected';
45
+ promise.reason = e;
46
+ },
47
+ );
48
+ }
49
+ };
50
+
51
+ const continuablePromiseMap = new WeakMap<PromiseLike<unknown>, Promise<unknown>>();
52
+
53
+ const createContinuablePromise = <T>(
54
+ store: Store,
55
+ promise: PromiseLike<T>,
56
+ getValue: () => PromiseLike<T> | T,
57
+ ) => {
58
+ const buildingBlocks = INTERNAL_getBuildingBlocks(store);
59
+ const registerAbortHandler = buildingBlocks[26];
60
+ let continuablePromise = continuablePromiseMap.get(promise);
61
+ if (!continuablePromise) {
62
+ continuablePromise = new Promise<T>((resolve, reject) => {
63
+ let curr = promise;
64
+ const onFulfilled = (me: PromiseLike<T>) => (v: T) => {
65
+ if (curr === me) {
66
+ resolve(v);
67
+ }
68
+ };
69
+ const onRejected = (me: PromiseLike<T>) => (e: unknown) => {
70
+ if (curr === me) {
71
+ reject(e);
72
+ }
73
+ };
74
+ const onAbort = () => {
75
+ try {
76
+ const nextValue = getValue();
77
+ if (isPromiseLike(nextValue)) {
78
+ continuablePromiseMap.set(nextValue, continuablePromise!);
79
+ curr = nextValue;
80
+ nextValue.then(onFulfilled(nextValue), onRejected(nextValue));
81
+ registerAbortHandler(buildingBlocks, store, nextValue, onAbort);
82
+ } else {
83
+ resolve(nextValue);
84
+ }
85
+ } catch (e) {
86
+ reject(e);
87
+ }
88
+ };
89
+ promise.then(onFulfilled(promise), onRejected(promise));
90
+ registerAbortHandler(buildingBlocks, store, promise, onAbort);
91
+ });
92
+ continuablePromiseMap.set(promise, continuablePromise);
93
+ }
94
+ return continuablePromise;
95
+ };
96
+
97
+ type Options = Parameters<typeof useStore>[0] & {
98
+ /** @deprecated delay option is deprecated and will be removed in v3. https://github.com/pmndrs/jotai/pull/3264 */
99
+ delay?: number;
100
+ unstable_promiseStatus?: boolean;
101
+ };
102
+
103
+ export function useAtomValue<Value>(atom: Atom<Value>, options?: Options): Awaited<Value>;
104
+
105
+ export function useAtomValue<AtomType extends Atom<unknown>>(
106
+ atom: AtomType,
107
+ options?: Options,
108
+ ): Awaited<ExtractAtomValue<AtomType>>;
109
+
110
+ export function useAtomValue<Value>(
111
+ atom: Atom<Value>,
112
+ ...rest: [options?: Options, slot?: symbol]
113
+ ) {
114
+ const [user, slot] = splitSlot(rest);
115
+ const options = user[0] as Options | undefined;
116
+ const { delay, unstable_promiseStatus: promiseStatus = false } = options || {};
117
+ const store = useStore(options);
118
+
119
+ const [[valueFromReducer, storeFromReducer, atomFromReducer], rerender] = useReducer<
120
+ readonly [Value, Store, Atom<Value>],
121
+ void,
122
+ undefined
123
+ >(
124
+ (prev) => {
125
+ const nextValue = store.get(atom);
126
+ if (Object.is(prev[0], nextValue) && prev[1] === store && prev[2] === atom) {
127
+ return prev;
128
+ }
129
+ return [nextValue, store, atom];
130
+ },
131
+ undefined,
132
+ () => [store.get(atom), store, atom],
133
+ subSlot(slot, 'uav:r'),
134
+ );
135
+
136
+ let value = valueFromReducer;
137
+ if (storeFromReducer !== store || atomFromReducer !== atom) {
138
+ rerender();
139
+ value = store.get(atom);
140
+ }
141
+
142
+ useEffect(
143
+ () => {
144
+ const unsub = store.sub(atom, () => {
145
+ if (promiseStatus) {
146
+ try {
147
+ const value = store.get(atom);
148
+ if (isPromiseLike(value)) {
149
+ attachPromiseStatus(createContinuablePromise(store, value, () => store.get(atom)));
150
+ }
151
+ } catch {
152
+ // ignore
153
+ }
154
+ }
155
+ if (typeof delay === 'number') {
156
+ if (process.env.NODE_ENV !== 'production') {
157
+ console.warn(
158
+ '[DEPRECATED] delay option is deprecated and will be removed in v3. https://github.com/pmndrs/jotai/pull/3264',
159
+ );
160
+ }
161
+ // delay rerendering to wait a promise possibly to resolve
162
+ setTimeout(rerender, delay);
163
+ return;
164
+ }
165
+ rerender();
166
+ });
167
+ rerender();
168
+ return unsub;
169
+ },
170
+ [store, atom, delay, promiseStatus],
171
+ subSlot(slot, 'uav:e'),
172
+ );
173
+
174
+ useDebugValue(value);
175
+ if (isPromiseLike(value)) {
176
+ const promise = createContinuablePromise(store, value, () => store.get(atom));
177
+ if (promiseStatus) {
178
+ attachPromiseStatus(promise);
179
+ }
180
+ return use(promise);
181
+ }
182
+ return value as Awaited<Value>;
183
+ }
@@ -0,0 +1,42 @@
1
+ // useSetAtom — port of jotai's react/useSetAtom.ts. Write-only: no reducer, no
2
+ // subscription — a component using only the setter never re-renders on atom
3
+ // writes. The setter is memoized per [store, atom].
4
+ import { useCallback } from 'octane';
5
+ import type { ExtractAtomArgs, ExtractAtomResult, WritableAtom } from 'jotai/vanilla';
6
+ import { useStore, type Store } from './store';
7
+ import { splitSlot, subSlot } from '../internal';
8
+
9
+ type SetAtom<Args extends unknown[], Result> = (...args: Args) => Result;
10
+ type Options = Parameters<typeof useStore>[0];
11
+
12
+ export function useSetAtom<Value, Args extends unknown[], Result>(
13
+ atom: WritableAtom<Value, Args, Result>,
14
+ options?: Options,
15
+ ): SetAtom<Args, Result>;
16
+
17
+ export function useSetAtom<AtomType extends WritableAtom<unknown, never[], unknown>>(
18
+ atom: AtomType,
19
+ options?: Options,
20
+ ): SetAtom<ExtractAtomArgs<AtomType>, ExtractAtomResult<AtomType>>;
21
+
22
+ export function useSetAtom<Value, Args extends unknown[], Result>(
23
+ atom: WritableAtom<Value, Args, Result>,
24
+ ...rest: [options?: Options, slot?: symbol]
25
+ ) {
26
+ const [user, slot] = splitSlot(rest);
27
+ const options = user[0] as Options | undefined;
28
+ const store = useStore(options);
29
+ const setAtom = useCallback(
30
+ (...args: Args) => {
31
+ if (process.env.NODE_ENV !== 'production' && !('write' in atom)) {
32
+ // useAtom can pass non writable atom with wrong type assertion,
33
+ // so we should check here.
34
+ throw new Error('not writable atom');
35
+ }
36
+ return store.set(atom, ...args);
37
+ },
38
+ [store, atom],
39
+ subSlot(slot, 'usa:cb'),
40
+ );
41
+ return setAtom;
42
+ }
@@ -0,0 +1,170 @@
1
+ // `@octanejs/jotai/react/utils` — port of jotai's react/utils/*.ts (the four
2
+ // binding-side utils; everything else in `jotai/utils` is vanilla and reused
3
+ // verbatim). Each hook derives stable sub-slots for the base hooks it
4
+ // composes, so multiple uses in one component stay independent.
5
+ import { useCallback, useMemo } from 'octane';
6
+ import { atom } from 'jotai/vanilla';
7
+ import type { Getter, PrimitiveAtom, Setter, WritableAtom } from 'jotai/vanilla';
8
+ import { RESET } from 'jotai/vanilla/utils';
9
+ import { useStore, type Store } from './store';
10
+ import { useSetAtom } from './useSetAtom';
11
+ import { useAtom } from './useAtom';
12
+ import { splitSlot, subSlot } from '../internal';
13
+
14
+ type SetAtomOptions = Parameters<typeof useSetAtom>[1];
15
+
16
+ // useResetAtom — port of react/utils/useResetAtom.ts.
17
+ export function useResetAtom<T>(
18
+ anAtom: WritableAtom<unknown, [typeof RESET], T>,
19
+ options?: SetAtomOptions,
20
+ ): () => T;
21
+ export function useResetAtom<T>(
22
+ anAtom: WritableAtom<unknown, [typeof RESET], T>,
23
+ ...rest: [options?: SetAtomOptions, slot?: symbol]
24
+ ) {
25
+ const [user, slot] = splitSlot(rest);
26
+ const options = user[0] as SetAtomOptions;
27
+ const setAtom = (useSetAtom as (a: unknown, o?: unknown, s?: symbol) => (v: unknown) => T)(
28
+ anAtom,
29
+ options,
30
+ subSlot(slot, 'ura:set'),
31
+ );
32
+ const resetAtom = useCallback(() => setAtom(RESET), [setAtom], subSlot(slot, 'ura:cb'));
33
+ return resetAtom;
34
+ }
35
+
36
+ type AtomOptions = Parameters<typeof useAtom>[1];
37
+
38
+ /**
39
+ * @deprecated please use a recipe instead
40
+ * https://github.com/pmndrs/jotai/pull/2467
41
+ */
42
+ export function useReducerAtom<Value, Action>(
43
+ anAtom: PrimitiveAtom<Value>,
44
+ reducer: (v: Value, a?: Action) => Value,
45
+ options?: AtomOptions,
46
+ ): [Value, (action?: Action) => void];
47
+ /**
48
+ * @deprecated please use a recipe instead
49
+ * https://github.com/pmndrs/jotai/pull/2467
50
+ */
51
+ export function useReducerAtom<Value, Action>(
52
+ anAtom: PrimitiveAtom<Value>,
53
+ reducer: (v: Value, a: Action) => Value,
54
+ options?: AtomOptions,
55
+ ): [Value, (action: Action) => void];
56
+ export function useReducerAtom<Value, Action>(
57
+ anAtom: PrimitiveAtom<Value>,
58
+ reducer: (v: Value, a: Action) => Value,
59
+ ...rest: [options?: AtomOptions, slot?: symbol]
60
+ ) {
61
+ const [user, slot] = splitSlot(rest);
62
+ const options = user[0] as AtomOptions;
63
+ if (process.env.NODE_ENV !== 'production') {
64
+ console.warn(
65
+ '[DEPRECATED] useReducerAtom is deprecated and will be removed in the future. Please create your own version using the recipe. https://github.com/pmndrs/jotai/pull/2467',
66
+ );
67
+ }
68
+ const [state, setState] = (
69
+ useAtom as (
70
+ a: unknown,
71
+ o?: unknown,
72
+ s?: symbol,
73
+ ) => [Value, (update: (prev: Value) => Value) => void]
74
+ )(anAtom, options, subSlot(slot, 'urda:a'));
75
+ const dispatch = useCallback(
76
+ (action: Action) => {
77
+ setState((prev) => reducer(prev, action));
78
+ },
79
+ [setState, reducer],
80
+ subSlot(slot, 'urda:cb'),
81
+ );
82
+ return [state, dispatch];
83
+ }
84
+
85
+ // useAtomCallback — port of react/utils/useAtomCallback.ts: an ephemeral
86
+ // write-only atom memoized per callback, exposed through useSetAtom.
87
+ export function useAtomCallback<Result, Args extends unknown[]>(
88
+ callback: (get: Getter, set: Setter, ...arg: Args) => Result,
89
+ options?: SetAtomOptions,
90
+ ): (...args: Args) => Result;
91
+ export function useAtomCallback<Result, Args extends unknown[]>(
92
+ callback: (get: Getter, set: Setter, ...arg: Args) => Result,
93
+ ...rest: [options?: SetAtomOptions, slot?: symbol]
94
+ ) {
95
+ const [user, slot] = splitSlot(rest);
96
+ const options = user[0] as SetAtomOptions;
97
+ const anAtom = useMemo(
98
+ () => atom(null, (get, set, ...args: Args) => callback(get, set, ...args)),
99
+ [callback],
100
+ subSlot(slot, 'uac:m'),
101
+ );
102
+ return (useSetAtom as (a: unknown, o?: unknown, s?: symbol) => (...args: Args) => Result)(
103
+ anAtom,
104
+ options,
105
+ subSlot(slot, 'uac:set'),
106
+ );
107
+ }
108
+
109
+ type HydrateOptions = Parameters<typeof useStore>[0] & {
110
+ dangerouslyForceHydrate?: boolean;
111
+ };
112
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+ type AnyWritableAtom = WritableAtom<any, any[], any>;
114
+
115
+ type InferAtomTuples<T> = {
116
+ [K in keyof T]: T[K] extends readonly [infer A, ...infer Rest]
117
+ ? A extends WritableAtom<unknown, infer Args, unknown>
118
+ ? Rest extends Args
119
+ ? readonly [A, ...Rest]
120
+ : never
121
+ : T[K]
122
+ : never;
123
+ };
124
+
125
+ // For internal use only
126
+ // This can be changed without notice.
127
+ export type INTERNAL_InferAtomTuples<T> = InferAtomTuples<T>;
128
+
129
+ const hydratedMap: WeakMap<Store, WeakSet<AnyWritableAtom>> = new WeakMap();
130
+
131
+ // useHydrateAtoms — port of react/utils/useHydrateAtoms.ts. Writes each
132
+ // [atom, value] into the resolved store DURING render, once per (store, atom)
133
+ // unless dangerouslyForceHydrate. No slotted hooks — only the context read.
134
+ export function useHydrateAtoms<T extends (readonly [AnyWritableAtom, ...unknown[]])[]>(
135
+ values: InferAtomTuples<T>,
136
+ options?: HydrateOptions,
137
+ ): void;
138
+ export function useHydrateAtoms<T extends Map<AnyWritableAtom, unknown>>(
139
+ values: T,
140
+ options?: HydrateOptions,
141
+ ): void;
142
+ export function useHydrateAtoms<T extends Iterable<readonly [AnyWritableAtom, ...unknown[]]>>(
143
+ values: InferAtomTuples<T>,
144
+ options?: HydrateOptions,
145
+ ): void;
146
+ export function useHydrateAtoms<T extends Iterable<readonly [AnyWritableAtom, ...unknown[]]>>(
147
+ values: T,
148
+ ...rest: [options?: HydrateOptions, slot?: symbol]
149
+ ) {
150
+ const [user] = splitSlot(rest);
151
+ const options = user[0] as HydrateOptions | undefined;
152
+ const store = useStore(options);
153
+
154
+ const hydratedSet = getHydratedSet(store);
155
+ for (const [atom, ...args] of values) {
156
+ if (!hydratedSet.has(atom) || options?.dangerouslyForceHydrate) {
157
+ hydratedSet.add(atom);
158
+ store.set(atom, ...args);
159
+ }
160
+ }
161
+ }
162
+
163
+ const getHydratedSet = (store: Store) => {
164
+ let hydratedSet = hydratedMap.get(store);
165
+ if (!hydratedSet) {
166
+ hydratedSet = new WeakSet();
167
+ hydratedMap.set(store, hydratedSet);
168
+ }
169
+ return hydratedSet;
170
+ };
package/src/react.ts ADDED
@@ -0,0 +1,10 @@
1
+ // `@octanejs/jotai/react` — the binding layer, ported from jotai's react.ts.
2
+ // Upstream's implementation shape is kept deliberately (a force-update
3
+ // useReducer + effect subscription, NOT useSyncExternalStore) so behavior —
4
+ // including re-render timing and write-only non-re-rendering — matches jotai
5
+ // on React.
6
+ export { Provider } from './react/Provider.tsrx';
7
+ export { useStore } from './react/store';
8
+ export { useAtomValue } from './react/useAtomValue';
9
+ export { useSetAtom } from './react/useSetAtom';
10
+ export { useAtom } from './react/useAtom';
package/src/utils.ts ADDED
@@ -0,0 +1,4 @@
1
+ // `@octanejs/jotai/utils` — mirror of upstream jotai's utils.ts: the vanilla
2
+ // utils (re-exported verbatim from jotai) plus the ported binding-side utils.
3
+ export * from './vanilla/utils';
4
+ export * from './react/utils';
@@ -0,0 +1,6 @@
1
+ // `@octanejs/jotai/vanilla/internals` — re-exported verbatim from jotai.
2
+ //
3
+ // The INTERNAL_* store building blocks are framework-agnostic and unstable by
4
+ // contract upstream; they're re-exported unchanged for surface parity (and used
5
+ // by this package's own async-atom plumbing, see react/useAtomValue.ts).
6
+ export * from 'jotai/vanilla/internals';
@@ -0,0 +1,8 @@
1
+ // `@octanejs/jotai/vanilla/utils` — re-exported verbatim from jotai.
2
+ //
3
+ // Every vanilla util (RESET, atomWithReset, atomWithDefault, atomWithStorage,
4
+ // atomWithReducer, atomWithRefresh, atomWithLazy, atomWithObservable, atomFamily,
5
+ // selectAtom, splitAtom, loadable, unwrap, freezeAtom, …) is a framework-agnostic
6
+ // atom factory or store helper — it composes with octane's `useAtom`/`useAtomValue`
7
+ // unchanged, so we re-export it as-is (mirroring src/vanilla.ts).
8
+ export * from 'jotai/vanilla/utils';
package/src/vanilla.ts ADDED
@@ -0,0 +1,6 @@
1
+ // The framework-agnostic core, re-exported verbatim from jotai. `atom`,
2
+ // `createStore`, and `getDefaultStore` are pure JS with no React import — only
3
+ // the React binding (see react.ts) is swapped for one built on octane's hooks.
4
+ // Authors who only need atoms + a store (no component binding) can import from
5
+ // `@octanejs/jotai/vanilla` exactly as they would from `jotai/vanilla`.
6
+ export * from 'jotai/vanilla';