@pumped-fn/lite-react 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,66 @@
1
+ # @pumped-fn/lite-react
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 236aa4a: Rename packages to follow `lite-` prefix convention
8
+
9
+ **Breaking Change:** Package names have been renamed:
10
+
11
+ - `@pumped-fn/devtools` → `@pumped-fn/lite-devtools`
12
+ - `@pumped-fn/react-lite` → `@pumped-fn/lite-react`
13
+ - `@pumped-fn/vite-hmr` → `@pumped-fn/lite-hmr`
14
+
15
+ This establishes a consistent naming convention where all packages in the lite ecosystem use the `lite-` prefix.
16
+
17
+ **Migration:**
18
+
19
+ ```bash
20
+ # Update your dependencies
21
+ pnpm remove @pumped-fn/devtools @pumped-fn/react-lite @pumped-fn/vite-hmr
22
+ pnpm add @pumped-fn/lite-devtools @pumped-fn/lite-react @pumped-fn/lite-hmr
23
+ ```
24
+
25
+ ```typescript
26
+ // Update imports
27
+ - import { createDevtools } from '@pumped-fn/devtools'
28
+ + import { createDevtools } from '@pumped-fn/lite-devtools'
29
+
30
+ - import { ScopeProvider, useAtom } from '@pumped-fn/react-lite'
31
+ + import { ScopeProvider, useAtom } from '@pumped-fn/lite-react'
32
+
33
+ - import { pumpedHmr } from '@pumped-fn/vite-hmr'
34
+ + import { pumpedHmr } from '@pumped-fn/lite-hmr'
35
+ ```
36
+
37
+ ## 0.3.0
38
+
39
+ ### Minor Changes
40
+
41
+ - a0362d7: ### Features
42
+
43
+ - Re-export `createScope`, `atom`, `flow`, `preset` from `@pumped-fn/lite` for convenience
44
+ - Update React peer dependency to support both React 18 and React 19 (`^18.0.0 || ^19.0.0`)
45
+
46
+ ### Bug Fixes
47
+
48
+ - **Critical**: Fix Suspense infinite loop by caching pending promises (React expects same promise during re-renders)
49
+ - Auto-resolve idle atoms lazily instead of throwing error (more ergonomic)
50
+ - Subscribe only to `resolved` events instead of `*` to avoid unnecessary re-renders
51
+
52
+ ## 0.2.0
53
+
54
+ ### Minor Changes
55
+
56
+ - 1587c37: feat(lite-react): initial release of React integration for @pumped-fn/lite
57
+
58
+ Adds minimal React bindings with Suspense and ErrorBoundary integration:
59
+
60
+ - ScopeProvider and ScopeContext for scope provisioning
61
+ - useScope hook for accessing scope from context
62
+ - useController hook for obtaining memoized controllers
63
+ - useAtom hook with full Suspense/ErrorBoundary integration
64
+ - useSelect hook for fine-grained reactivity with custom equality
65
+
66
+ SSR-compatible, zero-tolerance for `any` types, comprehensive TSDoc.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License Copyright (c) 2025 Duke
2
+
3
+ Permission is hereby granted, free of
4
+ charge, to any person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use, copy, modify, merge,
7
+ publish, distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice
12
+ (including the next paragraph) shall be included in all copies or substantial
13
+ portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # @pumped-fn/lite-react
2
+
3
+ React bindings for `@pumped-fn/lite` with Suspense and ErrorBoundary integration.
4
+
5
+ **Zero dependencies** · **<2KB bundle** · **React 18+**
6
+
7
+ ## How It Works
8
+
9
+ ```mermaid
10
+ sequenceDiagram
11
+ participant App
12
+ participant ScopeProvider
13
+ participant useAtom
14
+ participant Controller
15
+
16
+ App->>App: scope.resolve(atom)
17
+ App->>ScopeProvider: <ScopeProvider scope={scope}>
18
+
19
+ useAtom->>Controller: check ctrl.state
20
+ alt resolved
21
+ Controller-->>useAtom: value
22
+ useAtom->>Controller: subscribe to changes
23
+ else resolving
24
+ useAtom-->>App: throw Promise (Suspense)
25
+ else failed
26
+ useAtom-->>App: throw Error (ErrorBoundary)
27
+ else idle
28
+ useAtom-->>App: throw Error (not resolved)
29
+ end
30
+ ```
31
+
32
+ ## State Handling
33
+
34
+ ```mermaid
35
+ flowchart TD
36
+ Hook[useAtom/useSelect]
37
+ Hook --> State{ctrl.state?}
38
+
39
+ State -->|idle| AutoResolve[Auto-resolve + Throw Promise]
40
+ State -->|resolving| Promise[Throw cached Promise]
41
+ State -->|resolved| Value[Return value]
42
+ State -->|failed| Stored[Throw stored error]
43
+
44
+ AutoResolve --> Suspense[Suspense catches]
45
+ Promise --> Suspense
46
+ Stored --> ErrorBoundary[ErrorBoundary catches]
47
+ ```
48
+
49
+ | State | Hook Behavior |
50
+ |-------|---------------|
51
+ | `idle` | Auto-resolves and suspends — Suspense shows fallback |
52
+ | `resolving` | Throws cached promise — Suspense shows fallback |
53
+ | `resolved` | Returns value, subscribes to changes |
54
+ | `failed` | Throws stored error — ErrorBoundary catches |
55
+
56
+ ## API
57
+
58
+ ### ScopeProvider
59
+
60
+ Provides scope to component tree.
61
+
62
+ ```tsx
63
+ import { createScope } from '@pumped-fn/lite'
64
+ import { ScopeProvider } from '@pumped-fn/lite-react'
65
+
66
+ const scope = createScope()
67
+ await scope.resolve(userAtom)
68
+
69
+ <ScopeProvider scope={scope}>
70
+ <App />
71
+ </ScopeProvider>
72
+ ```
73
+
74
+ ### useScope
75
+
76
+ Access scope from context.
77
+
78
+ ```tsx
79
+ const scope = useScope()
80
+ await scope.resolve(someAtom)
81
+ ```
82
+
83
+ ### useController
84
+
85
+ Get memoized controller for imperative operations.
86
+
87
+ ```tsx
88
+ const ctrl = useController(counterAtom)
89
+ ctrl.set(10)
90
+ ctrl.update(n => n + 1)
91
+ ctrl.invalidate()
92
+ ```
93
+
94
+ ### useAtom
95
+
96
+ Subscribe to atom value with Suspense integration.
97
+
98
+ ```tsx
99
+ function UserProfile() {
100
+ const user = useAtom(userAtom)
101
+ return <div>{user.name}</div>
102
+ }
103
+
104
+ // Wrap with Suspense + ErrorBoundary
105
+ <ErrorBoundary fallback={<Error />}>
106
+ <Suspense fallback={<Loading />}>
107
+ <UserProfile />
108
+ </Suspense>
109
+ </ErrorBoundary>
110
+ ```
111
+
112
+ ### useSelect
113
+
114
+ Fine-grained selection — only re-renders when selected value changes.
115
+
116
+ ```tsx
117
+ const name = useSelect(userAtom, user => user.name)
118
+ const count = useSelect(todosAtom, todos => todos.length, (a, b) => a === b)
119
+ ```
120
+
121
+ ## Invalidation
122
+
123
+ When an atom is invalidated, hooks automatically suspend during re-resolution:
124
+
125
+ ```mermaid
126
+ sequenceDiagram
127
+ participant Component
128
+ participant useAtom
129
+ participant Controller
130
+
131
+ Note over Controller: state = resolved
132
+ Component->>useAtom: render (value)
133
+
134
+ Note over Controller: ctrl.invalidate()
135
+ Controller->>Controller: state = resolving
136
+ useAtom-->>Component: throw Promise
137
+ Note over Component: Suspense fallback
138
+
139
+ Controller->>Controller: factory runs
140
+ Controller->>Controller: state = resolved
141
+ useAtom->>Component: re-render (new value)
142
+ ```
143
+
144
+ ## Testing
145
+
146
+ Use presets for test isolation:
147
+
148
+ ```tsx
149
+ import { createScope, preset } from '@pumped-fn/lite'
150
+ import { ScopeProvider } from '@pumped-fn/lite-react'
151
+
152
+ const scope = createScope({
153
+ presets: [preset(userAtom, { name: 'Test User' })]
154
+ })
155
+ await scope.resolve(userAtom)
156
+
157
+ render(
158
+ <ScopeProvider scope={scope}>
159
+ <UserProfile />
160
+ </ScopeProvider>
161
+ )
162
+ ```
163
+
164
+ ## SSR
165
+
166
+ SSR-compatible by design:
167
+
168
+ - No side effects on import
169
+ - Uses `useSyncExternalStore` with server snapshot
170
+ - Scope passed as prop (no global state)
171
+
172
+ ```tsx
173
+ // Server
174
+ const scope = createScope()
175
+ await scope.resolve(dataAtom)
176
+ const html = renderToString(<ScopeProvider scope={scope}><App /></ScopeProvider>)
177
+
178
+ // Client
179
+ const clientScope = createScope({
180
+ presets: [preset(dataAtom, window.__DATA__)]
181
+ })
182
+ await clientScope.resolve(dataAtom)
183
+ hydrateRoot(root, <ScopeProvider scope={clientScope}><App /></ScopeProvider>)
184
+ ```
185
+
186
+ ## Full API
187
+
188
+ See [`dist/index.d.mts`](./dist/index.d.mts) for complete type definitions.
189
+
190
+ ## License
191
+
192
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,173 @@
1
+ let __pumped_fn_lite = require("@pumped-fn/lite");
2
+ let react = require("react");
3
+ let react_jsx_runtime = require("react/jsx-runtime");
4
+
5
+ //#region src/context.tsx
6
+ /**
7
+ * React context for Lite.Scope.
8
+ */
9
+ const ScopeContext = (0, react.createContext)(null);
10
+ /**
11
+ * Provider component for Lite.Scope.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <ScopeProvider scope={scope}>
16
+ * <App />
17
+ * </ScopeProvider>
18
+ * ```
19
+ */
20
+ function ScopeProvider({ scope, children }) {
21
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScopeContext.Provider, {
22
+ value: scope,
23
+ children
24
+ });
25
+ }
26
+
27
+ //#endregion
28
+ //#region src/hooks.ts
29
+ const pendingPromises = /* @__PURE__ */ new WeakMap();
30
+ function getOrCreatePendingPromise(atom$1, ctrl) {
31
+ let pending = pendingPromises.get(atom$1);
32
+ if (!pending) {
33
+ pending = ctrl.resolve();
34
+ pendingPromises.set(atom$1, pending);
35
+ pending.finally(() => pendingPromises.delete(atom$1));
36
+ }
37
+ return pending;
38
+ }
39
+ /**
40
+ * Access the current Lite.Scope from context.
41
+ *
42
+ * @returns The current Lite.Scope instance from context
43
+ * @throws When called outside of a ScopeProvider
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * const scope = useScope()
48
+ * await scope.resolve(myAtom)
49
+ * ```
50
+ */
51
+ function useScope() {
52
+ const scope = (0, react.useContext)(ScopeContext);
53
+ if (!scope) throw new Error("useScope must be used within a ScopeProvider");
54
+ return scope;
55
+ }
56
+ /**
57
+ * Get a memoized controller for an atom.
58
+ *
59
+ * @param atom - The atom to create a controller for
60
+ * @returns A memoized Lite.Controller instance
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const ctrl = useController(counterAtom)
65
+ * ctrl.set(ctrl.get() + 1)
66
+ * ```
67
+ */
68
+ function useController(atom$1) {
69
+ const scope = useScope();
70
+ return (0, react.useMemo)(() => scope.controller(atom$1), [scope, atom$1]);
71
+ }
72
+ /**
73
+ * Subscribe to atom value with Suspense/ErrorBoundary integration.
74
+ * Auto-resolves atoms lazily and throws cached Promise for Suspense.
75
+ *
76
+ * @param atom - The atom to read
77
+ * @returns The current value of the atom
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * function UserProfile() {
82
+ * const user = useAtom(userAtom)
83
+ * return <div>{user.name}</div>
84
+ * }
85
+ * ```
86
+ */
87
+ function useAtom(atom$1) {
88
+ const ctrl = useController(atom$1);
89
+ const atomRef = (0, react.useRef)(atom$1);
90
+ atomRef.current = atom$1;
91
+ const getSnapshot = (0, react.useCallback)(() => {
92
+ const state = ctrl.state;
93
+ if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
94
+ if (state === "failed") throw ctrl.get();
95
+ return ctrl.get();
96
+ }, [ctrl]);
97
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((onStoreChange) => ctrl.on("resolved", onStoreChange), [ctrl]), getSnapshot, getSnapshot);
98
+ }
99
+ /**
100
+ * Select a derived value from an atom with fine-grained reactivity.
101
+ * Only re-renders when the selected value changes per equality function.
102
+ *
103
+ * @param atom - The atom to select from
104
+ * @param selector - Function to extract a derived value
105
+ * @param eq - Optional equality function
106
+ * @returns The selected value
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * const name = useSelect(userAtom, user => user.name)
111
+ * ```
112
+ */
113
+ function useSelect(atom$1, selector, eq) {
114
+ const scope = useScope();
115
+ const ctrl = useController(atom$1);
116
+ const atomRef = (0, react.useRef)(atom$1);
117
+ atomRef.current = atom$1;
118
+ const selectorRef = (0, react.useRef)(selector);
119
+ const eqRef = (0, react.useRef)(eq);
120
+ selectorRef.current = selector;
121
+ eqRef.current = eq;
122
+ const handleRef = (0, react.useRef)(null);
123
+ const getOrCreateHandle = (0, react.useCallback)(() => {
124
+ if (!handleRef.current || handleRef.current.scope !== scope || handleRef.current.atom !== atom$1) handleRef.current = {
125
+ scope,
126
+ atom: atom$1,
127
+ handle: scope.select(atom$1, selectorRef.current, { eq: eqRef.current })
128
+ };
129
+ return handleRef.current.handle;
130
+ }, [scope, atom$1]);
131
+ const getSnapshot = (0, react.useCallback)(() => {
132
+ const state = ctrl.state;
133
+ if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
134
+ if (state === "failed") throw ctrl.get();
135
+ return getOrCreateHandle().get();
136
+ }, [ctrl, getOrCreateHandle]);
137
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((onStoreChange) => {
138
+ if (ctrl.state !== "resolved") return () => {};
139
+ return getOrCreateHandle().subscribe(onStoreChange);
140
+ }, [ctrl, getOrCreateHandle]), getSnapshot, getSnapshot);
141
+ }
142
+
143
+ //#endregion
144
+ exports.ScopeContext = ScopeContext;
145
+ exports.ScopeProvider = ScopeProvider;
146
+ Object.defineProperty(exports, 'atom', {
147
+ enumerable: true,
148
+ get: function () {
149
+ return __pumped_fn_lite.atom;
150
+ }
151
+ });
152
+ Object.defineProperty(exports, 'createScope', {
153
+ enumerable: true,
154
+ get: function () {
155
+ return __pumped_fn_lite.createScope;
156
+ }
157
+ });
158
+ Object.defineProperty(exports, 'flow', {
159
+ enumerable: true,
160
+ get: function () {
161
+ return __pumped_fn_lite.flow;
162
+ }
163
+ });
164
+ Object.defineProperty(exports, 'preset', {
165
+ enumerable: true,
166
+ get: function () {
167
+ return __pumped_fn_lite.preset;
168
+ }
169
+ });
170
+ exports.useAtom = useAtom;
171
+ exports.useController = useController;
172
+ exports.useScope = useScope;
173
+ exports.useSelect = useSelect;
@@ -0,0 +1,90 @@
1
+ import { Lite, Lite as Lite$1, atom, createScope, flow, preset } from "@pumped-fn/lite";
2
+ import * as react0 from "react";
3
+ import { ReactNode } from "react";
4
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
5
+
6
+ //#region src/context.d.ts
7
+ /**
8
+ * React context for Lite.Scope.
9
+ */
10
+ declare const ScopeContext: react0.Context<Lite$1.Scope | null>;
11
+ interface ScopeProviderProps {
12
+ scope: Lite$1.Scope;
13
+ children: ReactNode;
14
+ }
15
+ /**
16
+ * Provider component for Lite.Scope.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <ScopeProvider scope={scope}>
21
+ * <App />
22
+ * </ScopeProvider>
23
+ * ```
24
+ */
25
+ declare function ScopeProvider({
26
+ scope,
27
+ children
28
+ }: ScopeProviderProps): react_jsx_runtime0.JSX.Element;
29
+ //#endregion
30
+ //#region src/hooks.d.ts
31
+ /**
32
+ * Access the current Lite.Scope from context.
33
+ *
34
+ * @returns The current Lite.Scope instance from context
35
+ * @throws When called outside of a ScopeProvider
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * const scope = useScope()
40
+ * await scope.resolve(myAtom)
41
+ * ```
42
+ */
43
+ declare function useScope(): Lite$1.Scope;
44
+ /**
45
+ * Get a memoized controller for an atom.
46
+ *
47
+ * @param atom - The atom to create a controller for
48
+ * @returns A memoized Lite.Controller instance
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * const ctrl = useController(counterAtom)
53
+ * ctrl.set(ctrl.get() + 1)
54
+ * ```
55
+ */
56
+ declare function useController<T>(atom: Lite$1.Atom<T>): Lite$1.Controller<T>;
57
+ /**
58
+ * Subscribe to atom value with Suspense/ErrorBoundary integration.
59
+ * Auto-resolves atoms lazily and throws cached Promise for Suspense.
60
+ *
61
+ * @param atom - The atom to read
62
+ * @returns The current value of the atom
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * function UserProfile() {
67
+ * const user = useAtom(userAtom)
68
+ * return <div>{user.name}</div>
69
+ * }
70
+ * ```
71
+ */
72
+ declare function useAtom<T>(atom: Lite$1.Atom<T>): T;
73
+ /**
74
+ * Select a derived value from an atom with fine-grained reactivity.
75
+ * Only re-renders when the selected value changes per equality function.
76
+ *
77
+ * @param atom - The atom to select from
78
+ * @param selector - Function to extract a derived value
79
+ * @param eq - Optional equality function
80
+ * @returns The selected value
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * const name = useSelect(userAtom, user => user.name)
85
+ * ```
86
+ */
87
+ declare function useSelect<T, S>(atom: Lite$1.Atom<T>, selector: (value: T) => S, eq?: (a: S, b: S) => boolean): S;
88
+ //#endregion
89
+ export { type Lite, ScopeContext, ScopeProvider, type ScopeProviderProps, atom, createScope, flow, preset, useAtom, useController, useScope, useSelect };
90
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAMM,cAAY,MAAA,CAAA,QAAA,MAAA,CAAA;AALyB,UAOjC,kBAAA,CAFiD;EAEjD,KAAA,EACD,MAAA,CAAK,KADJ;EAeD,QAAA,EAbG,SAaU;;;;;;;;;ACtBqB;AA2BZ;;iBDLtB,aAAA,CCyB4B;EAAA,KAAA;EAAA;AAAA,CAAA,EDzBO,kBCyBP,CAAA,EDzByB,kBAAA,CAAA,GAAA,CAAA,OCyBzB;;;;;;;;AD/CM;AAKzB;AAIG;;;;;iBCkBZ,QAAA,CAAA,CDLqD,ECKzC,MAAA,CAAK,KDLoC;;;;ACtBnB;AA2BZ;;;;;;AAoB+B;;iBAArD,aAoBsB,CAAA,CAAA,CAAA,CAAA,IAAA,EApBC,MAAA,CAAK,IAoBN,CApBW,CAoBX,CAAA,CAAA,EApBgB,MAAA,CAAK,UAoBrB,CApBgC,CAoBhC,CAAA;;;AAAW;;;;;;;;;;;;;iBAAjC,iBAAiB,MAAA,CAAK,KAAK,KAAK;;;;;;;;;;;;;;;iBAsChC,sBACD,MAAA,CAAK,KAAK,sBACE,MAAM,YACf,MAAM,gBACd"}
@@ -0,0 +1,90 @@
1
+ import { Lite, Lite as Lite$1, atom, createScope, flow, preset } from "@pumped-fn/lite";
2
+ import * as react0 from "react";
3
+ import { ReactNode } from "react";
4
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
5
+
6
+ //#region src/context.d.ts
7
+ /**
8
+ * React context for Lite.Scope.
9
+ */
10
+ declare const ScopeContext: react0.Context<Lite$1.Scope | null>;
11
+ interface ScopeProviderProps {
12
+ scope: Lite$1.Scope;
13
+ children: ReactNode;
14
+ }
15
+ /**
16
+ * Provider component for Lite.Scope.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <ScopeProvider scope={scope}>
21
+ * <App />
22
+ * </ScopeProvider>
23
+ * ```
24
+ */
25
+ declare function ScopeProvider({
26
+ scope,
27
+ children
28
+ }: ScopeProviderProps): react_jsx_runtime0.JSX.Element;
29
+ //#endregion
30
+ //#region src/hooks.d.ts
31
+ /**
32
+ * Access the current Lite.Scope from context.
33
+ *
34
+ * @returns The current Lite.Scope instance from context
35
+ * @throws When called outside of a ScopeProvider
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * const scope = useScope()
40
+ * await scope.resolve(myAtom)
41
+ * ```
42
+ */
43
+ declare function useScope(): Lite$1.Scope;
44
+ /**
45
+ * Get a memoized controller for an atom.
46
+ *
47
+ * @param atom - The atom to create a controller for
48
+ * @returns A memoized Lite.Controller instance
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * const ctrl = useController(counterAtom)
53
+ * ctrl.set(ctrl.get() + 1)
54
+ * ```
55
+ */
56
+ declare function useController<T>(atom: Lite$1.Atom<T>): Lite$1.Controller<T>;
57
+ /**
58
+ * Subscribe to atom value with Suspense/ErrorBoundary integration.
59
+ * Auto-resolves atoms lazily and throws cached Promise for Suspense.
60
+ *
61
+ * @param atom - The atom to read
62
+ * @returns The current value of the atom
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * function UserProfile() {
67
+ * const user = useAtom(userAtom)
68
+ * return <div>{user.name}</div>
69
+ * }
70
+ * ```
71
+ */
72
+ declare function useAtom<T>(atom: Lite$1.Atom<T>): T;
73
+ /**
74
+ * Select a derived value from an atom with fine-grained reactivity.
75
+ * Only re-renders when the selected value changes per equality function.
76
+ *
77
+ * @param atom - The atom to select from
78
+ * @param selector - Function to extract a derived value
79
+ * @param eq - Optional equality function
80
+ * @returns The selected value
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * const name = useSelect(userAtom, user => user.name)
85
+ * ```
86
+ */
87
+ declare function useSelect<T, S>(atom: Lite$1.Atom<T>, selector: (value: T) => S, eq?: (a: S, b: S) => boolean): S;
88
+ //#endregion
89
+ export { type Lite, ScopeContext, ScopeProvider, type ScopeProviderProps, atom, createScope, flow, preset, useAtom, useController, useScope, useSelect };
90
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cAMM,cAAY,MAAA,CAAA,QAAA,MAAA,CAAA;AALyB,UAOjC,kBAAA,CAFiD;EAEjD,KAAA,EACD,MAAA,CAAK,KADJ;EAeD,QAAA,EAbG,SAaU;;;;;;;;;ACtBqB;AA2BZ;;iBDLtB,aAAA,CCyB4B;EAAA,KAAA;EAAA;AAAA,CAAA,EDzBO,kBCyBP,CAAA,EDzByB,kBAAA,CAAA,GAAA,CAAA,OCyBzB;;;;;;;;AD/CM;AAKzB;AAIG;;;;;iBCkBZ,QAAA,CAAA,CDLqD,ECKzC,MAAA,CAAK,KDLoC;;;;ACtBnB;AA2BZ;;;;;;AAoB+B;;iBAArD,aAoBsB,CAAA,CAAA,CAAA,CAAA,IAAA,EApBC,MAAA,CAAK,IAoBN,CApBW,CAoBX,CAAA,CAAA,EApBgB,MAAA,CAAK,UAoBrB,CApBgC,CAoBhC,CAAA;;;AAAW;;;;;;;;;;;;;iBAAjC,iBAAiB,MAAA,CAAK,KAAK,KAAK;;;;;;;;;;;;;;;iBAsChC,sBACD,MAAA,CAAK,KAAK,sBACE,MAAM,YACf,MAAM,gBACd"}
package/dist/index.mjs ADDED
@@ -0,0 +1,145 @@
1
+ import { atom, createScope, flow, preset } from "@pumped-fn/lite";
2
+ import { createContext, useCallback, useContext, useMemo, useRef, useSyncExternalStore } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+
5
+ //#region src/context.tsx
6
+ /**
7
+ * React context for Lite.Scope.
8
+ */
9
+ const ScopeContext = createContext(null);
10
+ /**
11
+ * Provider component for Lite.Scope.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <ScopeProvider scope={scope}>
16
+ * <App />
17
+ * </ScopeProvider>
18
+ * ```
19
+ */
20
+ function ScopeProvider({ scope, children }) {
21
+ return /* @__PURE__ */ jsx(ScopeContext.Provider, {
22
+ value: scope,
23
+ children
24
+ });
25
+ }
26
+
27
+ //#endregion
28
+ //#region src/hooks.ts
29
+ const pendingPromises = /* @__PURE__ */ new WeakMap();
30
+ function getOrCreatePendingPromise(atom$1, ctrl) {
31
+ let pending = pendingPromises.get(atom$1);
32
+ if (!pending) {
33
+ pending = ctrl.resolve();
34
+ pendingPromises.set(atom$1, pending);
35
+ pending.finally(() => pendingPromises.delete(atom$1));
36
+ }
37
+ return pending;
38
+ }
39
+ /**
40
+ * Access the current Lite.Scope from context.
41
+ *
42
+ * @returns The current Lite.Scope instance from context
43
+ * @throws When called outside of a ScopeProvider
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * const scope = useScope()
48
+ * await scope.resolve(myAtom)
49
+ * ```
50
+ */
51
+ function useScope() {
52
+ const scope = useContext(ScopeContext);
53
+ if (!scope) throw new Error("useScope must be used within a ScopeProvider");
54
+ return scope;
55
+ }
56
+ /**
57
+ * Get a memoized controller for an atom.
58
+ *
59
+ * @param atom - The atom to create a controller for
60
+ * @returns A memoized Lite.Controller instance
61
+ *
62
+ * @example
63
+ * ```tsx
64
+ * const ctrl = useController(counterAtom)
65
+ * ctrl.set(ctrl.get() + 1)
66
+ * ```
67
+ */
68
+ function useController(atom$1) {
69
+ const scope = useScope();
70
+ return useMemo(() => scope.controller(atom$1), [scope, atom$1]);
71
+ }
72
+ /**
73
+ * Subscribe to atom value with Suspense/ErrorBoundary integration.
74
+ * Auto-resolves atoms lazily and throws cached Promise for Suspense.
75
+ *
76
+ * @param atom - The atom to read
77
+ * @returns The current value of the atom
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * function UserProfile() {
82
+ * const user = useAtom(userAtom)
83
+ * return <div>{user.name}</div>
84
+ * }
85
+ * ```
86
+ */
87
+ function useAtom(atom$1) {
88
+ const ctrl = useController(atom$1);
89
+ const atomRef = useRef(atom$1);
90
+ atomRef.current = atom$1;
91
+ const getSnapshot = useCallback(() => {
92
+ const state = ctrl.state;
93
+ if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
94
+ if (state === "failed") throw ctrl.get();
95
+ return ctrl.get();
96
+ }, [ctrl]);
97
+ return useSyncExternalStore(useCallback((onStoreChange) => ctrl.on("resolved", onStoreChange), [ctrl]), getSnapshot, getSnapshot);
98
+ }
99
+ /**
100
+ * Select a derived value from an atom with fine-grained reactivity.
101
+ * Only re-renders when the selected value changes per equality function.
102
+ *
103
+ * @param atom - The atom to select from
104
+ * @param selector - Function to extract a derived value
105
+ * @param eq - Optional equality function
106
+ * @returns The selected value
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * const name = useSelect(userAtom, user => user.name)
111
+ * ```
112
+ */
113
+ function useSelect(atom$1, selector, eq) {
114
+ const scope = useScope();
115
+ const ctrl = useController(atom$1);
116
+ const atomRef = useRef(atom$1);
117
+ atomRef.current = atom$1;
118
+ const selectorRef = useRef(selector);
119
+ const eqRef = useRef(eq);
120
+ selectorRef.current = selector;
121
+ eqRef.current = eq;
122
+ const handleRef = useRef(null);
123
+ const getOrCreateHandle = useCallback(() => {
124
+ if (!handleRef.current || handleRef.current.scope !== scope || handleRef.current.atom !== atom$1) handleRef.current = {
125
+ scope,
126
+ atom: atom$1,
127
+ handle: scope.select(atom$1, selectorRef.current, { eq: eqRef.current })
128
+ };
129
+ return handleRef.current.handle;
130
+ }, [scope, atom$1]);
131
+ const getSnapshot = useCallback(() => {
132
+ const state = ctrl.state;
133
+ if (state === "idle" || state === "resolving") throw getOrCreatePendingPromise(atomRef.current, ctrl);
134
+ if (state === "failed") throw ctrl.get();
135
+ return getOrCreateHandle().get();
136
+ }, [ctrl, getOrCreateHandle]);
137
+ return useSyncExternalStore(useCallback((onStoreChange) => {
138
+ if (ctrl.state !== "resolved") return () => {};
139
+ return getOrCreateHandle().subscribe(onStoreChange);
140
+ }, [ctrl, getOrCreateHandle]), getSnapshot, getSnapshot);
141
+ }
142
+
143
+ //#endregion
144
+ export { ScopeContext, ScopeProvider, atom, createScope, flow, preset, useAtom, useController, useScope, useSelect };
145
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":["atom"],"sources":["../src/context.tsx","../src/hooks.ts"],"sourcesContent":["import { createContext, type ReactNode } from 'react'\nimport { type Lite } from '@pumped-fn/lite'\n\n/**\n * React context for Lite.Scope.\n */\nconst ScopeContext = createContext<Lite.Scope | null>(null)\n\ninterface ScopeProviderProps {\n scope: Lite.Scope\n children: ReactNode\n}\n\n/**\n * Provider component for Lite.Scope.\n *\n * @example\n * ```tsx\n * <ScopeProvider scope={scope}>\n * <App />\n * </ScopeProvider>\n * ```\n */\nfunction ScopeProvider({ scope, children }: ScopeProviderProps) {\n return (\n <ScopeContext.Provider value={scope}>\n {children}\n </ScopeContext.Provider>\n )\n}\n\nexport { ScopeContext, ScopeProvider }\nexport type { ScopeProviderProps }\n","import { useCallback, useContext, useMemo, useRef, useSyncExternalStore } from 'react'\nimport { type Lite } from '@pumped-fn/lite'\nimport { ScopeContext } from './context'\n\nconst pendingPromises = new WeakMap<Lite.Atom<unknown>, Promise<unknown>>()\n\nfunction getOrCreatePendingPromise<T>(atom: Lite.Atom<T>, ctrl: Lite.Controller<T>): Promise<T> {\n let pending = pendingPromises.get(atom) as Promise<T> | undefined\n if (!pending) {\n pending = ctrl.resolve()\n pendingPromises.set(atom, pending)\n pending.finally(() => pendingPromises.delete(atom))\n }\n return pending\n}\n\n/**\n * Access the current Lite.Scope from context.\n *\n * @returns The current Lite.Scope instance from context\n * @throws When called outside of a ScopeProvider\n *\n * @example\n * ```tsx\n * const scope = useScope()\n * await scope.resolve(myAtom)\n * ```\n */\nfunction useScope(): Lite.Scope {\n const scope = useContext(ScopeContext)\n if (!scope) {\n throw new Error(\"useScope must be used within a ScopeProvider\")\n }\n return scope\n}\n\n/**\n * Get a memoized controller for an atom.\n *\n * @param atom - The atom to create a controller for\n * @returns A memoized Lite.Controller instance\n *\n * @example\n * ```tsx\n * const ctrl = useController(counterAtom)\n * ctrl.set(ctrl.get() + 1)\n * ```\n */\nfunction useController<T>(atom: Lite.Atom<T>): Lite.Controller<T> {\n const scope = useScope()\n return useMemo(() => scope.controller(atom), [scope, atom])\n}\n\n/**\n * Subscribe to atom value with Suspense/ErrorBoundary integration.\n * Auto-resolves atoms lazily and throws cached Promise for Suspense.\n *\n * @param atom - The atom to read\n * @returns The current value of the atom\n *\n * @example\n * ```tsx\n * function UserProfile() {\n * const user = useAtom(userAtom)\n * return <div>{user.name}</div>\n * }\n * ```\n */\nfunction useAtom<T>(atom: Lite.Atom<T>): T {\n const ctrl = useController(atom)\n const atomRef = useRef(atom)\n atomRef.current = atom\n\n const getSnapshot = useCallback((): T => {\n const state = ctrl.state\n if (state === 'idle' || state === 'resolving') {\n throw getOrCreatePendingPromise(atomRef.current, ctrl)\n }\n if (state === 'failed') {\n throw ctrl.get()\n }\n return ctrl.get()\n }, [ctrl])\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => ctrl.on('resolved', onStoreChange),\n [ctrl]\n )\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\n/**\n * Select a derived value from an atom with fine-grained reactivity.\n * Only re-renders when the selected value changes per equality function.\n *\n * @param atom - The atom to select from\n * @param selector - Function to extract a derived value\n * @param eq - Optional equality function\n * @returns The selected value\n *\n * @example\n * ```tsx\n * const name = useSelect(userAtom, user => user.name)\n * ```\n */\nfunction useSelect<T, S>(\n atom: Lite.Atom<T>,\n selector: (value: T) => S,\n eq?: (a: S, b: S) => boolean\n): S {\n const scope = useScope()\n const ctrl = useController(atom)\n\n const atomRef = useRef(atom)\n atomRef.current = atom\n\n const selectorRef = useRef(selector)\n const eqRef = useRef(eq)\n selectorRef.current = selector\n eqRef.current = eq\n\n const handleRef = useRef<{\n scope: Lite.Scope\n atom: Lite.Atom<T>\n handle: Lite.SelectHandle<S>\n } | null>(null)\n\n const getOrCreateHandle = useCallback(() => {\n if (\n !handleRef.current ||\n handleRef.current.scope !== scope ||\n handleRef.current.atom !== atom\n ) {\n const handle = scope.select(atom, selectorRef.current, { eq: eqRef.current })\n handleRef.current = { scope, atom, handle }\n }\n return handleRef.current.handle\n }, [scope, atom])\n\n const getSnapshot = useCallback((): S => {\n const state = ctrl.state\n if (state === 'idle' || state === 'resolving') {\n throw getOrCreatePendingPromise(atomRef.current, ctrl)\n }\n if (state === 'failed') {\n throw ctrl.get()\n }\n return getOrCreateHandle().get()\n }, [ctrl, getOrCreateHandle])\n\n const subscribe = useCallback((onStoreChange: () => void) => {\n if (ctrl.state !== 'resolved') {\n return () => {}\n }\n return getOrCreateHandle().subscribe(onStoreChange)\n }, [ctrl, getOrCreateHandle])\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n}\n\nexport { useScope, useController, useAtom, useSelect }\n"],"mappings":";;;;;;;;AAMA,MAAM,eAAe,cAAiC,KAAK;;;;;;;;;;;AAiB3D,SAAS,cAAc,EAAE,OAAO,YAAgC;AAC9D,QACE,oBAAC,aAAa;EAAS,OAAO;EAC3B;GACqB;;;;;ACvB5B,MAAM,kCAAkB,IAAI,SAA+C;AAE3E,SAAS,0BAA6B,QAAoB,MAAsC;CAC9F,IAAI,UAAU,gBAAgB,IAAIA,OAAK;AACvC,KAAI,CAAC,SAAS;AACZ,YAAU,KAAK,SAAS;AACxB,kBAAgB,IAAIA,QAAM,QAAQ;AAClC,UAAQ,cAAc,gBAAgB,OAAOA,OAAK,CAAC;;AAErD,QAAO;;;;;;;;;;;;;;AAeT,SAAS,WAAuB;CAC9B,MAAM,QAAQ,WAAW,aAAa;AACtC,KAAI,CAAC,MACH,OAAM,IAAI,MAAM,+CAA+C;AAEjE,QAAO;;;;;;;;;;;;;;AAeT,SAAS,cAAiB,QAAwC;CAChE,MAAM,QAAQ,UAAU;AACxB,QAAO,cAAc,MAAM,WAAWA,OAAK,EAAE,CAAC,OAAOA,OAAK,CAAC;;;;;;;;;;;;;;;;;AAkB7D,SAAS,QAAW,QAAuB;CACzC,MAAM,OAAO,cAAcA,OAAK;CAChC,MAAM,UAAU,OAAOA,OAAK;AAC5B,SAAQ,UAAUA;CAElB,MAAM,cAAc,kBAAqB;EACvC,MAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,UAAU,UAAU,YAChC,OAAM,0BAA0B,QAAQ,SAAS,KAAK;AAExD,MAAI,UAAU,SACZ,OAAM,KAAK,KAAK;AAElB,SAAO,KAAK,KAAK;IAChB,CAAC,KAAK,CAAC;AAOV,QAAO,qBALW,aACf,kBAA8B,KAAK,GAAG,YAAY,cAAc,EACjE,CAAC,KAAK,CACP,EAEsC,aAAa,YAAY;;;;;;;;;;;;;;;;AAiBlE,SAAS,UACP,QACA,UACA,IACG;CACH,MAAM,QAAQ,UAAU;CACxB,MAAM,OAAO,cAAcA,OAAK;CAEhC,MAAM,UAAU,OAAOA,OAAK;AAC5B,SAAQ,UAAUA;CAElB,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,QAAQ,OAAO,GAAG;AACxB,aAAY,UAAU;AACtB,OAAM,UAAU;CAEhB,MAAM,YAAY,OAIR,KAAK;CAEf,MAAM,oBAAoB,kBAAkB;AAC1C,MACE,CAAC,UAAU,WACX,UAAU,QAAQ,UAAU,SAC5B,UAAU,QAAQ,SAASA,OAG3B,WAAU,UAAU;GAAE;GAAO;GAAM,QADpB,MAAM,OAAOA,QAAM,YAAY,SAAS,EAAE,IAAI,MAAM,SAAS,CAAC;GAClC;AAE7C,SAAO,UAAU,QAAQ;IACxB,CAAC,OAAOA,OAAK,CAAC;CAEjB,MAAM,cAAc,kBAAqB;EACvC,MAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,UAAU,UAAU,YAChC,OAAM,0BAA0B,QAAQ,SAAS,KAAK;AAExD,MAAI,UAAU,SACZ,OAAM,KAAK,KAAK;AAElB,SAAO,mBAAmB,CAAC,KAAK;IAC/B,CAAC,MAAM,kBAAkB,CAAC;AAS7B,QAAO,qBAPW,aAAa,kBAA8B;AAC3D,MAAI,KAAK,UAAU,WACjB,cAAa;AAEf,SAAO,mBAAmB,CAAC,UAAU,cAAc;IAClD,CAAC,MAAM,kBAAkB,CAAC,EAEU,aAAa,YAAY"}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@pumped-fn/lite-react",
3
+ "version": "1.0.0",
4
+ "description": "React integration for @pumped-fn/lite",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.cts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE",
25
+ "CHANGELOG.md"
26
+ ],
27
+ "peerDependencies": {
28
+ "@pumped-fn/lite": ">=1.4.0",
29
+ "react": "^18.0.0 || ^19.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "@testing-library/jest-dom": "^6.9.1",
33
+ "@testing-library/react": "^16.3.0",
34
+ "@types/react": "^18.3.18",
35
+ "jsdom": "^27.2.0",
36
+ "react": "^18.3.1",
37
+ "tsdown": "^0.16.5",
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^4.0.5",
40
+ "@pumped-fn/lite": "1.6.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "directory": "packages/lite-react",
51
+ "url": "git+https://github.com/pumped-fn/pumped-fn.git"
52
+ },
53
+ "keywords": [
54
+ "react",
55
+ "hooks",
56
+ "dependency-injection",
57
+ "di",
58
+ "lite",
59
+ "lite-react",
60
+ "reactivity",
61
+ "typescript"
62
+ ],
63
+ "license": "MIT",
64
+ "scripts": {
65
+ "build": "tsdown",
66
+ "typecheck": "tsc --noEmit",
67
+ "typecheck:full": "tsc --noEmit -p tsconfig.test.json",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest"
70
+ }
71
+ }