atomirx 0.0.1

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.
Files changed (121) hide show
  1. package/README.md +1666 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +1440 -0
  5. package/coverage/coverage-final.json +14 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +131 -0
  8. package/coverage/prettify.css +1 -0
  9. package/coverage/prettify.js +2 -0
  10. package/coverage/sort-arrow-sprite.png +0 -0
  11. package/coverage/sorter.js +210 -0
  12. package/coverage/src/core/atom.ts.html +889 -0
  13. package/coverage/src/core/batch.ts.html +223 -0
  14. package/coverage/src/core/define.ts.html +805 -0
  15. package/coverage/src/core/emitter.ts.html +919 -0
  16. package/coverage/src/core/equality.ts.html +631 -0
  17. package/coverage/src/core/hook.ts.html +460 -0
  18. package/coverage/src/core/index.html +281 -0
  19. package/coverage/src/core/isAtom.ts.html +100 -0
  20. package/coverage/src/core/isPromiseLike.ts.html +133 -0
  21. package/coverage/src/core/onCreateHook.ts.html +136 -0
  22. package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
  23. package/coverage/src/core/types.ts.html +523 -0
  24. package/coverage/src/core/withUse.ts.html +253 -0
  25. package/coverage/src/index.html +116 -0
  26. package/coverage/src/index.ts.html +106 -0
  27. package/dist/core/atom.d.ts +63 -0
  28. package/dist/core/atom.test.d.ts +1 -0
  29. package/dist/core/atomState.d.ts +104 -0
  30. package/dist/core/atomState.test.d.ts +1 -0
  31. package/dist/core/batch.d.ts +126 -0
  32. package/dist/core/batch.test.d.ts +1 -0
  33. package/dist/core/define.d.ts +173 -0
  34. package/dist/core/define.test.d.ts +1 -0
  35. package/dist/core/derived.d.ts +102 -0
  36. package/dist/core/derived.test.d.ts +1 -0
  37. package/dist/core/effect.d.ts +120 -0
  38. package/dist/core/effect.test.d.ts +1 -0
  39. package/dist/core/emitter.d.ts +237 -0
  40. package/dist/core/emitter.test.d.ts +1 -0
  41. package/dist/core/equality.d.ts +62 -0
  42. package/dist/core/equality.test.d.ts +1 -0
  43. package/dist/core/hook.d.ts +134 -0
  44. package/dist/core/hook.test.d.ts +1 -0
  45. package/dist/core/isAtom.d.ts +9 -0
  46. package/dist/core/isPromiseLike.d.ts +9 -0
  47. package/dist/core/isPromiseLike.test.d.ts +1 -0
  48. package/dist/core/onCreateHook.d.ts +79 -0
  49. package/dist/core/promiseCache.d.ts +134 -0
  50. package/dist/core/promiseCache.test.d.ts +1 -0
  51. package/dist/core/scheduleNotifyHook.d.ts +51 -0
  52. package/dist/core/select.d.ts +151 -0
  53. package/dist/core/selector.test.d.ts +1 -0
  54. package/dist/core/types.d.ts +279 -0
  55. package/dist/core/withUse.d.ts +38 -0
  56. package/dist/core/withUse.test.d.ts +1 -0
  57. package/dist/index-2ok7ilik.js +1217 -0
  58. package/dist/index-B_5SFzfl.cjs +1 -0
  59. package/dist/index.cjs +1 -0
  60. package/dist/index.d.ts +14 -0
  61. package/dist/index.js +20 -0
  62. package/dist/index.test.d.ts +1 -0
  63. package/dist/react/index.cjs +30 -0
  64. package/dist/react/index.d.ts +7 -0
  65. package/dist/react/index.js +823 -0
  66. package/dist/react/rx.d.ts +250 -0
  67. package/dist/react/rx.test.d.ts +1 -0
  68. package/dist/react/strictModeTest.d.ts +10 -0
  69. package/dist/react/useAction.d.ts +381 -0
  70. package/dist/react/useAction.test.d.ts +1 -0
  71. package/dist/react/useStable.d.ts +183 -0
  72. package/dist/react/useStable.test.d.ts +1 -0
  73. package/dist/react/useValue.d.ts +134 -0
  74. package/dist/react/useValue.test.d.ts +1 -0
  75. package/package.json +57 -0
  76. package/scripts/publish.js +198 -0
  77. package/src/core/atom.test.ts +369 -0
  78. package/src/core/atom.ts +189 -0
  79. package/src/core/atomState.test.ts +342 -0
  80. package/src/core/atomState.ts +256 -0
  81. package/src/core/batch.test.ts +257 -0
  82. package/src/core/batch.ts +172 -0
  83. package/src/core/define.test.ts +342 -0
  84. package/src/core/define.ts +243 -0
  85. package/src/core/derived.test.ts +381 -0
  86. package/src/core/derived.ts +339 -0
  87. package/src/core/effect.test.ts +196 -0
  88. package/src/core/effect.ts +184 -0
  89. package/src/core/emitter.test.ts +364 -0
  90. package/src/core/emitter.ts +392 -0
  91. package/src/core/equality.test.ts +392 -0
  92. package/src/core/equality.ts +182 -0
  93. package/src/core/hook.test.ts +227 -0
  94. package/src/core/hook.ts +177 -0
  95. package/src/core/isAtom.ts +27 -0
  96. package/src/core/isPromiseLike.test.ts +72 -0
  97. package/src/core/isPromiseLike.ts +16 -0
  98. package/src/core/onCreateHook.ts +92 -0
  99. package/src/core/promiseCache.test.ts +239 -0
  100. package/src/core/promiseCache.ts +279 -0
  101. package/src/core/scheduleNotifyHook.ts +53 -0
  102. package/src/core/select.ts +454 -0
  103. package/src/core/selector.test.ts +257 -0
  104. package/src/core/types.ts +311 -0
  105. package/src/core/withUse.test.ts +249 -0
  106. package/src/core/withUse.ts +56 -0
  107. package/src/index.test.ts +80 -0
  108. package/src/index.ts +51 -0
  109. package/src/react/index.ts +20 -0
  110. package/src/react/rx.test.tsx +416 -0
  111. package/src/react/rx.tsx +300 -0
  112. package/src/react/strictModeTest.tsx +71 -0
  113. package/src/react/useAction.test.ts +989 -0
  114. package/src/react/useAction.ts +605 -0
  115. package/src/react/useStable.test.ts +553 -0
  116. package/src/react/useStable.ts +288 -0
  117. package/src/react/useValue.test.ts +182 -0
  118. package/src/react/useValue.ts +261 -0
  119. package/tsconfig.json +9 -0
  120. package/v2.md +725 -0
  121. package/vite.config.ts +39 -0
@@ -0,0 +1,250 @@
1
+ import { Atom, Equality } from '../core/types';
2
+ import { ContextSelectorFn } from '../core/select';
3
+ /**
4
+ * Reactive inline component that renders atom values directly in JSX.
5
+ *
6
+ * `rx` is a convenience wrapper around `useValue` that returns a memoized
7
+ * React component instead of a value. This enables fine-grained reactivity
8
+ * without creating separate components for each reactive value.
9
+ *
10
+ * ## IMPORTANT: Selector Must Return Synchronous Value
11
+ *
12
+ * **The selector function MUST NOT be async or return a Promise.**
13
+ *
14
+ * ```tsx
15
+ * // ❌ WRONG - Don't use async function
16
+ * rx(async ({ get }) => {
17
+ * const data = await fetch('/api');
18
+ * return data.name;
19
+ * });
20
+ *
21
+ * // ❌ WRONG - Don't return a Promise
22
+ * rx(({ get }) => fetch('/api').then(r => r.json()));
23
+ *
24
+ * // ✅ CORRECT - Create async atom and read with get()
25
+ * const data$ = atom(fetch('/api').then(r => r.json()));
26
+ * rx(({ get }) => get(data$).name); // Suspends until resolved
27
+ * ```
28
+ *
29
+ * ## Why Use `rx`?
30
+ *
31
+ * Without `rx`, you need a separate component to subscribe to an atom:
32
+ * ```tsx
33
+ * function PostsList() {
34
+ * const posts = useValue(postsAtom);
35
+ * return posts.map((post) => <Post post={post} />);
36
+ * }
37
+ *
38
+ * function Page() {
39
+ * return (
40
+ * <Suspense fallback={<Loading />}>
41
+ * <PostsList />
42
+ * </Suspense>
43
+ * );
44
+ * }
45
+ * ```
46
+ *
47
+ * With `rx`, you can subscribe inline:
48
+ * ```tsx
49
+ * function Page() {
50
+ * return (
51
+ * <Suspense fallback={<Loading />}>
52
+ * {rx(({ get }) =>
53
+ * get(postsAtom).map((post) => <Post post={post} />)
54
+ * )}
55
+ * </Suspense>
56
+ * );
57
+ * }
58
+ * ```
59
+ *
60
+ * ## Key Benefits
61
+ *
62
+ * 1. **Fine-grained updates**: Only the `rx` component re-renders when the atom changes,
63
+ * not the parent component
64
+ * 2. **Less boilerplate**: No need to create single-purpose wrapper components
65
+ * 3. **Colocation**: Keep reactive logic inline where it's used
66
+ * 4. **Memoized**: Uses `React.memo` to prevent unnecessary re-renders
67
+ * 5. **Type-safe**: Full TypeScript support with proper type inference
68
+ *
69
+ * ## Async Atoms (Suspense-Style API)
70
+ *
71
+ * `rx` inherits the Suspense-style API from `useValue`:
72
+ * - **Loading state**: The getter throws a Promise (triggers Suspense)
73
+ * - **Error state**: The getter throws the error (triggers ErrorBoundary)
74
+ * - **Resolved state**: The getter returns the value
75
+ *
76
+ * For async atoms, you MUST wrap with `<Suspense>` and `<ErrorBoundary>`:
77
+ * ```tsx
78
+ * function App() {
79
+ * return (
80
+ * <ErrorBoundary fallback={<div>Error!</div>}>
81
+ * <Suspense fallback={<div>Loading...</div>}>
82
+ * {rx(({ get }) => get(userAtom).name)}
83
+ * </Suspense>
84
+ * </ErrorBoundary>
85
+ * );
86
+ * }
87
+ * ```
88
+ *
89
+ * Or catch errors in the selector to handle loading/error inline:
90
+ * ```tsx
91
+ * {rx(({ get }) => {
92
+ * try {
93
+ * return get(userAtom).name;
94
+ * } catch {
95
+ * return "Loading...";
96
+ * }
97
+ * })}
98
+ * ```
99
+ *
100
+ * @template T - The type of the selected/derived value
101
+ * @param selector - Context-based selector function with `{ get, all, any, race, settled }`.
102
+ * Must return sync value, not a Promise.
103
+ * @param equals - Equality function or shorthand ("strict", "shallow", "deep").
104
+ * Defaults to "shallow".
105
+ * @returns A React element that renders the selected value
106
+ * @throws Error if selector returns a Promise or PromiseLike
107
+ *
108
+ * @example Shorthand - render atom value directly
109
+ * ```tsx
110
+ * const count = atom(5);
111
+ *
112
+ * function Counter() {
113
+ * return <div>Count: {rx(count)}</div>;
114
+ * }
115
+ * ```
116
+ *
117
+ * @example Context selector - derive a value
118
+ * ```tsx
119
+ * const count = atom(5);
120
+ *
121
+ * function DoubledCounter() {
122
+ * return <div>Doubled: {rx(({ get }) => get(count) * 2)}</div>;
123
+ * }
124
+ * ```
125
+ *
126
+ * @example Multiple atoms
127
+ * ```tsx
128
+ * const firstName = atom("John");
129
+ * const lastName = atom("Doe");
130
+ *
131
+ * function FullName() {
132
+ * return (
133
+ * <div>
134
+ * {rx(({ get }) => `${get(firstName)} ${get(lastName)}`)}
135
+ * </div>
136
+ * );
137
+ * }
138
+ * ```
139
+ *
140
+ * @example Fine-grained updates - parent doesn't re-render
141
+ * ```tsx
142
+ * const count = atom(0);
143
+ *
144
+ * function Parent() {
145
+ * console.log("Parent renders once");
146
+ * return (
147
+ * <div>
148
+ * {rx(count)} {/* Only this re-renders when count changes *\/}
149
+ * <button onClick={() => count.set((n) => n + 1)}>+</button>
150
+ * </div>
151
+ * );
152
+ * }
153
+ * ```
154
+ *
155
+ * @example Multiple subscriptions in one component
156
+ * ```tsx
157
+ * function Dashboard() {
158
+ * return (
159
+ * <div>
160
+ * <header>
161
+ * <Suspense fallback="...">{rx(({ get }) => get(userAtom).name)}</Suspense>
162
+ * </header>
163
+ * <main>
164
+ * <Suspense fallback="...">
165
+ * {rx(({ get }) => get(postsAtom).length)} posts
166
+ * </Suspense>
167
+ * <Suspense fallback="...">
168
+ * {rx(({ get }) => get(notificationsAtom).length)} notifications
169
+ * </Suspense>
170
+ * </main>
171
+ * </div>
172
+ * );
173
+ * }
174
+ * ```
175
+ *
176
+ * @example Conditional dependencies - only subscribes to accessed atoms
177
+ * ```tsx
178
+ * const showDetails = atom(false);
179
+ * const summary = atom("Brief info");
180
+ * const details = atom("Detailed info");
181
+ *
182
+ * function Info() {
183
+ * return (
184
+ * <div>
185
+ * {rx(({ get }) =>
186
+ * get(showDetails) ? get(details) : get(summary)
187
+ * )}
188
+ * </div>
189
+ * );
190
+ * }
191
+ * ```
192
+ *
193
+ * @example With custom equality
194
+ * ```tsx
195
+ * const user = atom({ id: 1, name: "John" });
196
+ *
197
+ * function UserName() {
198
+ * return (
199
+ * <div>
200
+ * {rx(
201
+ * ({ get }) => get(user).name,
202
+ * (a, b) => a === b // Only re-render if name string changes
203
+ * )}
204
+ * </div>
205
+ * );
206
+ * }
207
+ * ```
208
+ *
209
+ * @example Combining multiple async atoms with async utilities
210
+ * ```tsx
211
+ * const userAtom = atom(fetchUser());
212
+ * const postsAtom = atom(fetchPosts());
213
+ *
214
+ * function Dashboard() {
215
+ * return (
216
+ * <Suspense fallback={<Loading />}>
217
+ * {rx(({ all }) => {
218
+ * // Use all() to wait for multiple atoms
219
+ * const [user, posts] = all([userAtom, postsAtom]);
220
+ * return <DashboardContent user={user} posts={posts} />;
221
+ * })}
222
+ * </Suspense>
223
+ * );
224
+ * }
225
+ * ```
226
+ *
227
+ * @example Using settled for partial failures
228
+ * ```tsx
229
+ * const userAtom = atom(fetchUser());
230
+ * const postsAtom = atom(fetchPosts());
231
+ *
232
+ * function Dashboard() {
233
+ * return (
234
+ * <Suspense fallback={<Loading />}>
235
+ * {rx(({ settled }) => {
236
+ * const [userResult, postsResult] = settled([userAtom, postsAtom]);
237
+ * return (
238
+ * <DashboardContent
239
+ * user={userResult.status === 'resolved' ? userResult.value : null}
240
+ * posts={postsResult.status === 'resolved' ? postsResult.value : []}
241
+ * />
242
+ * );
243
+ * })}
244
+ * </Suspense>
245
+ * );
246
+ * }
247
+ * ```
248
+ */
249
+ export declare function rx<T>(atom: Atom<T>, equals?: Equality<Awaited<T>>): Awaited<T>;
250
+ export declare function rx<T>(selector: ContextSelectorFn<T>, equals?: Equality<T>): T;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { default as React } from 'react';
2
+ import { render, renderHook, RenderHookOptions } from '@testing-library/react';
3
+ export declare const wrappers: {
4
+ mode: "normal" | "strict";
5
+ Wrapper: React.FC<{
6
+ children: React.ReactNode;
7
+ }>;
8
+ render: (ui: React.ReactElement) => ReturnType<typeof render>;
9
+ renderHook: <TResult, TProps>(render: (props: TProps) => TResult, options?: RenderHookOptions<TProps>) => ReturnType<typeof renderHook<TResult, TProps>>;
10
+ }[];
@@ -0,0 +1,381 @@
1
+ /**
2
+ * State for an action that hasn't been dispatched yet.
3
+ */
4
+ export type ActionIdleState = {
5
+ readonly status: "idle";
6
+ readonly result: undefined;
7
+ readonly error: undefined;
8
+ };
9
+ /**
10
+ * State for an action that is currently executing.
11
+ */
12
+ export type ActionLoadingState = {
13
+ readonly status: "loading";
14
+ readonly result: undefined;
15
+ readonly error: undefined;
16
+ };
17
+ /**
18
+ * State for an action that completed successfully.
19
+ */
20
+ export type ActionSuccessState<T> = {
21
+ readonly status: "success";
22
+ readonly result: T;
23
+ readonly error: undefined;
24
+ };
25
+ /**
26
+ * State for an action that failed with an error.
27
+ */
28
+ export type ActionErrorState = {
29
+ readonly status: "error";
30
+ readonly result: undefined;
31
+ readonly error: unknown;
32
+ };
33
+ /**
34
+ * Union of all possible action states.
35
+ */
36
+ export type ActionState<T> = ActionIdleState | ActionLoadingState | ActionSuccessState<T> | ActionErrorState;
37
+ /**
38
+ * Action state without idle (used when lazy is false).
39
+ */
40
+ export type ActionStateWithoutIdle<T> = Exclude<ActionState<T>, ActionIdleState>;
41
+ /**
42
+ * A promise with an abort method for manual cancellation.
43
+ */
44
+ export type AbortablePromise<T> = PromiseLike<T> & {
45
+ abort: () => void;
46
+ };
47
+ /**
48
+ * Options for useAction hook.
49
+ */
50
+ export interface UseActionOptions {
51
+ /**
52
+ * If true, only one request runs at a time - previous requests are aborted.
53
+ * Also aborts on unmount and reset().
54
+ * - `exclusive: true` (default) - Aborts previous request on re-call, unmount, deps change, reset()
55
+ * - `exclusive: false` - Allows concurrent requests, manual abort only via abort() or promise.abort()
56
+ * @default true
57
+ */
58
+ exclusive?: boolean;
59
+ /**
60
+ * Dependencies array. When lazy is false, re-executes when deps change.
61
+ * - Regular values: compared by reference (like useEffect deps)
62
+ * - Atoms: automatically tracked via useValue, re-executes when atom values change
63
+ * @default []
64
+ */
65
+ deps?: unknown[];
66
+ }
67
+ /**
68
+ * Context passed to the action function.
69
+ */
70
+ export interface ActionContext {
71
+ /** AbortSignal for cancellation. New signal per dispatch. */
72
+ signal: AbortSignal;
73
+ }
74
+ /**
75
+ * API methods for controlling the action.
76
+ */
77
+ export type ActionApi = {
78
+ /** Abort the current in-flight request. */
79
+ abort: () => void;
80
+ /** Reset state back to idle. Respects exclusive setting. */
81
+ reset: () => void;
82
+ };
83
+ /**
84
+ * Dispatch function type - callable and returns AbortablePromise.
85
+ */
86
+ export type ActionDispatch<T> = () => AbortablePromise<T extends PromiseLike<infer U> ? U : T>;
87
+ /**
88
+ * Return type for useAction - a callable dispatch function with state and API attached.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const fetchPosts = useAction(async () => api.getPosts());
93
+ *
94
+ * // Call it like a function
95
+ * const posts = await fetchPosts();
96
+ *
97
+ * // Access state via properties
98
+ * fetchPosts.loading // boolean
99
+ * fetchPosts.status // "idle" | "loading" | "success" | "error"
100
+ * fetchPosts.result // Post[] | undefined
101
+ * fetchPosts.error // unknown
102
+ * fetchPosts.abort() // cancel current request
103
+ * fetchPosts.reset() // reset state to idle
104
+ * ```
105
+ */
106
+ export type Action<TResult, TLazy extends boolean = true> = ActionDispatch<TResult> & (TLazy extends true ? ActionState<Awaited<TResult>> : ActionStateWithoutIdle<Awaited<TResult>>) & ActionApi;
107
+ /**
108
+ * React hook for handling async actions with loading/error states and abort support.
109
+ *
110
+ * `useAction` provides a complete solution for managing async operations in React,
111
+ * with automatic state tracking, cancellation support, and race condition handling.
112
+ *
113
+ * Returns a callable function with state properties attached, making it easy to
114
+ * manage multiple actions in a single component:
115
+ *
116
+ * ```tsx
117
+ * const fetchUser = useAction(() => api.getUser(id));
118
+ * const updateUser = useAction(() => api.updateUser(id, data));
119
+ * const deleteUser = useAction(() => api.deleteUser(id));
120
+ *
121
+ * // Call directly - no need to destructure
122
+ * await fetchUser();
123
+ *
124
+ * // Access state via properties
125
+ * fetchUser.loading // boolean
126
+ * fetchUser.result // User | undefined
127
+ * fetchUser.error // unknown
128
+ * fetchUser.status // "idle" | "loading" | "success" | "error"
129
+ * fetchUser.abort() // cancel request
130
+ * fetchUser.reset() // reset to idle state
131
+ * ```
132
+ *
133
+ * ## Key Features
134
+ *
135
+ * 1. **Automatic state management**: Tracks idle → loading → success/error transitions
136
+ * 2. **AbortSignal support**: Built-in cancellation via AbortController
137
+ * 3. **Exclusive mode**: Only one request at a time - previous aborted automatically (configurable)
138
+ * 4. **Lazy/eager execution**: Wait for manual call or execute on mount (configurable)
139
+ * 5. **Stale closure prevention**: Ignores outdated results from cancelled requests
140
+ * 6. **Atom deps support**: Atoms in deps array are reactively tracked
141
+ *
142
+ * ## State Machine
143
+ *
144
+ * ```
145
+ * ┌──────┐ dispatch() ┌─────────┐ success ┌─────────┐
146
+ * │ idle │ ───────────► │ loading │ ────────► │ success │
147
+ * └──────┘ └─────────┘ └─────────┘
148
+ * │
149
+ * │ error
150
+ * ▼
151
+ * ┌─────────┐
152
+ * │ error │
153
+ * └─────────┘
154
+ * ```
155
+ *
156
+ * ## Exclusive Mode (exclusive option)
157
+ *
158
+ * The `exclusive` option controls whether only one request can run at a time:
159
+ *
160
+ * | Trigger | exclusive: true (default) | exclusive: false |
161
+ * |---------|---------------------------|------------------|
162
+ * | Call action again | ✅ Aborts previous | ❌ No abort |
163
+ * | Component unmounts | ✅ Aborts current | ❌ No abort |
164
+ * | Deps change (lazy: false) | ✅ Aborts previous | ❌ No abort |
165
+ * | `reset()` called | ✅ Aborts current | ❌ No abort |
166
+ * | `abort()` called | ✅ Always aborts | ✅ Always aborts |
167
+ * | `promise.abort()` called | ✅ Always aborts | ✅ Always aborts |
168
+ *
169
+ * ## Reset Behavior
170
+ *
171
+ * `reset()` clears the state back to idle and respects `exclusive`:
172
+ * - **exclusive: true**: Aborts any in-flight request, then resets to idle
173
+ * - **exclusive: false**: Only resets state (request continues in background)
174
+ *
175
+ * ## Race Condition Handling
176
+ *
177
+ * When a new dispatch starts before the previous completes:
178
+ * - Previous request's result is ignored (even if it resolves)
179
+ * - Only the latest request's result updates state
180
+ * - This prevents stale data from overwriting fresh data
181
+ *
182
+ * @template TResult - The return type of the action function
183
+ * @param fn - Action function receiving `{ signal: AbortSignal }`. Can be sync or async.
184
+ * @param options - Configuration options
185
+ * @param options.lazy - If true (default), waits for manual call. If false, executes on mount.
186
+ * @param options.exclusive - If true (default), aborts previous request on re-call/unmount.
187
+ * @param options.deps - Dependencies for lazy: false mode. Atoms are reactively tracked.
188
+ * @returns A callable dispatch function with state and API properties attached
189
+ *
190
+ * @example Basic usage - manual dispatch
191
+ * ```tsx
192
+ * function UserProfile({ userId }) {
193
+ * const fetchUser = useAction(async ({ signal }) => {
194
+ * const response = await fetch(`/api/users/${userId}`, { signal });
195
+ * if (!response.ok) throw new Error('Failed to fetch');
196
+ * return response.json();
197
+ * });
198
+ *
199
+ * return (
200
+ * <div>
201
+ * {fetchUser.status === 'idle' && <button onClick={fetchUser}>Load User</button>}
202
+ * {fetchUser.status === 'loading' && <Spinner />}
203
+ * {fetchUser.status === 'success' && <div>{fetchUser.result.name}</div>}
204
+ * {fetchUser.status === 'error' && <div>Error: {fetchUser.error.message}</div>}
205
+ * </div>
206
+ * );
207
+ * }
208
+ * ```
209
+ *
210
+ * @example Eager execution on mount and deps change
211
+ * ```tsx
212
+ * function UserProfile({ userId }) {
213
+ * const fetchUser = useAction(
214
+ * async ({ signal }) => {
215
+ * const response = await fetch(`/api/users/${userId}`, { signal });
216
+ * return response.json();
217
+ * },
218
+ * { lazy: false, deps: [userId] }
219
+ * );
220
+ * // Fetches automatically on mount
221
+ * // Re-fetches when userId changes
222
+ * // Previous request is aborted when userId changes
223
+ * }
224
+ * ```
225
+ *
226
+ * @example Eager execution with atom deps
227
+ * ```tsx
228
+ * const userIdAtom = atom(1);
229
+ *
230
+ * function UserProfile() {
231
+ * const fetchUser = useAction(
232
+ * async ({ signal }) => fetchUserApi(userIdAtom.value, { signal }),
233
+ * { lazy: false, deps: [userIdAtom] }
234
+ * );
235
+ * // Automatically re-fetches when userIdAtom changes
236
+ * // Atoms in deps are tracked reactively via useValue
237
+ * }
238
+ * ```
239
+ *
240
+ * @example Allow concurrent requests (non-exclusive)
241
+ * ```tsx
242
+ * function SearchResults() {
243
+ * const search = useAction(
244
+ * async ({ signal }) => searchAPI(query, { signal }),
245
+ * { exclusive: false }
246
+ * );
247
+ *
248
+ * return (
249
+ * <div>
250
+ * <button onClick={search}>Search</button>
251
+ * <button onClick={search.abort} disabled={search.status !== 'loading'}>
252
+ * Cancel
253
+ * </button>
254
+ * </div>
255
+ * );
256
+ * }
257
+ * ```
258
+ *
259
+ * @example Abort via returned promise
260
+ * ```tsx
261
+ * const longTask = useAction(async ({ signal }) => longRunningTask({ signal }));
262
+ *
263
+ * const handleClick = () => {
264
+ * const promise = longTask();
265
+ *
266
+ * // Abort after 5 seconds
267
+ * setTimeout(() => promise.abort(), 5000);
268
+ *
269
+ * // Or await the result
270
+ * try {
271
+ * const result = await promise;
272
+ * } catch (error) {
273
+ * if (error.name === 'AbortError') {
274
+ * console.log('Request was cancelled');
275
+ * }
276
+ * }
277
+ * };
278
+ * ```
279
+ *
280
+ * @example Chaining multiple actions
281
+ * ```tsx
282
+ * function CreateUserForm() {
283
+ * const createUser = useAction(({ signal }) => api.createUser(data, { signal }));
284
+ * const sendWelcomeEmail = useAction(({ signal }) => api.sendEmail(email, { signal }));
285
+ *
286
+ * const handleSubmit = async () => {
287
+ * try {
288
+ * const user = await createUser();
289
+ * await sendWelcomeEmail();
290
+ * toast.success('User created and email sent!');
291
+ * } catch (error) {
292
+ * toast.error('Operation failed');
293
+ * }
294
+ * };
295
+ *
296
+ * const isLoading = createUser.status === 'loading' || sendWelcomeEmail.status === 'loading';
297
+ *
298
+ * return <button onClick={handleSubmit} disabled={isLoading}>Create User</button>;
299
+ * }
300
+ * ```
301
+ *
302
+ * @example Sync action (non-async function)
303
+ * ```tsx
304
+ * const compute = useAction(({ signal }) => {
305
+ * // Sync computation - still works!
306
+ * return computeExpensiveValue(data);
307
+ * });
308
+ * // compute() returns a promise that resolves immediately
309
+ * ```
310
+ *
311
+ * @example Form submission with validation
312
+ * ```tsx
313
+ * function ContactForm() {
314
+ * const [formData, setFormData] = useState({ name: '', email: '' });
315
+ *
316
+ * const submit = useAction(async ({ signal }) => {
317
+ * // Validate
318
+ * if (!formData.name) throw new Error('Name required');
319
+ *
320
+ * // Submit
321
+ * const response = await fetch('/api/contact', {
322
+ * method: 'POST',
323
+ * body: JSON.stringify(formData),
324
+ * signal,
325
+ * });
326
+ *
327
+ * if (!response.ok) throw new Error('Submission failed');
328
+ * return response.json();
329
+ * });
330
+ *
331
+ * return (
332
+ * <form onSubmit={(e) => { e.preventDefault(); submit(); }}>
333
+ * <input value={formData.name} onChange={...} />
334
+ * <input value={formData.email} onChange={...} />
335
+ * <button disabled={submit.status === 'loading'}>
336
+ * {submit.status === 'loading' ? 'Submitting...' : 'Submit'}
337
+ * </button>
338
+ * {submit.status === 'error' && <p className="error">{submit.error.message}</p>}
339
+ * {submit.status === 'success' && <p className="success">Submitted!</p>}
340
+ * </form>
341
+ * );
342
+ * }
343
+ * ```
344
+ *
345
+ * @example Reset after success or error
346
+ * ```tsx
347
+ * function SubmitForm() {
348
+ * const submit = useAction(async () => api.submit(data));
349
+ *
350
+ * if (submit.status === 'success') {
351
+ * return (
352
+ * <div>
353
+ * <p>Success!</p>
354
+ * <button onClick={submit.reset}>Submit Another</button>
355
+ * </div>
356
+ * );
357
+ * }
358
+ *
359
+ * if (submit.status === 'error') {
360
+ * return (
361
+ * <div>
362
+ * <p>Error: {submit.error.message}</p>
363
+ * <button onClick={submit.reset}>Dismiss</button>
364
+ * <button onClick={submit}>Retry</button>
365
+ * </div>
366
+ * );
367
+ * }
368
+ *
369
+ * return <button onClick={submit} disabled={submit.status === 'loading'}>Submit</button>;
370
+ * }
371
+ * ```
372
+ */
373
+ export declare function useAction<TResult, TLazy extends boolean = true>(fn: (context: ActionContext) => TResult, options?: UseActionOptions & {
374
+ /**
375
+ * If true, waits for manual call to execute. If false, executes on mount and when deps change.
376
+ * - `lazy: true` (default) - Action starts in "idle" state, waits for you to call it
377
+ * - `lazy: false` - Action executes immediately on mount and re-executes when deps change
378
+ * @default true
379
+ */
380
+ lazy?: TLazy;
381
+ }): Action<TResult, TLazy>;
@@ -0,0 +1 @@
1
+ export {};