react-action-z 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 delpikye-v
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,490 @@
1
+ # โšก react-action-z
2
+
3
+ [![NPM](https://img.shields.io/npm/v/react-action-z.svg)](https://www.npmjs.com/package/react-action-z) ![Downloads](https://img.shields.io/npm/dt/react-action-z.svg)
4
+
5
+ <a href="https://codesandbox.io/p/sandbox/hgk4hx" target="_blank">LIVE EXAMPLE</a>
6
+
7
+ Unified async action layer for React.
8
+
9
+ > Run async functions as **actions**, not boilerplate.
10
+ > Actions run only when you call run() โ€” never automatically.
11
+
12
+ ---
13
+
14
+ # Why react-action-z?
15
+
16
+ - โšก Unified async actions
17
+ - ๐Ÿง  Built-in loading / error state
18
+ - ๐Ÿ” Retry support
19
+ - ๐Ÿงฉ Middleware system
20
+ - ๐ŸŒ Global action state
21
+ - ๐Ÿงพ Full TypeScript support
22
+
23
+ Traditional async UI code:
24
+
25
+ ```ts
26
+ const [loading, setLoading] = useState(false)
27
+ const [error, setError] = useState(null)
28
+
29
+ async function login(data) {
30
+ try {
31
+ setLoading(true)
32
+ const result = await api.login(data)
33
+ } catch (err) {
34
+ setError(err)
35
+ } finally {
36
+ setLoading(false)
37
+ }
38
+ }
39
+ ```
40
+
41
+ React Action:
42
+
43
+ ```ts
44
+ const login = useAction(api.login)
45
+
46
+ await login.run(data)
47
+ ```
48
+
49
+ UI state automatically available:
50
+
51
+ ```ts
52
+ login.loading
53
+ login.data
54
+ login.error
55
+ ```
56
+
57
+ ---
58
+
59
+ # Installation
60
+
61
+ ```bash
62
+ npm install react-action-z
63
+ ```
64
+
65
+ ---
66
+
67
+ # Basic Usage
68
+
69
+ ```ts
70
+ import { useAction } from "react-action-z"
71
+
72
+ const login = useAction(api.login)
73
+
74
+ await login.run({
75
+ email,
76
+ password
77
+ })
78
+ ```
79
+
80
+ State is automatically managed:
81
+
82
+ ```ts
83
+ login.loading
84
+ login.data
85
+ login.error
86
+ ```
87
+
88
+ UI example:
89
+
90
+ ```tsx
91
+ <button disabled={login.loading}>
92
+ {login.loading ? "Loading..." : "Login"}
93
+ </button>
94
+ ```
95
+
96
+ ---
97
+
98
+ # Example with JSONPlaceholder
99
+
100
+ Example using the free API from JSONPlaceholder.
101
+
102
+ Fetch a user from:
103
+
104
+ ```
105
+ https://jsonplaceholder.typicode.com/users/1
106
+ ```
107
+
108
+ ---
109
+
110
+ ## API
111
+
112
+ ```ts
113
+ async function fetchUser(id: number) {
114
+ const res = await fetch(
115
+ `https://jsonplaceholder.typicode.com/users/${id}`
116
+ )
117
+
118
+ if (!res.ok) {
119
+ throw new Error("Failed to fetch user")
120
+ }
121
+
122
+ return res.json()
123
+ }
124
+ ```
125
+
126
+ ---
127
+
128
+ ## React Component
129
+
130
+ ```tsx
131
+ import React from "react"
132
+ import { useAction } from "react-action-z"
133
+
134
+ async function fetchUser(id: number) {
135
+ const res = await fetch(
136
+ `https://jsonplaceholder.typicode.com/users/${id}`
137
+ )
138
+
139
+ if (!res.ok) {
140
+ throw new Error("Failed to fetch user")
141
+ }
142
+
143
+ return res.json()
144
+ }
145
+
146
+ export default function UserExample() {
147
+
148
+ const getUser = useAction(fetchUser)
149
+
150
+ return (
151
+ <div>
152
+
153
+ <button
154
+ onClick={() => getUser.run(1)}
155
+ disabled={getUser.loading}
156
+ >
157
+ {getUser.loading ? "Loading..." : "Load User"}
158
+ </button>
159
+
160
+ {getUser.error && (
161
+ <p>Error loading user</p>
162
+ )}
163
+
164
+ {getUser.data && (
165
+ <div style={{ marginTop: 20 }}>
166
+ <h3>{getUser.data.name}</h3>
167
+ <p>{getUser.data.email}</p>
168
+ <p>{getUser.data.phone}</p>
169
+ </div>
170
+ )}
171
+
172
+ </div>
173
+ )
174
+ }
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Result
180
+
181
+ Click the button:
182
+
183
+ ```
184
+ Load User
185
+ ```
186
+
187
+ The action automatically manages:
188
+
189
+ ```
190
+ loading
191
+ data
192
+ error
193
+ ```
194
+
195
+ Example returned data:
196
+
197
+ ```json
198
+ {
199
+ "id": 1,
200
+ "name": "Leanne Graham",
201
+ "username": "Bret",
202
+ "email": "Sincere@april.biz",
203
+ "phone": "1-770-736-8031"
204
+ }
205
+ ```
206
+
207
+ ---
208
+
209
+ # Action API
210
+
211
+ ```ts
212
+ const action = useAction(fn, options)
213
+ ```
214
+
215
+ Return value:
216
+
217
+ ```ts
218
+ action.run(...)
219
+ action.loading
220
+ action.data
221
+ action.error
222
+ action.reset()
223
+ ```
224
+
225
+ ---
226
+
227
+ # Options
228
+
229
+ ```ts
230
+ useAction(api.login, {
231
+ retry: 2,
232
+ strategy: "replace",
233
+ key: "login",
234
+ optimistic: (data) => {},
235
+ onSuccess: (data) => {},
236
+ onError: (err) => {}
237
+ })
238
+ ```
239
+
240
+ | option | description |
241
+ |-------------|---------------------|
242
+ | retry | retry on failure |
243
+ | strategy | concurrency control |
244
+ | key | global action key |
245
+ | optimistic | optimistic update |
246
+ | onSuccess | success callback |
247
+ | onError | error callback |
248
+
249
+ ---
250
+
251
+ # Concurrency Strategy
252
+
253
+ ```ts
254
+ useAction(api.save, {
255
+ strategy: "ignore"
256
+ })
257
+ ```
258
+
259
+ | strategy | behavior |
260
+ |----------|-----------------------------------|
261
+ | replace | only latest result updates state |
262
+ | ignore | ignore if running |
263
+ | parallel | allow multiple runs |
264
+
265
+ Example:
266
+
267
+ ```ts
268
+ const save = useAction(api.save, {
269
+ strategy: "ignore"
270
+ })
271
+ ```
272
+
273
+ Prevent double submit.
274
+
275
+ ---
276
+
277
+ # Retry
278
+
279
+ Automatically retry failed requests.
280
+
281
+ ```ts
282
+ const fetchUser = useAction(api.fetchUser, {
283
+ retry: 2
284
+ })
285
+ ```
286
+
287
+ Behavior:
288
+
289
+ ```text
290
+ attempt 1
291
+ attempt 2
292
+ attempt 3
293
+ ```
294
+
295
+ ---
296
+
297
+ # Optimistic Update
298
+
299
+ Apply UI update before request finishes.
300
+
301
+ ```ts
302
+ const updateUser = useAction(api.updateUser, {
303
+ optimistic: (data) => {
304
+ setUser(data)
305
+ }
306
+ })
307
+ ```
308
+
309
+ This improves UI responsiveness.
310
+
311
+ ---
312
+
313
+ # Global Action State
314
+
315
+ Actions can share state across components.
316
+
317
+ ```ts
318
+ import { useAction } from "react-action-z"
319
+
320
+ const login = useAction(api.login, {
321
+ key: "login"
322
+ })
323
+
324
+ export function LoginButton() {
325
+ return (
326
+ <button
327
+ disabled={login.loading}
328
+ onClick={() => login.run({ email, password })}
329
+ >
330
+ {login.loading ? "Logging in..." : "Login"}
331
+ </button>
332
+ )
333
+ }
334
+ ```
335
+
336
+ Access anywhere:
337
+
338
+ ```ts
339
+ // GlobalSpinner.tsx
340
+
341
+ import { useGlobalAction } from "react-action-z"
342
+
343
+ export function GlobalSpinner() {
344
+
345
+ const login = useGlobalAction("login")
346
+
347
+ if (!login.loading) return null
348
+
349
+ return <div>Authenticating...</div>
350
+ }
351
+ ```
352
+
353
+ Result:
354
+ - LoginButton triggers action
355
+ - GlobalSpinner reacts automatically
356
+
357
+ ---
358
+
359
+ # Middleware
360
+
361
+ React Action supports middleware similar to Redux.
362
+
363
+ ```ts
364
+ import { createActionClient } from "react-action-z"
365
+
366
+ createActionClient({
367
+ middleware: [
368
+ async (context, next) => {
369
+ console.log("action:", context.key)
370
+ return next()
371
+ }
372
+ ]
373
+ })
374
+ ```
375
+
376
+ Middleware context:
377
+
378
+ ```ts
379
+ {
380
+ key?: string
381
+ args: any[]
382
+ }
383
+ ```
384
+
385
+ Use cases:
386
+
387
+ - logging
388
+ - analytics
389
+ - metrics
390
+ - debugging
391
+
392
+ ---
393
+
394
+ # Reset State
395
+
396
+ Reset action state manually.
397
+
398
+ ```ts
399
+ login.reset()
400
+ ```
401
+
402
+ Result:
403
+
404
+ ```ts
405
+ {
406
+ loading: false
407
+ data: null
408
+ error: null
409
+ }
410
+ ```
411
+
412
+ ---
413
+
414
+ # Example
415
+
416
+ ```ts
417
+ const createPost = useAction(api.createPost, {
418
+ retry: 1
419
+ })
420
+
421
+ async function handleSubmit(data) {
422
+ await createPost.run(data)
423
+ }
424
+ ```
425
+
426
+ UI:
427
+
428
+ ```tsx
429
+ <button disabled={createPost.loading}>
430
+ Publish
431
+ </button>
432
+
433
+ {createPost.error && <p>Error</p>}
434
+ ```
435
+
436
+ ---
437
+
438
+ # Comparison
439
+
440
+ | Criteria | react-action-z | React state |
441
+ |---------------------|----------------|-------------|
442
+ | Async state | โœ… | manual |
443
+ | Retry | โœ… | manual |
444
+ | Global action state | โœ… | โŒ |
445
+ | Middleware | โœ… | โŒ |
446
+ | Optimistic update | โœ… | manual |
447
+ | Boilerplate | minimal | high |
448
+
449
+ ---
450
+
451
+ # Architecture
452
+
453
+ ```text
454
+ Component
455
+ โ†“
456
+ useAction()
457
+ โ†“
458
+ Action Runner
459
+ โ†“
460
+ Middleware
461
+ โ†“
462
+ Async Function
463
+ โ†“
464
+ State Update
465
+ ```
466
+
467
+ ---
468
+
469
+ # Philosophy
470
+
471
+ React Action treats async functions as **first-class UI actions**.
472
+
473
+ Instead of manually managing:
474
+
475
+ - loading
476
+ - error
477
+ - retries
478
+ - concurrency
479
+
480
+ You simply run actions:
481
+
482
+ ```ts
483
+ action.run()
484
+ ```
485
+
486
+ ---
487
+
488
+ # License
489
+
490
+ MIT
@@ -0,0 +1,5 @@
1
+ import type { Middleware } from "./middleware";
2
+ export declare function createActionClient(options: {
3
+ middleware?: Middleware[];
4
+ }): void;
5
+ export declare function getGlobalMiddleware(): Middleware[];
@@ -0,0 +1,6 @@
1
+ export type MiddlewareContext = {
2
+ key?: string;
3
+ args: any[];
4
+ };
5
+ export type Middleware = (context: MiddlewareContext, next: () => Promise<any>) => Promise<any>;
6
+ export declare function runMiddleware(context: MiddlewareContext, middleware: Middleware[], fn: () => Promise<any>): Promise<any>;
@@ -0,0 +1,9 @@
1
+ type ActionState<TResult> = {
2
+ loading: boolean;
3
+ data: TResult | null;
4
+ error: any;
5
+ };
6
+ export declare function setGlobalState(key: string, state: ActionState<any>): void;
7
+ export declare function subscribe(key: string, fn: Function): () => void;
8
+ export declare function getGlobalState(key: string): ActionState<any> | undefined;
9
+ export {};
@@ -0,0 +1,3 @@
1
+ export { useAction } from "./useAction";
2
+ export { useActionStatus } from "./useActionStatus";
3
+ export { useGlobalAction } from "./useGlobalAction";
@@ -0,0 +1,21 @@
1
+ import type { Middleware } from "../core/middleware";
2
+ type AsyncFn<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>;
3
+ type Strategy = "replace" | "ignore" | "parallel";
4
+ type ActionOptions<TArgs extends any[], TResult> = {
5
+ key?: string;
6
+ retry?: number;
7
+ retryDelay?: number;
8
+ strategy?: Strategy;
9
+ optimistic?: (...args: TArgs) => void;
10
+ middleware?: Middleware[];
11
+ onSuccess?: (data: TResult) => void;
12
+ onError?: (err: any) => void;
13
+ };
14
+ export declare function useAction<TArgs extends any[], TResult>(fn: AsyncFn<TArgs, TResult>, options?: ActionOptions<TArgs, TResult>): {
15
+ loading: boolean;
16
+ data: TResult | null;
17
+ error: any;
18
+ run: (...args: TArgs) => Promise<any>;
19
+ reset: () => void;
20
+ };
21
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function useActionStatus<TData = any>(action: {
2
+ loading: boolean;
3
+ data: TData | null;
4
+ error: any;
5
+ }, field?: "loading" | "data" | "error"): any;
@@ -0,0 +1,7 @@
1
+ type ActionState<TResult = any> = {
2
+ loading: boolean;
3
+ data: TResult | null;
4
+ error: any;
5
+ };
6
+ export declare function useGlobalAction<TResult = any>(key: string): ActionState<TResult>;
7
+ export {};
@@ -0,0 +1,4 @@
1
+ export * from "./hooks";
2
+ export { createActionClient } from "./core/client";
3
+ export { logger } from "./middleware/logger";
4
+ export type { Middleware } from "./core/middleware";
@@ -0,0 +1 @@
1
+ import{useState as r,useRef as t,useCallback as n,useEffect as e}from"react";let o=[];function a(r){o=r.middleware||[]}const c=new Map,l=new Map;function i(r,t){c.set(r,t);const n=l.get(r);n&&n.forEach(r=>r(t))}async function u(r,t,n,e){try{return await r(...t)}catch(a){if(n>0)return await(o=e,new Promise(r=>setTimeout(r,o))),u(r,t,n-1,e);throw a}var o}function s(e,a={}){const{key:c,retry:l=0,retryDelay:s=500,strategy:d="replace",optimistic:g,middleware:f=[],onSuccess:y,onError:w}=a,[m,p]=r({loading:!1,data:null,error:null}),h=t(!1),k=t(0),D=n(async(...r)=>{if("ignore"===d&&h.current)return;const t=++k.current;"parallel"!==d&&(h.current=!0),g?.(...r),p(r=>({...r,loading:!0,error:null}));const n={key:c,args:r};try{const a=await function(r,t,n){let e=-1;const o=a=>{if(a<=e)return Promise.reject();e=a;const c=t[a];return c?c(r,()=>o(a+1)):n()};return o(0)}(n,[...o,...f],()=>u(e,r,l,s));if("replace"!==d||t===k.current){const r={loading:!1,data:a,error:null};p(r),c&&i(c,r),y?.(a)}return a}catch(r){if("replace"!==d||t===k.current){const t={loading:!1,data:null,error:r};p(t),c&&i(c,t),w?.(r)}throw r}finally{"parallel"!==d&&(h.current=!1)}},[e,l,s,d,g,f,c]),E=n(()=>{const r={loading:!1,data:null,error:null};p(r),c&&i(c,r)},[c]);return{loading:m.loading,data:m.data,error:m.error,run:D,reset:E}}function d(r,t){return t?r[t]:{loading:r.loading,data:r.data,error:r.error}}function g(t){const[n,o]=r(function(r){return c.get(r)}(t)||{loading:!1,data:null,error:null});return e(()=>function(r,t){return l.has(r)||l.set(r,new Set),l.get(r).add(t),()=>{l.get(r).delete(t)}}(t,o),[t]),n}function f(){return async(r,t)=>{const n=Date.now();console.log("[react-action] start:",r.key,r.args);try{const e=await t();return console.log("[react-action] success:",r.key,"time:",Date.now()-n,"ms"),e}catch(t){throw console.error("[react-action] error:",r.key,t),t}}}export{a as createActionClient,f as logger,s as useAction,d as useActionStatus,g as useGlobalAction};
package/build/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var r=require("react");let t=[];const e=new Map,n=new Map;function a(r,t){e.set(r,t);const a=n.get(r);a&&a.forEach(r=>r(t))}async function o(r,t,e,n){try{return await r(...t)}catch(c){if(e>0)return await(a=n,new Promise(r=>setTimeout(r,a))),o(r,t,e-1,n);throw c}var a}exports.createActionClient=function(r){t=r.middleware||[]},exports.logger=function(){return async(r,t)=>{const e=Date.now();console.log("[react-action] start:",r.key,r.args);try{const n=await t();return console.log("[react-action] success:",r.key,"time:",Date.now()-e,"ms"),n}catch(t){throw console.error("[react-action] error:",r.key,t),t}}},exports.useAction=function(e,n={}){const{key:c,retry:l=0,retryDelay:s=500,strategy:u="replace",optimistic:i,middleware:d=[],onSuccess:g,onError:f}=n,[y,w]=r.useState({loading:!1,data:null,error:null}),p=r.useRef(!1),h=r.useRef(0),m=r.useCallback(async(...r)=>{if("ignore"===u&&p.current)return;const n=++h.current;"parallel"!==u&&(p.current=!0),i?.(...r),w(r=>({...r,loading:!0,error:null}));const y={key:c,args:r};try{const i=await function(r,t,e){let n=-1;const a=o=>{if(o<=n)return Promise.reject();n=o;const c=t[o];return c?c(r,()=>a(o+1)):e()};return a(0)}(y,[...t,...d],()=>o(e,r,l,s));if("replace"!==u||n===h.current){const r={loading:!1,data:i,error:null};w(r),c&&a(c,r),g?.(i)}return i}catch(r){if("replace"!==u||n===h.current){const t={loading:!1,data:null,error:r};w(t),c&&a(c,t),f?.(r)}throw r}finally{"parallel"!==u&&(p.current=!1)}},[e,l,s,u,i,d,c]),k=r.useCallback(()=>{const r={loading:!1,data:null,error:null};w(r),c&&a(c,r)},[c]);return{loading:y.loading,data:y.data,error:y.error,run:m,reset:k}},exports.useActionStatus=function(r,t){return t?r[t]:{loading:r.loading,data:r.data,error:r.error}},exports.useGlobalAction=function(t){const[a,o]=r.useState(function(r){return e.get(r)}(t)||{loading:!1,data:null,error:null});return r.useEffect(()=>function(r,t){return n.has(r)||n.set(r,new Set),n.get(r).add(t),()=>{n.get(r).delete(t)}}(t,o),[t]),a};
@@ -0,0 +1,2 @@
1
+ import type { Middleware } from "../core/middleware";
2
+ export declare function logger(): Middleware;
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "react-action-z",
3
+ "version": "0.0.1",
4
+ "description": "Turn async functions into UI actions. Handle loading, error, retry, and concurrency automatically.",
5
+ "author": "Delpi.Kye",
6
+ "license": "MIT",
7
+ "type": "module",
8
+
9
+ "main": "./build/index.js",
10
+ "module": "./build/index.esm.js",
11
+ "types": "./build/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./build/index.d.ts",
15
+ "import": "./build/index.esm.js",
16
+ "require": "./build/index.js"
17
+ }
18
+ },
19
+
20
+ "files": [
21
+ "build"
22
+ ],
23
+
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "clean": "rimraf build",
27
+ "build": "rimraf build && rollup -c",
28
+ "watch": "rollup -c -w",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "engines": {
32
+ "node": ">=16"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/delpikye-v/react-action-z.git"
37
+ },
38
+ "homepage": "https://github.com/delpikye-v/react-action-z#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/delpikye-v/react-action-z/issues"
41
+ },
42
+ "keywords": [
43
+ "react",
44
+ "react-hooks",
45
+ "async-actions",
46
+ "react-async",
47
+ "react-mutation",
48
+ "react-state",
49
+ "react-data",
50
+ "async-state",
51
+ "optimistic-ui",
52
+ "react-utils",
53
+ "react-library",
54
+ "frontend",
55
+ "typescript"
56
+ ],
57
+ "peerDependencies": {
58
+ "react": ">=18",
59
+ "react-dom": ">=18"
60
+ },
61
+ "dependencies": {},
62
+ "devDependencies": {
63
+ "@rollup/plugin-commonjs": "^25.0.7",
64
+ "@rollup/plugin-node-resolve": "^15.2.3",
65
+ "@rollup/plugin-terser": "^0.4.4",
66
+ "@types/react": "^18.0.0",
67
+ "@types/react-dom": "^18.0.0",
68
+ "react": "^18.0.0",
69
+ "react-dom": "^18.0.0",
70
+ "rimraf": "^5.0.5",
71
+ "rollup": "^3.29.4",
72
+ "rollup-plugin-peer-deps-external": "^2.2.4",
73
+ "rollup-plugin-typescript2": "^0.36.0",
74
+ "tslib": "^2.6.2",
75
+ "typescript": "^5.0.0"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ }
80
+ }