@faasjs/react 8.0.0-beta.16 → 8.0.0-beta.18
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/README.md +0 -25
- package/dist/index.cjs +512 -364
- package/dist/index.d.ts +601 -341
- package/dist/index.mjs +513 -356
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -2,14 +2,18 @@ import { Component, cloneElement, createContext, forwardRef, useCallback, useCon
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
//#region src/generateId.ts
|
|
4
4
|
/**
|
|
5
|
-
* Generate random
|
|
5
|
+
* Generate a random identifier with an optional prefix.
|
|
6
6
|
*
|
|
7
|
-
* @param prefix
|
|
8
|
-
* @param length
|
|
7
|
+
* @param prefix - Prefix prepended to the generated identifier.
|
|
8
|
+
* @param length - Length of the generated identifier excluding `prefix`. Must be between `8` and `18`.
|
|
9
|
+
* @returns Generated identifier string.
|
|
10
|
+
* @throws {Error} When `length` is outside the supported `8` to `18` range.
|
|
9
11
|
*
|
|
10
12
|
* @example
|
|
11
13
|
* ```ts
|
|
12
|
-
* generateId('prefix-')
|
|
14
|
+
* const id = generateId('prefix-')
|
|
15
|
+
*
|
|
16
|
+
* id.startsWith('prefix-') // true
|
|
13
17
|
* ```
|
|
14
18
|
*/
|
|
15
19
|
function generateId(prefix = "", length = 18) {
|
|
@@ -134,14 +138,30 @@ function generateId(prefix = "", length = 18) {
|
|
|
134
138
|
* @see FaasBrowserClient.action for method returning Response
|
|
135
139
|
*/
|
|
136
140
|
var Response = class {
|
|
141
|
+
/**
|
|
142
|
+
* HTTP status code exposed to callers.
|
|
143
|
+
*/
|
|
137
144
|
status;
|
|
145
|
+
/**
|
|
146
|
+
* Response headers keyed by header name.
|
|
147
|
+
*/
|
|
138
148
|
headers;
|
|
149
|
+
/**
|
|
150
|
+
* Raw response body.
|
|
151
|
+
*/
|
|
139
152
|
body;
|
|
153
|
+
/**
|
|
154
|
+
* Parsed response payload when JSON data is available.
|
|
155
|
+
*/
|
|
140
156
|
data;
|
|
141
157
|
/**
|
|
142
158
|
* Create a wrapped response object.
|
|
143
159
|
*
|
|
144
160
|
* @param props - Response properties including status, headers, body, and data.
|
|
161
|
+
* @param props.status - HTTP status code. Defaults to `200` when `data` or `body` exists, otherwise `204`.
|
|
162
|
+
* @param props.headers - Response headers keyed by header name.
|
|
163
|
+
* @param props.body - Raw response body to expose without additional parsing.
|
|
164
|
+
* @param props.data - Parsed response payload to expose on `response.data`.
|
|
145
165
|
* @returns Wrapped response instance.
|
|
146
166
|
*/
|
|
147
167
|
constructor(props = {}) {
|
|
@@ -249,17 +269,22 @@ var Response = class {
|
|
|
249
269
|
* @see setMock for mocking errors in tests
|
|
250
270
|
*/
|
|
251
271
|
var ResponseError = class extends Error {
|
|
272
|
+
/**
|
|
273
|
+
* HTTP status code reported for the failed request.
|
|
274
|
+
*/
|
|
252
275
|
status;
|
|
276
|
+
/**
|
|
277
|
+
* Response headers returned with the error.
|
|
278
|
+
*/
|
|
253
279
|
headers;
|
|
280
|
+
/**
|
|
281
|
+
* Raw error body or fallback error payload.
|
|
282
|
+
*/
|
|
254
283
|
body;
|
|
255
|
-
originalError;
|
|
256
284
|
/**
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
* @param data - Error message, Error object, or structured response error props.
|
|
260
|
-
* @param options - Additional options such as status, headers, and body.
|
|
261
|
-
* @returns ResponseError instance.
|
|
285
|
+
* Original error used to construct this instance, when available.
|
|
262
286
|
*/
|
|
287
|
+
originalError;
|
|
263
288
|
constructor(data, options) {
|
|
264
289
|
let props;
|
|
265
290
|
if (typeof data === "string") props = {
|
|
@@ -276,11 +301,12 @@ var ResponseError = class extends Error {
|
|
|
276
301
|
this.status = props.status || 500;
|
|
277
302
|
this.headers = props.headers || {};
|
|
278
303
|
this.body = props.body || props.originalError || { error: { message: props.message } };
|
|
304
|
+
if (props.originalError) this.originalError = props.originalError;
|
|
279
305
|
}
|
|
280
306
|
};
|
|
281
307
|
let mock = null;
|
|
282
308
|
/**
|
|
283
|
-
* Set global mock handler
|
|
309
|
+
* Set the global mock handler used by all {@link FaasBrowserClient} instances.
|
|
284
310
|
*
|
|
285
311
|
* @param handler - Mock handler, can be:
|
|
286
312
|
* - MockHandler function: receives (action, params, options) and returns response data
|
|
@@ -288,24 +314,52 @@ let mock = null;
|
|
|
288
314
|
* - Response instance: pre-configured Response object
|
|
289
315
|
* - null or undefined: clear mock
|
|
290
316
|
*
|
|
317
|
+
* @example Reset in Vitest shared setup
|
|
318
|
+
* ```ts
|
|
319
|
+
* import { afterEach } from 'vitest'
|
|
320
|
+
*
|
|
321
|
+
* afterEach(() => {
|
|
322
|
+
* setMock(null)
|
|
323
|
+
* })
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* @example Use ResponseProps object
|
|
327
|
+
* ```ts
|
|
328
|
+
* setMock({
|
|
329
|
+
* data: { name: 'FaasJS' },
|
|
330
|
+
* })
|
|
331
|
+
*
|
|
332
|
+
* setMock({
|
|
333
|
+
* status: 500,
|
|
334
|
+
* data: { message: 'Internal Server Error' },
|
|
335
|
+
* })
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
291
338
|
* @example Use MockHandler function
|
|
292
339
|
* ```ts
|
|
293
|
-
* setMock(async (action
|
|
294
|
-
* if (action === '
|
|
295
|
-
* return { data: { name: '
|
|
340
|
+
* setMock(async (action) => {
|
|
341
|
+
* if (action === '/pages/users/get') {
|
|
342
|
+
* return { data: { id: 1, name: 'FaasJS' } }
|
|
296
343
|
* }
|
|
297
|
-
*
|
|
344
|
+
*
|
|
345
|
+
* return { status: 404, data: { message: 'Not Found' } }
|
|
298
346
|
* })
|
|
299
347
|
*
|
|
300
|
-
* const response = await client.action('
|
|
348
|
+
* const response = await client.action('/pages/users/get')
|
|
301
349
|
* ```
|
|
302
350
|
*
|
|
303
|
-
* @example
|
|
351
|
+
* @example Branch by action and params
|
|
304
352
|
* ```ts
|
|
305
|
-
* setMock({
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
353
|
+
* setMock(async (action, params) => {
|
|
354
|
+
* if (action === '/pages/users/get' && params?.id === 1) {
|
|
355
|
+
* return { data: { id: 1, name: 'Admin' } }
|
|
356
|
+
* }
|
|
357
|
+
*
|
|
358
|
+
* if (action === '/pages/users/get' && params?.id === 2) {
|
|
359
|
+
* return { data: { id: 2, name: 'Editor' } }
|
|
360
|
+
* }
|
|
361
|
+
*
|
|
362
|
+
* return { status: 404, data: { message: 'User not found' } }
|
|
309
363
|
* })
|
|
310
364
|
* ```
|
|
311
365
|
*
|
|
@@ -317,11 +371,22 @@ let mock = null;
|
|
|
317
371
|
* }))
|
|
318
372
|
* ```
|
|
319
373
|
*
|
|
374
|
+
* @example Streaming response
|
|
375
|
+
* ```ts
|
|
376
|
+
* setMock({
|
|
377
|
+
* body: new ReadableStream({
|
|
378
|
+
* start(controller) {
|
|
379
|
+
* controller.enqueue(new TextEncoder().encode('hello'))
|
|
380
|
+
* controller.enqueue(new TextEncoder().encode(' world'))
|
|
381
|
+
* controller.close()
|
|
382
|
+
* },
|
|
383
|
+
* }),
|
|
384
|
+
* })
|
|
385
|
+
* ```
|
|
386
|
+
*
|
|
320
387
|
* @example Clear mock
|
|
321
388
|
* ```ts
|
|
322
389
|
* setMock(null)
|
|
323
|
-
* // or
|
|
324
|
-
* setMock(undefined)
|
|
325
390
|
* ```
|
|
326
391
|
*
|
|
327
392
|
* @example Handle errors
|
|
@@ -405,14 +470,25 @@ function setMock(handler) {
|
|
|
405
470
|
* @see ResponseError for error handling
|
|
406
471
|
*/
|
|
407
472
|
var FaasBrowserClient = class {
|
|
473
|
+
/**
|
|
474
|
+
* Unique identifier for this client instance.
|
|
475
|
+
*/
|
|
408
476
|
id;
|
|
477
|
+
/**
|
|
478
|
+
* Base URL used to build action request URLs.
|
|
479
|
+
*/
|
|
409
480
|
baseUrl;
|
|
481
|
+
/**
|
|
482
|
+
* Default request options merged into every request.
|
|
483
|
+
*/
|
|
410
484
|
defaultOptions;
|
|
411
485
|
/**
|
|
412
486
|
* Creates a new FaasBrowserClient instance.
|
|
413
487
|
*
|
|
414
488
|
* @param baseUrl - Base URL for all API requests. Must end with `/`. Defaults to `/` for relative requests.
|
|
415
489
|
* @param options - Default request options such as headers, hooks, request override, or stream mode.
|
|
490
|
+
* See {@link Options} for supported request fields such as `headers`, `beforeRequest`,
|
|
491
|
+
* `request`, `baseUrl`, and `stream`.
|
|
416
492
|
*
|
|
417
493
|
* @example Basic initialization
|
|
418
494
|
* ```ts
|
|
@@ -482,9 +558,11 @@ var FaasBrowserClient = class {
|
|
|
482
558
|
* Optional if the function accepts no parameters.
|
|
483
559
|
* @param options - Optional request options that override client defaults.
|
|
484
560
|
* Supports headers, beforeRequest hook, custom request function, baseUrl override, and streaming mode.
|
|
561
|
+
* See {@link Options} for supported request fields such as `headers`, `beforeRequest`,
|
|
562
|
+
* `request`, `baseUrl`, and `stream`.
|
|
485
563
|
*
|
|
486
|
-
* @returns A
|
|
487
|
-
*
|
|
564
|
+
* @returns A promise resolving to the wrapped FaasJS response. When `options.stream`
|
|
565
|
+
* is `true`, the runtime returns the native fetch response so callers can read the stream.
|
|
488
566
|
*
|
|
489
567
|
* @throws {Error} When action is not provided or is empty
|
|
490
568
|
* @throws {ResponseError} When the server returns an error response (status >= 400 or body.error exists)
|
|
@@ -662,16 +740,24 @@ var FaasBrowserClient = class {
|
|
|
662
740
|
/**
|
|
663
741
|
* Call the currently configured FaasReactClient.
|
|
664
742
|
*
|
|
743
|
+
* This helper forwards the request to `getClient`. When the registered
|
|
744
|
+
* client defines `onError`, the hook is invoked before the promise rejects.
|
|
745
|
+
*
|
|
746
|
+
* @template PathOrData - Action path or response data type used for inference.
|
|
747
|
+
*
|
|
665
748
|
* @param action - Action path to invoke.
|
|
666
749
|
* @param params - Parameters sent to the action.
|
|
667
750
|
* @param options - Optional per-request overrides such as headers or base URL.
|
|
751
|
+
* See the request `Options` type for supported fields such as `headers`, `beforeRequest`,
|
|
752
|
+
* `request`, `baseUrl`, and `stream`.
|
|
668
753
|
* @returns Response returned by the active browser client.
|
|
754
|
+
* @throws {ResponseError} When the request fails and the active client does not recover inside `onError`.
|
|
669
755
|
*
|
|
670
756
|
* @example
|
|
671
757
|
* ```ts
|
|
672
758
|
* import { faas } from '@faasjs/react'
|
|
673
759
|
*
|
|
674
|
-
* const response = await faas
|
|
760
|
+
* const response = await faas('posts/get', { id: 1 })
|
|
675
761
|
*
|
|
676
762
|
* console.log(response.data.title)
|
|
677
763
|
* ```
|
|
@@ -698,6 +784,14 @@ const AsyncFunction = (async () => {}).constructor;
|
|
|
698
784
|
* @param a - The first value to compare.
|
|
699
785
|
* @param b - The second value to compare.
|
|
700
786
|
* @returns `true` if the values are deeply equal, `false` otherwise.
|
|
787
|
+
*
|
|
788
|
+
* @example
|
|
789
|
+
* ```ts
|
|
790
|
+
* import { equal } from '@faasjs/react'
|
|
791
|
+
*
|
|
792
|
+
* equal({ page: 1, filters: ['a'] }, { page: 1, filters: ['a'] }) // true
|
|
793
|
+
* equal({ page: 1 }, { page: 2 }) // false
|
|
794
|
+
* ```
|
|
701
795
|
*/
|
|
702
796
|
function equal(a, b) {
|
|
703
797
|
if (a === b) return true;
|
|
@@ -732,6 +826,17 @@ function equal(a, b) {
|
|
|
732
826
|
*
|
|
733
827
|
* @param value - The value to be memoized.
|
|
734
828
|
* @returns The memoized value.
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* ```tsx
|
|
832
|
+
* import { useEqualMemoize } from '@faasjs/react'
|
|
833
|
+
*
|
|
834
|
+
* function Filters({ filters }: { filters: Record<string, any> }) {
|
|
835
|
+
* const memoizedFilters = useEqualMemoize(filters)
|
|
836
|
+
*
|
|
837
|
+
* return <pre>{JSON.stringify(memoizedFilters)}</pre>
|
|
838
|
+
* }
|
|
839
|
+
* ```
|
|
735
840
|
*/
|
|
736
841
|
function useEqualMemoize(value) {
|
|
737
842
|
const ref = useRef(value);
|
|
@@ -753,6 +858,19 @@ function useEqualSignal(value) {
|
|
|
753
858
|
* @param callback - The effect callback function to run.
|
|
754
859
|
* @param dependencies - The list of dependencies for the effect.
|
|
755
860
|
* @returns The result of the `useEffect` hook with memoized dependencies.
|
|
861
|
+
*
|
|
862
|
+
* @example
|
|
863
|
+
* ```tsx
|
|
864
|
+
* import { useEqualEffect } from '@faasjs/react'
|
|
865
|
+
*
|
|
866
|
+
* function Page({ filters }: { filters: Record<string, any> }) {
|
|
867
|
+
* useEqualEffect(() => {
|
|
868
|
+
* console.log('filters changed', filters)
|
|
869
|
+
* }, [filters])
|
|
870
|
+
*
|
|
871
|
+
* return null
|
|
872
|
+
* }
|
|
873
|
+
* ```
|
|
756
874
|
*/
|
|
757
875
|
function useEqualEffect(callback, dependencies) {
|
|
758
876
|
return useEffect(callback, [useEqualSignal(dependencies)]);
|
|
@@ -760,9 +878,22 @@ function useEqualEffect(callback, dependencies) {
|
|
|
760
878
|
/**
|
|
761
879
|
* Custom hook that works like `useMemo` but uses deep comparison on dependencies.
|
|
762
880
|
*
|
|
881
|
+
* @template T - Memoized value type returned by the callback.
|
|
882
|
+
*
|
|
763
883
|
* @param callback - The callback function to run.
|
|
764
884
|
* @param dependencies - The list of dependencies.
|
|
765
885
|
* @returns The result of the `useMemo` hook with memoized dependencies.
|
|
886
|
+
*
|
|
887
|
+
* @example
|
|
888
|
+
* ```tsx
|
|
889
|
+
* import { useEqualMemo } from '@faasjs/react'
|
|
890
|
+
*
|
|
891
|
+
* function Page({ filters }: { filters: Record<string, any> }) {
|
|
892
|
+
* const queryString = useEqualMemo(() => JSON.stringify(filters), [filters])
|
|
893
|
+
*
|
|
894
|
+
* return <span>{queryString}</span>
|
|
895
|
+
* }
|
|
896
|
+
* ```
|
|
766
897
|
*/
|
|
767
898
|
function useEqualMemo(callback, dependencies) {
|
|
768
899
|
const signal = useEqualSignal(dependencies);
|
|
@@ -775,13 +906,113 @@ function useEqualMemo(callback, dependencies) {
|
|
|
775
906
|
/**
|
|
776
907
|
* Custom hook that works like `useCallback` but uses deep comparison on dependencies.
|
|
777
908
|
*
|
|
909
|
+
* @template T - Callback signature to memoize.
|
|
910
|
+
*
|
|
778
911
|
* @param callback - The callback function to run.
|
|
779
912
|
* @param dependencies - The list of dependencies.
|
|
780
913
|
* @returns The result of the `useCallback` hook with memoized dependencies.
|
|
914
|
+
*
|
|
915
|
+
* @example
|
|
916
|
+
* ```tsx
|
|
917
|
+
* import { useEqualCallback } from '@faasjs/react'
|
|
918
|
+
*
|
|
919
|
+
* function Search({ filters }: { filters: Record<string, any> }) {
|
|
920
|
+
* const handleSubmit = useEqualCallback(() => {
|
|
921
|
+
* console.log(filters)
|
|
922
|
+
* }, [filters])
|
|
923
|
+
*
|
|
924
|
+
* return <button onClick={handleSubmit}>Search</button>
|
|
925
|
+
* }
|
|
926
|
+
* ```
|
|
781
927
|
*/
|
|
782
928
|
function useEqualCallback(callback, dependencies) {
|
|
783
929
|
return useCallback((...args) => callback(...args), [useEqualSignal(dependencies)]);
|
|
784
930
|
}
|
|
931
|
+
/**
|
|
932
|
+
* Fetch FaasJS data and inject the result into a render prop or child element.
|
|
933
|
+
*
|
|
934
|
+
* The wrapper defers rendering `children` or `render` until the first request
|
|
935
|
+
* completes, then keeps passing the latest request state to the rendered output.
|
|
936
|
+
*
|
|
937
|
+
* @param props - Wrapper props controlling the request and rendered fallback.
|
|
938
|
+
* @param props.render - Render prop that receives the resolved Faas request state.
|
|
939
|
+
* @param props.children - Child element cloned with injected Faas request state.
|
|
940
|
+
* @param props.fallback - Element rendered before the first successful load.
|
|
941
|
+
* @param props.action - Action path to request.
|
|
942
|
+
* @param props.params - Params sent to the action.
|
|
943
|
+
* @param props.onDataChange - Callback invoked when the resolved data value changes.
|
|
944
|
+
* @param props.data - Controlled data value used instead of internal state.
|
|
945
|
+
* @param props.setData - Controlled setter used instead of internal state.
|
|
946
|
+
* @param props.baseUrl - Base URL override used for this wrapper instance.
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* ```tsx
|
|
950
|
+
* import { FaasDataWrapper } from '@faasjs/react'
|
|
951
|
+
*
|
|
952
|
+
* type User = {
|
|
953
|
+
* name: string
|
|
954
|
+
* }
|
|
955
|
+
*
|
|
956
|
+
* function UserView(props: {
|
|
957
|
+
* data?: User
|
|
958
|
+
* error?: Error
|
|
959
|
+
* reload?: () => void
|
|
960
|
+
* }) {
|
|
961
|
+
* if (props.error) {
|
|
962
|
+
* return (
|
|
963
|
+
* <div>
|
|
964
|
+
* <p>Failed to load user: {props.error.message}</p>
|
|
965
|
+
* <button type="button" onClick={() => props.reload?.()}>
|
|
966
|
+
* Retry
|
|
967
|
+
* </button>
|
|
968
|
+
* </div>
|
|
969
|
+
* )
|
|
970
|
+
* }
|
|
971
|
+
*
|
|
972
|
+
* return <div>Hello, {props.data?.name}</div>
|
|
973
|
+
* }
|
|
974
|
+
*
|
|
975
|
+
* // Render-prop mode
|
|
976
|
+
* export function UserProfile(props: { id: number }) {
|
|
977
|
+
* return (
|
|
978
|
+
* <FaasDataWrapper<User>
|
|
979
|
+
* action="/pages/users/get"
|
|
980
|
+
* params={{ id: props.id }}
|
|
981
|
+
* fallback={<div>Loading user...</div>}
|
|
982
|
+
* render={({ data, error, reload }) => {
|
|
983
|
+
* if (error) {
|
|
984
|
+
* return (
|
|
985
|
+
* <div>
|
|
986
|
+
* <p>Failed to load user: {error.message}</p>
|
|
987
|
+
* <button type="button" onClick={() => reload()}>
|
|
988
|
+
* Retry
|
|
989
|
+
* </button>
|
|
990
|
+
* </div>
|
|
991
|
+
* )
|
|
992
|
+
* }
|
|
993
|
+
*
|
|
994
|
+
* return <div>Hello, {data.name}</div>
|
|
995
|
+
* }}
|
|
996
|
+
* />
|
|
997
|
+
* )
|
|
998
|
+
* }
|
|
999
|
+
*
|
|
1000
|
+
* // Children injection mode
|
|
1001
|
+
* export function UserProfileWithChildren(props: { id: number }) {
|
|
1002
|
+
* return (
|
|
1003
|
+
* <FaasDataWrapper<User>
|
|
1004
|
+
* action="/pages/users/get"
|
|
1005
|
+
* params={{ id: props.id }}
|
|
1006
|
+
* fallback={<div>Loading user...</div>}
|
|
1007
|
+
* >
|
|
1008
|
+
* <UserView />
|
|
1009
|
+
* </FaasDataWrapper>
|
|
1010
|
+
* )
|
|
1011
|
+
* }
|
|
1012
|
+
* ```
|
|
1013
|
+
*
|
|
1014
|
+
* When a ref is provided, it exposes the current Faas request state imperatively.
|
|
1015
|
+
*/
|
|
785
1016
|
const FaasDataWrapper = forwardRef((props, ref) => {
|
|
786
1017
|
const requestOptions = {
|
|
787
1018
|
...props.data !== void 0 ? { data: props.data } : {},
|
|
@@ -813,11 +1044,36 @@ const FaasDataWrapper = forwardRef((props, ref) => {
|
|
|
813
1044
|
});
|
|
814
1045
|
Object.assign(FaasDataWrapper, { displayName: "FaasDataWrapper" });
|
|
815
1046
|
/**
|
|
816
|
-
*
|
|
1047
|
+
* Wrap a component with {@link FaasDataWrapper} and inject Faas request state as props.
|
|
1048
|
+
*
|
|
1049
|
+
* `withFaasData` is most useful for wrapper-style exports or compatibility with
|
|
1050
|
+
* an existing component boundary. For new code, prefer `useFaas` or
|
|
1051
|
+
* `FaasDataWrapper` when they express the request ownership more directly.
|
|
1052
|
+
*
|
|
1053
|
+
* @template PathOrData - Action path or response data type used for inference.
|
|
1054
|
+
* @template TComponentProps - Component props including injected Faas data fields.
|
|
1055
|
+
* @param Component - Component that consumes injected Faas data props.
|
|
1056
|
+
* @param faasProps - Request configuration forwarded to `FaasDataWrapper`.
|
|
1057
|
+
* @returns Component that accepts the original props minus the injected Faas data fields.
|
|
817
1058
|
*
|
|
818
1059
|
* @example
|
|
819
1060
|
* ```tsx
|
|
820
|
-
*
|
|
1061
|
+
* import { withFaasData } from '@faasjs/react'
|
|
1062
|
+
*
|
|
1063
|
+
* const MyComponent = withFaasData(
|
|
1064
|
+
* ({ data, error, reload }) => {
|
|
1065
|
+
* if (error) {
|
|
1066
|
+
* return (
|
|
1067
|
+
* <button type="button" onClick={() => reload()}>
|
|
1068
|
+
* Retry
|
|
1069
|
+
* </button>
|
|
1070
|
+
* )
|
|
1071
|
+
* }
|
|
1072
|
+
*
|
|
1073
|
+
* return <div>{data.name}</div>
|
|
1074
|
+
* },
|
|
1075
|
+
* { action: '/pages/users/get', params: { id: 1 } },
|
|
1076
|
+
* )
|
|
821
1077
|
* ```
|
|
822
1078
|
*/
|
|
823
1079
|
function withFaasData(Component, faasProps) {
|
|
@@ -831,22 +1087,51 @@ function withFaasData(Component, faasProps) {
|
|
|
831
1087
|
/**
|
|
832
1088
|
* Request FaasJS data and keep request state in React state.
|
|
833
1089
|
*
|
|
834
|
-
* `useFaas`
|
|
835
|
-
*
|
|
1090
|
+
* `useFaas` is the default hook for standard FaasJS request-response flows in React.
|
|
1091
|
+
* It sends an initial request unless `skip` is enabled, and returns request state
|
|
1092
|
+
* plus helpers for reloading, updating data, and handling errors.
|
|
1093
|
+
*
|
|
1094
|
+
* @template PathOrData - Action path or response data type used for inference.
|
|
836
1095
|
*
|
|
837
1096
|
* @param action - Action path to invoke.
|
|
838
1097
|
* @param defaultParams - Params used for the initial request and future reloads.
|
|
839
1098
|
* @param options - Optional hook configuration such as controlled data, debounce, and skip logic.
|
|
840
|
-
* @
|
|
1099
|
+
* @param options.params - Request params override used without mutating the hook's stored params state.
|
|
1100
|
+
* @param options.data - Controlled data value used instead of the hook's internal state.
|
|
1101
|
+
* @param options.setData - Controlled setter used instead of the hook's internal `setData`.
|
|
1102
|
+
* @param options.skip - Boolean or predicate that suppresses the automatic request until `reload()` runs.
|
|
1103
|
+
* @param options.debounce - Milliseconds to wait before sending the latest request.
|
|
1104
|
+
* @param options.baseUrl - Base URL override used for this hook instance.
|
|
1105
|
+
* @returns Request state and helper methods described by {@link FaasDataInjection}.
|
|
841
1106
|
*
|
|
842
1107
|
* @example
|
|
843
1108
|
* ```tsx
|
|
844
1109
|
* import { useFaas } from '@faasjs/react'
|
|
845
1110
|
*
|
|
846
|
-
* function
|
|
847
|
-
* const { data } = useFaas
|
|
1111
|
+
* function Profile({ id }: { id: number }) {
|
|
1112
|
+
* const { data, error, loading, reload } = useFaas('/pages/users/get', { id })
|
|
1113
|
+
*
|
|
1114
|
+
* if (loading) return <div>Loading...</div>
|
|
1115
|
+
*
|
|
1116
|
+
* if (error) {
|
|
1117
|
+
* return (
|
|
1118
|
+
* <div>
|
|
1119
|
+
* <div>Load failed: {error.message}</div>
|
|
1120
|
+
* <button type="button" onClick={() => reload()}>
|
|
1121
|
+
* Retry
|
|
1122
|
+
* </button>
|
|
1123
|
+
* </div>
|
|
1124
|
+
* )
|
|
1125
|
+
* }
|
|
848
1126
|
*
|
|
849
|
-
* return
|
|
1127
|
+
* return (
|
|
1128
|
+
* <div>
|
|
1129
|
+
* <span>{data.name}</span>
|
|
1130
|
+
* <button type="button" onClick={() => reload()}>
|
|
1131
|
+
* Refresh
|
|
1132
|
+
* </button>
|
|
1133
|
+
* </div>
|
|
1134
|
+
* )
|
|
850
1135
|
* }
|
|
851
1136
|
* ```
|
|
852
1137
|
*/
|
|
@@ -933,7 +1218,6 @@ function useFaas(action, defaultParams, options = {}) {
|
|
|
933
1218
|
if (skip) setSkip(false);
|
|
934
1219
|
if (params) setParams(params);
|
|
935
1220
|
const reloadCounter = ++reloadCounterRef.current;
|
|
936
|
-
setReloadTimes((prev) => prev + 1);
|
|
937
1221
|
return new Promise((resolve, reject) => {
|
|
938
1222
|
pendingReloadsRef.current.set(reloadCounter, {
|
|
939
1223
|
resolve,
|
|
@@ -971,18 +1255,32 @@ const clients = {};
|
|
|
971
1255
|
* used by helpers such as {@link faas} and {@link useFaas}.
|
|
972
1256
|
*
|
|
973
1257
|
* @param options - Client configuration including base URL, default request options, and error hooks.
|
|
1258
|
+
* @param options.baseUrl - Base URL used to register and route the client instance.
|
|
1259
|
+
* @param options.options - Default browser-client request options forwarded to `FaasBrowserClient`.
|
|
1260
|
+
* @param options.onError - Hook factory used to handle failed `faas` and `useFaas` requests.
|
|
1261
|
+
* See {@link Options} for supported browser-client request fields such as `headers`,
|
|
1262
|
+
* `beforeRequest`, `request`, `baseUrl`, and `stream`.
|
|
974
1263
|
* @returns Registered FaasReactClient instance.
|
|
975
1264
|
*
|
|
976
1265
|
* @example
|
|
977
1266
|
* ```ts
|
|
978
|
-
* import { FaasReactClient } from '@faasjs/react'
|
|
1267
|
+
* import { FaasReactClient, ResponseError } from '@faasjs/react'
|
|
979
1268
|
*
|
|
980
1269
|
* const client = FaasReactClient({
|
|
981
1270
|
* baseUrl: 'http://localhost:8080/api/',
|
|
1271
|
+
* onError: (action, params) => async (res) => {
|
|
1272
|
+
* if (res instanceof ResponseError) {
|
|
1273
|
+
* reportErrorToSentry(res, {
|
|
1274
|
+
* tags: { action },
|
|
1275
|
+
* extra: { params },
|
|
1276
|
+
* })
|
|
1277
|
+
* }
|
|
1278
|
+
* },
|
|
982
1279
|
* })
|
|
983
1280
|
* ```
|
|
984
1281
|
*/
|
|
985
|
-
function FaasReactClient(
|
|
1282
|
+
function FaasReactClient(options = { baseUrl: "/" }) {
|
|
1283
|
+
const { baseUrl, options: clientOptions, onError } = options;
|
|
986
1284
|
const resolvedBaseUrl = baseUrl ?? "/";
|
|
987
1285
|
const client = new FaasBrowserClient(resolvedBaseUrl, clientOptions);
|
|
988
1286
|
function withBaseUrl(options) {
|
|
@@ -1011,16 +1309,28 @@ function FaasReactClient({ baseUrl, options: clientOptions, onError } = { baseUr
|
|
|
1011
1309
|
*
|
|
1012
1310
|
* When `host` is omitted, the first registered client is returned. If no client
|
|
1013
1311
|
* has been created yet, a default client is initialized automatically.
|
|
1312
|
+
* Use `getClient` only for special cases such as multiple Faas clients with
|
|
1313
|
+
* different base URLs. In normal single-client app code, prefer the default
|
|
1314
|
+
* `faas`, `useFaas`, or `FaasReactClient` setup directly.
|
|
1014
1315
|
*
|
|
1015
1316
|
* @param host - Registered base URL to look up. Omit it to use the default client.
|
|
1016
1317
|
* @returns Registered or newly created FaasReactClient instance.
|
|
1017
1318
|
*
|
|
1018
1319
|
* @example
|
|
1019
1320
|
* ```ts
|
|
1020
|
-
* import { getClient } from '@faasjs/react'
|
|
1321
|
+
* import { FaasReactClient, getClient } from '@faasjs/react'
|
|
1322
|
+
*
|
|
1323
|
+
* FaasReactClient({
|
|
1324
|
+
* baseUrl: 'https://service-a.example.com/api/',
|
|
1325
|
+
* })
|
|
1326
|
+
*
|
|
1327
|
+
* FaasReactClient({
|
|
1328
|
+
* baseUrl: 'https://service-b.example.com/api/',
|
|
1329
|
+
* })
|
|
1021
1330
|
*
|
|
1022
|
-
* getClient()
|
|
1023
|
-
*
|
|
1331
|
+
* const client = getClient('https://service-b.example.com/api/')
|
|
1332
|
+
*
|
|
1333
|
+
* await client.faas('/pages/posts/get', { id: 1 })
|
|
1024
1334
|
* ```
|
|
1025
1335
|
*/
|
|
1026
1336
|
function getClient(host) {
|
|
@@ -1035,6 +1345,20 @@ function getClient(host) {
|
|
|
1035
1345
|
//#region src/constant.ts
|
|
1036
1346
|
/**
|
|
1037
1347
|
* Returns a constant value that is created by the given function.
|
|
1348
|
+
*
|
|
1349
|
+
* @template T - Constant value type returned by the initializer.
|
|
1350
|
+
* @param fn - Initializer that runs only once for the current component instance.
|
|
1351
|
+
*
|
|
1352
|
+
* @example
|
|
1353
|
+
* ```tsx
|
|
1354
|
+
* import { useConstant } from '@faasjs/react'
|
|
1355
|
+
*
|
|
1356
|
+
* function Page() {
|
|
1357
|
+
* const requestId = useConstant(() => crypto.randomUUID())
|
|
1358
|
+
*
|
|
1359
|
+
* return <span>{requestId}</span>
|
|
1360
|
+
* }
|
|
1361
|
+
* ```
|
|
1038
1362
|
*/
|
|
1039
1363
|
function useConstant(fn) {
|
|
1040
1364
|
const ref = useRef(null);
|
|
@@ -1043,8 +1367,39 @@ function useConstant(fn) {
|
|
|
1043
1367
|
}
|
|
1044
1368
|
//#endregion
|
|
1045
1369
|
//#region src/ErrorBoundary.tsx
|
|
1370
|
+
/**
|
|
1371
|
+
* React error boundary with an optional custom fallback element.
|
|
1372
|
+
*
|
|
1373
|
+
* The boundary renders its children until a descendant throws. After that it
|
|
1374
|
+
* either clones `errorChildren` with injected error details or renders a simple
|
|
1375
|
+
* built-in fallback.
|
|
1376
|
+
*
|
|
1377
|
+
* @example
|
|
1378
|
+
* ```tsx
|
|
1379
|
+
* import { ErrorBoundary } from '@faasjs/react'
|
|
1380
|
+
*
|
|
1381
|
+
* function Fallback({ errorMessage }: { errorMessage?: string }) {
|
|
1382
|
+
* return <div>{errorMessage}</div>
|
|
1383
|
+
* }
|
|
1384
|
+
*
|
|
1385
|
+
* <ErrorBoundary errorChildren={<Fallback />}>
|
|
1386
|
+
* <DangerousWidget />
|
|
1387
|
+
* </ErrorBoundary>
|
|
1388
|
+
* ```
|
|
1389
|
+
*/
|
|
1046
1390
|
var ErrorBoundary = class extends Component {
|
|
1391
|
+
/**
|
|
1392
|
+
* Stable display name used by React DevTools.
|
|
1393
|
+
*/
|
|
1047
1394
|
static displayName = "ErrorBoundary";
|
|
1395
|
+
/**
|
|
1396
|
+
* Create an error boundary with empty error state.
|
|
1397
|
+
*
|
|
1398
|
+
* @param props - Boundary props.
|
|
1399
|
+
* @param props.children - Descendant elements protected by the boundary.
|
|
1400
|
+
* @param props.onError - Callback invoked after a render error is captured.
|
|
1401
|
+
* @param props.errorChildren - Custom fallback element that receives error details.
|
|
1402
|
+
*/
|
|
1048
1403
|
constructor(props) {
|
|
1049
1404
|
super(props);
|
|
1050
1405
|
this.state = {
|
|
@@ -1052,12 +1407,21 @@ var ErrorBoundary = class extends Component {
|
|
|
1052
1407
|
info: { componentStack: "" }
|
|
1053
1408
|
};
|
|
1054
1409
|
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Capture rendering errors from descendant components.
|
|
1412
|
+
*
|
|
1413
|
+
* @param error - Caught render error.
|
|
1414
|
+
* @param info - React component stack metadata.
|
|
1415
|
+
*/
|
|
1055
1416
|
componentDidCatch(error, info) {
|
|
1056
1417
|
this.setState({
|
|
1057
1418
|
error,
|
|
1058
1419
|
info
|
|
1059
1420
|
});
|
|
1060
1421
|
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Render children or the configured fallback for the captured error.
|
|
1424
|
+
*/
|
|
1061
1425
|
render() {
|
|
1062
1426
|
const { error, info } = this.state;
|
|
1063
1427
|
const errorMessage = String(error ?? "");
|
|
@@ -1076,43 +1440,41 @@ var ErrorBoundary = class extends Component {
|
|
|
1076
1440
|
}
|
|
1077
1441
|
};
|
|
1078
1442
|
//#endregion
|
|
1079
|
-
//#region src/
|
|
1443
|
+
//#region src/OptionalWrapper.tsx
|
|
1080
1444
|
/**
|
|
1081
|
-
*
|
|
1445
|
+
* Conditionally wrap children with another component.
|
|
1082
1446
|
*
|
|
1083
|
-
* @
|
|
1084
|
-
* @param
|
|
1085
|
-
* @
|
|
1447
|
+
* @param props - Wrapper condition, wrapper component, and child content.
|
|
1448
|
+
* @param props.condition - When `true`, wrap children with `Wrapper`.
|
|
1449
|
+
* @param props.Wrapper - Component used as the wrapper when the condition passes.
|
|
1450
|
+
* @param props.wrapperProps - Props forwarded to the wrapper component.
|
|
1451
|
+
* @param props.children - Content rendered directly or inside the wrapper.
|
|
1452
|
+
* @returns Wrapped children or the original children when `condition` is false.
|
|
1086
1453
|
*
|
|
1087
1454
|
* @example
|
|
1088
1455
|
* ```tsx
|
|
1089
|
-
* import {
|
|
1456
|
+
* import { OptionalWrapper } from '@faasjs/react'
|
|
1090
1457
|
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1458
|
+
* const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
1459
|
+
* <div className='wrapper'>{children}</div>
|
|
1460
|
+
* )
|
|
1093
1461
|
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
* </div>
|
|
1100
|
-
* )
|
|
1101
|
-
* }
|
|
1462
|
+
* const App = () => (
|
|
1463
|
+
* <OptionalWrapper condition={true} Wrapper={Wrapper}>
|
|
1464
|
+
* <span>Test</span>
|
|
1465
|
+
* </OptionalWrapper>
|
|
1466
|
+
* )
|
|
1102
1467
|
* ```
|
|
1103
1468
|
*/
|
|
1104
|
-
function
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
return
|
|
1111
|
-
state,
|
|
1112
|
-
setState,
|
|
1113
|
-
ref
|
|
1114
|
-
];
|
|
1469
|
+
function OptionalWrapper(props) {
|
|
1470
|
+
const { condition, Wrapper, wrapperProps, children } = props;
|
|
1471
|
+
if (condition) return /* @__PURE__ */ jsx(Wrapper, {
|
|
1472
|
+
...wrapperProps,
|
|
1473
|
+
children
|
|
1474
|
+
});
|
|
1475
|
+
return children;
|
|
1115
1476
|
}
|
|
1477
|
+
OptionalWrapper.displayName = "OptionalWrapper";
|
|
1116
1478
|
//#endregion
|
|
1117
1479
|
//#region src/splittingState.tsx
|
|
1118
1480
|
/**
|
|
@@ -1145,9 +1507,15 @@ function useSplittingState(initialStates) {
|
|
|
1145
1507
|
//#endregion
|
|
1146
1508
|
//#region src/splittingContext.tsx
|
|
1147
1509
|
/**
|
|
1148
|
-
*
|
|
1510
|
+
* Create a context whose keys can be consumed independently.
|
|
1511
|
+
*
|
|
1512
|
+
* `createSplittingContext` returns a `Provider` and a `use` hook. Each key in
|
|
1513
|
+
* the provided shape is backed by a separate React context so readers only
|
|
1514
|
+
* subscribe to the values they access.
|
|
1149
1515
|
*
|
|
1150
|
-
* @
|
|
1516
|
+
* @template T - Context value shape exposed by the provider and hook.
|
|
1517
|
+
* @param defaultValue - Default value map or key list used to create split contexts.
|
|
1518
|
+
* @returns Provider and hook helpers for the split context.
|
|
1151
1519
|
*
|
|
1152
1520
|
* @example
|
|
1153
1521
|
* ```tsx
|
|
@@ -1225,306 +1593,46 @@ function createSplittingContext(defaultValue) {
|
|
|
1225
1593
|
};
|
|
1226
1594
|
}
|
|
1227
1595
|
//#endregion
|
|
1228
|
-
//#region src/Form/context.tsx
|
|
1229
|
-
const FormContext = createSplittingContext([
|
|
1230
|
-
"items",
|
|
1231
|
-
"onSubmit",
|
|
1232
|
-
"Elements",
|
|
1233
|
-
"lang",
|
|
1234
|
-
"rules",
|
|
1235
|
-
"submitting",
|
|
1236
|
-
"setSubmitting",
|
|
1237
|
-
"values",
|
|
1238
|
-
"setValues",
|
|
1239
|
-
"errors",
|
|
1240
|
-
"setErrors",
|
|
1241
|
-
"valuesRef"
|
|
1242
|
-
]);
|
|
1243
|
-
const FormContextProvider = FormContext.Provider;
|
|
1244
|
-
const useFormContext = FormContext.use;
|
|
1245
|
-
//#endregion
|
|
1246
|
-
//#region src/Form/Input.tsx
|
|
1247
|
-
function processValue(input, rules) {
|
|
1248
|
-
switch (rules?.type) {
|
|
1249
|
-
case "number": return Number(input);
|
|
1250
|
-
case "string": return String(input);
|
|
1251
|
-
default: return input;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
function FormInput({ name, rules, ...rest }) {
|
|
1255
|
-
const { Elements, values, setValues } = useFormContext();
|
|
1256
|
-
const value = values?.[name];
|
|
1257
|
-
if (rest.Input) return /* @__PURE__ */ jsx(rest.Input, {
|
|
1258
|
-
name,
|
|
1259
|
-
value,
|
|
1260
|
-
onChange: (v) => setValues((prev) => ({
|
|
1261
|
-
...prev,
|
|
1262
|
-
[name]: processValue(v, rules)
|
|
1263
|
-
})),
|
|
1264
|
-
...rest.props
|
|
1265
|
-
});
|
|
1266
|
-
return /* @__PURE__ */ jsx(Elements.Input, {
|
|
1267
|
-
name,
|
|
1268
|
-
value,
|
|
1269
|
-
onChange: (v) => setValues((prev) => ({
|
|
1270
|
-
...prev,
|
|
1271
|
-
[name]: processValue(v, rules)
|
|
1272
|
-
})),
|
|
1273
|
-
...rest.props
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
FormInput.displayName = "FormInput";
|
|
1277
|
-
//#endregion
|
|
1278
|
-
//#region src/Form/Item.tsx
|
|
1279
|
-
function FormItem(props) {
|
|
1280
|
-
const { Elements, errors } = useFormContext();
|
|
1281
|
-
return /* @__PURE__ */ jsx(props.label?.Label ?? Elements.Label, {
|
|
1282
|
-
name: props.name,
|
|
1283
|
-
...props.label,
|
|
1284
|
-
error: errors[props.name],
|
|
1285
|
-
children: /* @__PURE__ */ jsx(FormInput, {
|
|
1286
|
-
name: props.name,
|
|
1287
|
-
...props.input,
|
|
1288
|
-
...props.rules ? { rules: props.rules } : {}
|
|
1289
|
-
})
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
FormItem.displayName = "FormItem";
|
|
1293
|
-
//#endregion
|
|
1294
|
-
//#region src/Form/Body.tsx
|
|
1295
|
-
function FormBody() {
|
|
1296
|
-
const { items } = useFormContext();
|
|
1297
|
-
return items.map((item) => /* @__PURE__ */ jsx(FormItem, { ...item }, item.name));
|
|
1298
|
-
}
|
|
1299
|
-
FormBody.displayName = "FormBody";
|
|
1300
|
-
//#endregion
|
|
1301
|
-
//#region src/Form/elements/Button.tsx
|
|
1302
|
-
const FormButtonElement = forwardRef(({ children, submit, submitting, ...props }, ref) => /* @__PURE__ */ jsx("button", {
|
|
1303
|
-
type: "button",
|
|
1304
|
-
disabled: submitting,
|
|
1305
|
-
onClick: submit,
|
|
1306
|
-
...props,
|
|
1307
|
-
ref,
|
|
1308
|
-
children
|
|
1309
|
-
}));
|
|
1310
|
-
FormButtonElement.displayName = "FormButtonElement";
|
|
1311
|
-
//#endregion
|
|
1312
|
-
//#region src/Form/elements/Input.tsx
|
|
1313
|
-
const FormInputElement = forwardRef(({ onChange, ...props }, ref) => /* @__PURE__ */ jsx("input", {
|
|
1314
|
-
...props,
|
|
1315
|
-
onChange: (e) => onChange(e.target.value),
|
|
1316
|
-
ref
|
|
1317
|
-
}));
|
|
1318
|
-
FormInputElement.displayName = "FormInputElement";
|
|
1319
|
-
//#endregion
|
|
1320
|
-
//#region src/Form/elements/Label.tsx
|
|
1321
|
-
const FormLabelElement = ({ name, title, description, error, children }) => {
|
|
1322
|
-
return /* @__PURE__ */ jsxs("label", { children: [
|
|
1323
|
-
title ?? name,
|
|
1324
|
-
children,
|
|
1325
|
-
description,
|
|
1326
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
1327
|
-
style: { color: "red" },
|
|
1328
|
-
children: error.message
|
|
1329
|
-
})
|
|
1330
|
-
] });
|
|
1331
|
-
};
|
|
1332
|
-
FormLabelElement.displayName = "FormLabelElement";
|
|
1333
|
-
//#endregion
|
|
1334
|
-
//#region src/Form/elements/index.ts
|
|
1335
|
-
const FormDefaultElements = {
|
|
1336
|
-
Label: FormLabelElement,
|
|
1337
|
-
Input: FormInputElement,
|
|
1338
|
-
Button: FormButtonElement
|
|
1339
|
-
};
|
|
1340
|
-
//#endregion
|
|
1341
|
-
//#region src/Form/rules.ts
|
|
1342
|
-
/**
|
|
1343
|
-
* Default validation rules for a form.
|
|
1344
|
-
*/
|
|
1345
|
-
const FormDefaultRules = {
|
|
1346
|
-
required: async (value, _, lang) => {
|
|
1347
|
-
if (value === null || value === void 0 || value === "" || Number.isNaN(value)) throw Error(lang?.required);
|
|
1348
|
-
},
|
|
1349
|
-
type: async (value, options, lang) => {
|
|
1350
|
-
switch (options) {
|
|
1351
|
-
case "string":
|
|
1352
|
-
if (typeof value !== "string") throw Error(lang?.string);
|
|
1353
|
-
break;
|
|
1354
|
-
case "number":
|
|
1355
|
-
if (Number.isNaN(Number(value))) throw Error(lang?.number);
|
|
1356
|
-
break;
|
|
1357
|
-
}
|
|
1358
|
-
},
|
|
1359
|
-
custom: async (value, options) => {
|
|
1360
|
-
return options(value);
|
|
1361
|
-
}
|
|
1362
|
-
};
|
|
1363
|
-
async function validValues(rules, items, values, lang) {
|
|
1364
|
-
const errors = {};
|
|
1365
|
-
for (const item of items) {
|
|
1366
|
-
const value = values[item.name];
|
|
1367
|
-
const rulesOptions = item.rules;
|
|
1368
|
-
if (rulesOptions) for (const [name, options] of Object.entries(rulesOptions)) try {
|
|
1369
|
-
await rules[name](value, options, lang);
|
|
1370
|
-
} catch (error) {
|
|
1371
|
-
errors[item.name] = error;
|
|
1372
|
-
break;
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
return errors;
|
|
1376
|
-
}
|
|
1377
|
-
//#endregion
|
|
1378
|
-
//#region src/Form/Footer.tsx
|
|
1379
|
-
function FormFooter() {
|
|
1380
|
-
const { submitting, setSubmitting, onSubmit, valuesRef, Elements, items, setErrors, lang, rules } = useFormContext();
|
|
1381
|
-
const Button = Elements.Button;
|
|
1382
|
-
return /* @__PURE__ */ jsx(Button, {
|
|
1383
|
-
submitting,
|
|
1384
|
-
submit: useCallback(async () => {
|
|
1385
|
-
setSubmitting(true);
|
|
1386
|
-
setErrors({});
|
|
1387
|
-
const errors = await validValues(rules, items, valuesRef.current, lang);
|
|
1388
|
-
if (Object.keys(errors).length) {
|
|
1389
|
-
setErrors(errors);
|
|
1390
|
-
setSubmitting(false);
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
onSubmit(valuesRef.current).finally(() => setSubmitting(false));
|
|
1394
|
-
}, [
|
|
1395
|
-
setSubmitting,
|
|
1396
|
-
setErrors,
|
|
1397
|
-
rules,
|
|
1398
|
-
items,
|
|
1399
|
-
valuesRef,
|
|
1400
|
-
lang,
|
|
1401
|
-
onSubmit
|
|
1402
|
-
]),
|
|
1403
|
-
children: lang.submit
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
FormFooter.displayName = "FormFooter";
|
|
1407
|
-
//#endregion
|
|
1408
|
-
//#region src/Form/lang.ts
|
|
1409
|
-
const FormDefaultLang = {
|
|
1410
|
-
submit: "Submit",
|
|
1411
|
-
required: "This field is required",
|
|
1412
|
-
string: "This field must be a string",
|
|
1413
|
-
number: "This field must be a number"
|
|
1414
|
-
};
|
|
1415
|
-
//#endregion
|
|
1416
|
-
//#region src/Form/Container.tsx
|
|
1417
|
-
function mergeValues(items, defaultValues = {}) {
|
|
1418
|
-
const values = {};
|
|
1419
|
-
for (const item of items) values[item.name] = defaultValues[item.name] ?? "";
|
|
1420
|
-
return values;
|
|
1421
|
-
}
|
|
1422
|
-
/**
|
|
1423
|
-
* Render a form with context, default elements, and validation state.
|
|
1424
|
-
*
|
|
1425
|
-
* `FormContainer` merges provided elements, language strings, and rules with
|
|
1426
|
-
* the package defaults, then exposes them through form context.
|
|
1427
|
-
*
|
|
1428
|
-
* @template Values - The type of form values, defaults to Record<string, any>.
|
|
1429
|
-
* @template FormElements - The type of form elements, defaults to FormElementTypes.
|
|
1430
|
-
* @template Rules - The type of form rules, defaults to FormDefaultRules.
|
|
1431
|
-
* @param props - Form items and optional overrides for defaults, language, rules, and submit behavior.
|
|
1432
|
-
* @returns React form container with shared form context.
|
|
1433
|
-
*
|
|
1434
|
-
* @example
|
|
1435
|
-
* ```tsx
|
|
1436
|
-
* import { Form } from '@faasjs/react'
|
|
1437
|
-
*
|
|
1438
|
-
* function MyForm() {
|
|
1439
|
-
* return (
|
|
1440
|
-
* <Form
|
|
1441
|
-
* items={[
|
|
1442
|
-
* { name: 'name' },
|
|
1443
|
-
* ]}
|
|
1444
|
-
* />
|
|
1445
|
-
* )
|
|
1446
|
-
* }
|
|
1447
|
-
* ```
|
|
1448
|
-
*/
|
|
1449
|
-
function FormContainer({ defaultValues, Elements, rules, lang, items, ...props }) {
|
|
1450
|
-
const [values, setValues, valuesRef] = useStateRef(mergeValues(items, defaultValues));
|
|
1451
|
-
return /* @__PURE__ */ jsxs(FormContextProvider, {
|
|
1452
|
-
initializeStates: {
|
|
1453
|
-
errors: {},
|
|
1454
|
-
submitting: false
|
|
1455
|
-
},
|
|
1456
|
-
value: {
|
|
1457
|
-
Elements: Object.assign(FormDefaultElements, Elements),
|
|
1458
|
-
lang: Object.assign(FormDefaultLang, lang),
|
|
1459
|
-
rules: Object.assign(FormDefaultRules, rules),
|
|
1460
|
-
items,
|
|
1461
|
-
values,
|
|
1462
|
-
setValues,
|
|
1463
|
-
valuesRef,
|
|
1464
|
-
...props
|
|
1465
|
-
},
|
|
1466
|
-
memo: true,
|
|
1467
|
-
children: [/* @__PURE__ */ jsx(FormBody, {}), /* @__PURE__ */ jsx(FormFooter, {})]
|
|
1468
|
-
});
|
|
1469
|
-
}
|
|
1470
|
-
FormContainer.displayName = "FormContainer";
|
|
1471
|
-
//#endregion
|
|
1472
|
-
//#region src/OptionalWrapper.tsx
|
|
1473
|
-
/**
|
|
1474
|
-
* A wrapper component that conditionally wraps its children with a provided wrapper component.
|
|
1475
|
-
*
|
|
1476
|
-
* @example
|
|
1477
|
-
* ```tsx
|
|
1478
|
-
* import { OptionalWrapper } from '@faasjs/react'
|
|
1479
|
-
*
|
|
1480
|
-
* const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
1481
|
-
* <div className='wrapper'>{children}</div>
|
|
1482
|
-
* )
|
|
1483
|
-
*
|
|
1484
|
-
* const App = () => (
|
|
1485
|
-
* <OptionalWrapper condition={true} Wrapper={Wrapper}>
|
|
1486
|
-
* <span>Test</span>
|
|
1487
|
-
* </OptionalWrapper>
|
|
1488
|
-
* )
|
|
1489
|
-
* ```
|
|
1490
|
-
*/
|
|
1491
|
-
function OptionalWrapper({ condition, Wrapper, wrapperProps, children }) {
|
|
1492
|
-
if (condition) return /* @__PURE__ */ jsx(Wrapper, {
|
|
1493
|
-
...wrapperProps,
|
|
1494
|
-
children
|
|
1495
|
-
});
|
|
1496
|
-
return children;
|
|
1497
|
-
}
|
|
1498
|
-
OptionalWrapper.displayName = "OptionalWrapper";
|
|
1499
|
-
//#endregion
|
|
1500
1596
|
//#region src/useFaasStream.tsx
|
|
1501
1597
|
/**
|
|
1502
1598
|
* Stream a FaasJS response into React state.
|
|
1503
1599
|
*
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1600
|
+
* `useFaasStream` is the default hook for streaming FaasJS responses in React.
|
|
1601
|
+
* It sends a streaming request, appends decoded text chunks to `data`, and
|
|
1602
|
+
* exposes reload helpers for retrying the same action.
|
|
1506
1603
|
*
|
|
1507
1604
|
* @param action - Action path to invoke.
|
|
1508
1605
|
* @param defaultParams - Params used for the initial request and future reloads.
|
|
1509
1606
|
* @param options - Optional hook configuration such as controlled data, debounce, and skip logic.
|
|
1510
|
-
* @
|
|
1607
|
+
* @param options.params - Request params override used without mutating the hook's stored params state.
|
|
1608
|
+
* @param options.data - Controlled stream text used instead of the hook's internal state.
|
|
1609
|
+
* @param options.setData - Controlled setter used instead of the hook's internal `setData`.
|
|
1610
|
+
* @param options.skip - Boolean or predicate that suppresses the automatic request until `reload()` runs.
|
|
1611
|
+
* @param options.debounce - Milliseconds to wait before sending the latest request.
|
|
1612
|
+
* @param options.baseUrl - Base URL override used for this hook instance.
|
|
1613
|
+
* @returns Streaming request state and helper methods described by {@link UseFaasStreamResult}.
|
|
1511
1614
|
*
|
|
1512
1615
|
* @example
|
|
1513
1616
|
* ```tsx
|
|
1514
|
-
* import { useState } from 'react'
|
|
1515
1617
|
* import { useFaasStream } from '@faasjs/react'
|
|
1516
1618
|
*
|
|
1517
|
-
* function Chat() {
|
|
1518
|
-
* const
|
|
1519
|
-
* const { data, loading, reload } = useFaasStream('chat', { prompt })
|
|
1619
|
+
* function Chat({ prompt }: { prompt: string }) {
|
|
1620
|
+
* const { data, error, loading, reload } = useFaasStream('/pages/chat/stream', { prompt })
|
|
1520
1621
|
*
|
|
1521
|
-
* return
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1525
|
-
* <div>
|
|
1526
|
-
*
|
|
1527
|
-
*
|
|
1622
|
+
* if (loading) return <div>Streaming...</div>
|
|
1623
|
+
*
|
|
1624
|
+
* if (error) {
|
|
1625
|
+
* return (
|
|
1626
|
+
* <div>
|
|
1627
|
+
* <div>Stream failed: {error.message}</div>
|
|
1628
|
+
* <button type="button" onClick={() => reload()}>
|
|
1629
|
+
* Retry
|
|
1630
|
+
* </button>
|
|
1631
|
+
* </div>
|
|
1632
|
+
* )
|
|
1633
|
+
* }
|
|
1634
|
+
*
|
|
1635
|
+
* return <pre>{data}</pre>
|
|
1528
1636
|
* }
|
|
1529
1637
|
* ```
|
|
1530
1638
|
*/
|
|
@@ -1656,6 +1764,17 @@ function useFaasStream(action, defaultParams, options = {}) {
|
|
|
1656
1764
|
* @template T - The type of the value.
|
|
1657
1765
|
* @param value - The current value to track.
|
|
1658
1766
|
* @returns Previous value from the prior render, or `undefined` on the first render.
|
|
1767
|
+
*
|
|
1768
|
+
* @example
|
|
1769
|
+
* ```tsx
|
|
1770
|
+
* import { usePrevious } from '@faasjs/react'
|
|
1771
|
+
*
|
|
1772
|
+
* function Counter({ count }: { count: number }) {
|
|
1773
|
+
* const previous = usePrevious(count)
|
|
1774
|
+
*
|
|
1775
|
+
* return <span>{previous} -> {count}</span>
|
|
1776
|
+
* }
|
|
1777
|
+
* ```
|
|
1659
1778
|
*/
|
|
1660
1779
|
function usePrevious(value) {
|
|
1661
1780
|
const ref = useRef(void 0);
|
|
@@ -1665,4 +1784,42 @@ function usePrevious(value) {
|
|
|
1665
1784
|
return ref.current;
|
|
1666
1785
|
}
|
|
1667
1786
|
//#endregion
|
|
1668
|
-
|
|
1787
|
+
//#region src/useStateRef.ts
|
|
1788
|
+
/**
|
|
1789
|
+
* Custom hook that returns a stateful value and a ref to that value.
|
|
1790
|
+
*
|
|
1791
|
+
* @template T - The type of the value.
|
|
1792
|
+
* @param initialValue - Initial state value. When omitted, state starts as `null`.
|
|
1793
|
+
* @returns Tuple containing the current state, the state setter, and a ref that always points at the latest state.
|
|
1794
|
+
*
|
|
1795
|
+
* @example
|
|
1796
|
+
* ```tsx
|
|
1797
|
+
* import { useStateRef } from '@faasjs/react'
|
|
1798
|
+
*
|
|
1799
|
+
* function MyComponent() {
|
|
1800
|
+
* const [value, setValue, ref] = useStateRef(0)
|
|
1801
|
+
*
|
|
1802
|
+
* return (
|
|
1803
|
+
* <div>
|
|
1804
|
+
* <p>Value: {value}</p>
|
|
1805
|
+
* <button onClick={() => setValue(value + 1)}>Increment</button>
|
|
1806
|
+
* <button onClick={() => console.log(ref.current)}>Submit</button>
|
|
1807
|
+
* </div>
|
|
1808
|
+
* )
|
|
1809
|
+
* }
|
|
1810
|
+
* ```
|
|
1811
|
+
*/
|
|
1812
|
+
function useStateRef(initialValue) {
|
|
1813
|
+
const [state, setState] = useState(initialValue ?? null);
|
|
1814
|
+
const ref = useRef(state);
|
|
1815
|
+
useEffect(() => {
|
|
1816
|
+
ref.current = state;
|
|
1817
|
+
}, [state]);
|
|
1818
|
+
return [
|
|
1819
|
+
state,
|
|
1820
|
+
setState,
|
|
1821
|
+
ref
|
|
1822
|
+
];
|
|
1823
|
+
}
|
|
1824
|
+
//#endregion
|
|
1825
|
+
export { ErrorBoundary, FaasBrowserClient, FaasDataWrapper, FaasReactClient, OptionalWrapper, Response, ResponseError, createSplittingContext, equal, faas, generateId, getClient, setMock, useConstant, useEqualCallback, useEqualEffect, useEqualMemo, useEqualMemoize, useFaas, useFaasStream, usePrevious, useSplittingState, useStateRef, withFaasData };
|