@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 +21 -0
- package/README.md +90 -0
- package/package.json +56 -0
- package/src/index.ts +20 -0
- package/src/internal.ts +37 -0
- package/src/react/Provider.tsrx +19 -0
- package/src/react/Provider.tsrx.d.ts +4 -0
- package/src/react/store.ts +41 -0
- package/src/react/useAtom.ts +66 -0
- package/src/react/useAtomValue.ts +183 -0
- package/src/react/useSetAtom.ts +42 -0
- package/src/react/utils.ts +170 -0
- package/src/react.ts +10 -0
- package/src/utils.ts +4 -0
- package/src/vanilla/internals.ts +6 -0
- package/src/vanilla/utils.ts +8 -0
- package/src/vanilla.ts +6 -0
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';
|
package/src/internal.ts
ADDED
|
@@ -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,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,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';
|