@fougere/react 0.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fougere contributors
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.
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@fougere/react` — the four primitives, in React, for any host that renders it.
3
+ *
4
+ * It was extracted from `@fougere/next` once a second React host was on the table,
5
+ * and the extraction was cheap for a measurable reason: the hooks never imported
6
+ * Next. They reach `@fougere/app/client` and `react`, and talk to the server over
7
+ * `fetch` on same-origin paths — which is all a React app has in common with
8
+ * another React app.
9
+ *
10
+ * The Vue side has no counterpart on purpose. Nuxt's composables lean on
11
+ * `useAsyncData`, `useRequestFetch`, `useState` and `refreshNuxtData` — Nuxt's data
12
+ * layer, not Vue's — so a `@fougere/vue` would have to rebuild what Nuxt gives for
13
+ * free, and there is no second Vue host asking for it.
14
+ */
15
+ export { useQuery, useCommand } from './useFougereData.js';
16
+ export { useFormFor, type FormOptions } from './useFormFor.js';
17
+ export { useCurrentUser, FougereSession } from './useCurrentUser.js';
18
+ export { fetcher, CALL_ENDPOINT } from './transport.js';
19
+ export type { CallInput, EntityClass, FormEntity, FormField, SessionView, } from '@fougere/app/client';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAExD,YAAY,EACV,SAAS,EACT,WAAW,EACX,UAAU,EACV,SAAS,EACT,WAAW,GACZ,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+ /**
3
+ * `@fougere/react` — the four primitives, in React, for any host that renders it.
4
+ *
5
+ * It was extracted from `@fougere/next` once a second React host was on the table,
6
+ * and the extraction was cheap for a measurable reason: the hooks never imported
7
+ * Next. They reach `@fougere/app/client` and `react`, and talk to the server over
8
+ * `fetch` on same-origin paths — which is all a React app has in common with
9
+ * another React app.
10
+ *
11
+ * The Vue side has no counterpart on purpose. Nuxt's composables lean on
12
+ * `useAsyncData`, `useRequestFetch`, `useState` and `refreshNuxtData` — Nuxt's data
13
+ * layer, not Vue's — so a `@fougere/vue` would have to rebuild what Nuxt gives for
14
+ * free, and there is no second Vue host asking for it.
15
+ */
16
+ export { useQuery, useCommand } from './useFougereData.js';
17
+ export { useFormFor } from './useFormFor.js';
18
+ export { useCurrentUser, FougereSession } from './useCurrentUser.js';
19
+ export { fetcher, CALL_ENDPOINT } from './transport.js';
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAoB,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Kept as a re-export: the fetcher and the revalidation bus started here, and moving
3
+ * them into `@fougere/app/client` is what a second non-Nuxt client revealed — none
4
+ * of it was React. This file is the seam that used to hide that.
5
+ */
6
+ export { fetcher, onRefetch, revalidate, CALL_ENDPOINT } from '@fougere/app/client';
7
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA;;;;GAIG;AACH,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,8 @@
1
+ 'use client';
2
+ /**
3
+ * Kept as a re-export: the fetcher and the revalidation bus started here, and moving
4
+ * them into `@fougere/app/client` is what a second non-Nuxt client revealed — none
5
+ * of it was React. This file is the seam that used to hide that.
6
+ */
7
+ export { fetcher, onRefetch, revalidate, CALL_ENDPOINT } from '@fougere/app/client';
8
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;GAIG;AACH,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The session: resolved once server-side, handed down through the provider, read
3
+ * here as state. `refresh()` re-reads it after a client-side auth change (login,
4
+ * logout) — no page reload, no hand-rolled /api/me.
5
+ *
6
+ * `session` is the whole view (extensible context); `user` is its everyday
7
+ * projection. Nuxt hydrates through its own `useState` payload; React has no
8
+ * ambient equivalent, so a layout that already resolved the session passes it to
9
+ * `<FougereSession>` and the page arrives knowing its user. Without the provider
10
+ * the hook fetches once, which is correct and one round-trip slower.
11
+ */
12
+ import { type ReactNode } from 'react';
13
+ import type { SessionView } from '@fougere/app/client';
14
+ export declare function FougereSession({ value, children }: {
15
+ value: SessionView;
16
+ children: ReactNode;
17
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<SessionView | null>>;
18
+ export declare function useCurrentUser<TUser = Record<string, unknown>>(): {
19
+ session: SessionView;
20
+ user: TUser | null;
21
+ loggedIn: boolean;
22
+ refresh: () => Promise<void>;
23
+ };
24
+ //# sourceMappingURL=useCurrentUser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCurrentUser.d.ts","sourceRoot":"","sources":["../src/useCurrentUser.ts"],"names":[],"mappings":"AACA;;;;;;;;;;GAUG;AACH,OAAO,EAA8E,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACnH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+FAE9F;AAED,wBAAgB,cAAc,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;UAe1B,KAAK,GAAG,IAAI;;mBAXR,OAAO,CAAC,IAAI,CAAC;EAepD"}
@@ -0,0 +1,36 @@
1
+ 'use client';
2
+ /**
3
+ * The session: resolved once server-side, handed down through the provider, read
4
+ * here as state. `refresh()` re-reads it after a client-side auth change (login,
5
+ * logout) — no page reload, no hand-rolled /api/me.
6
+ *
7
+ * `session` is the whole view (extensible context); `user` is its everyday
8
+ * projection. Nuxt hydrates through its own `useState` payload; React has no
9
+ * ambient equivalent, so a layout that already resolved the session passes it to
10
+ * `<FougereSession>` and the page arrives knowing its user. Without the provider
11
+ * the hook fetches once, which is correct and one round-trip slower.
12
+ */
13
+ import { createContext, createElement, useCallback, useContext, useEffect, useState } from 'react';
14
+ const SessionContext = createContext(null);
15
+ export function FougereSession({ value, children }) {
16
+ return createElement(SessionContext.Provider, { value }, children);
17
+ }
18
+ export function useCurrentUser() {
19
+ const hydrated = useContext(SessionContext);
20
+ const [session, setSession] = useState(hydrated ?? { user: null });
21
+ const refresh = useCallback(async () => {
22
+ const response = await fetch('/_fougere/session');
23
+ setSession((await response.json()));
24
+ }, []);
25
+ useEffect(() => {
26
+ if (!hydrated)
27
+ void refresh();
28
+ }, [hydrated, refresh]);
29
+ return {
30
+ session,
31
+ user: (session.user ?? null),
32
+ loggedIn: session.user != null,
33
+ refresh,
34
+ };
35
+ }
36
+ //# sourceMappingURL=useCurrentUser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCurrentUser.js","sourceRoot":"","sources":["../src/useCurrentUser.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;;;;;;;GAUG;AACH,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAGnH,MAAM,cAAc,GAAG,aAAa,CAAqB,IAAI,CAAC,CAAC;AAE/D,MAAM,UAAU,cAAc,CAAC,EAAE,KAAK,EAAE,QAAQ,EAA+C;IAC7F,OAAO,aAAa,CAAC,cAAc,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,EAAE,QAAQ,CAAC,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,MAAM,QAAQ,GAAG,UAAU,CAAC,cAAc,CAAC,CAAC;IAC5C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAc,QAAQ,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAEhF,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACpD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAClD,UAAU,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAgB,CAAC,CAAC;IACrD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,QAAQ;YAAE,KAAK,OAAO,EAAE,CAAC;IAChC,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAExB,OAAO;QACL,OAAO;QACP,IAAI,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAiB;QAC5C,QAAQ,EAAE,OAAO,CAAC,IAAI,IAAI,IAAI;QAC9B,OAAO;KACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { FougereError } from '@fougere/core/contract';
2
+ import { type FormEntity, type FormField } from '@fougere/app/client';
3
+ export interface FormOptions {
4
+ /** Command the submit rides. Default: 'create'. */
5
+ op?: string;
6
+ /** Initial values (edit mode: the loaded entity). */
7
+ initial?: Record<string, unknown>;
8
+ /** Call params designating the target (edit mode: { id }). */
9
+ params?: Record<string, string>;
10
+ }
11
+ export declare function useFormFor<T = Record<string, unknown>>(entity: FormEntity, options?: FormOptions): {
12
+ fields: FormField[];
13
+ /**
14
+ * The same fields, keyed by name — a form that lays its inputs out by hand binds
15
+ * one at a time (`{...fieldsByName.email.attrs}`) and still states no rule of its
16
+ * own. Without it, a page retypes `type="email"` next to a card that already says
17
+ * `format: 'email'`, and the browser enforces the page rather than the declaration.
18
+ */
19
+ fieldsByName: Record<string, FormField>;
20
+ values: Record<string, unknown>;
21
+ setValue: (name: string, value: unknown) => void;
22
+ errors: Record<string, string>;
23
+ submit: () => Promise<T | null>;
24
+ loading: boolean;
25
+ /** Non-validation failure of the last submit (unreachable host, conflict…). */
26
+ error: FougereError | null;
27
+ valid: boolean;
28
+ };
29
+ //# sourceMappingURL=useFormFor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFormFor.d.ts","sourceRoot":"","sources":["../src/useFormFor.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,YAAY,EAAa,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAML,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,WAAW;IAC1B,mDAAmD;IACnD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,wBAAgB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,WAAgB;;IAkDjG;;;;;OAKG;;;qBAzC+B,MAAM,SAAS,OAAO;;kBAgBrB,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;;IAgCpD,+EAA+E;;;EAIlF"}
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+ /**
3
+ * The form contract — state, validation, submission, error mapping. Never a
4
+ * widget: the page owns the rendering, this owns the mechanics.
5
+ *
6
+ * The cycle composes the other primitives: fields from the io axes, local
7
+ * pre-judgment with the same rules the handler enforces (one declaration, both
8
+ * sides), submission through the command (so the entity link revalidates mounted
9
+ * queries), and per-field errors in the same `{ path, message }` shape whoever
10
+ * judged.
11
+ *
12
+ * One visible difference from the Vue composable, and it is React's, not ours:
13
+ * Vue hands back a reactive object a template mutates in place, React state is
14
+ * replaced rather than mutated, so `setValue` is part of the contract here.
15
+ */
16
+ import { useCallback, useMemo, useState } from 'react';
17
+ import { FougereError, ErrorCode } from '@fougere/core/contract';
18
+ import { entityKeyOf, errorsByField, formFieldsOf, payloadOf, } from '@fougere/app/client';
19
+ import { useCommand } from './useFougereData.js';
20
+ export function useFormFor(entity, options = {}) {
21
+ const entityKey = entityKeyOf(entity);
22
+ const fields = useMemo(() => formFieldsOf(entity, entityKey), [entity, entityKey]);
23
+ // `initial` wins over the declared default: editing a row shows the row, including
24
+ // a value the author deliberately changed away from that default. On a create form
25
+ // there is no `initial`, so the field opens on what is about to be written — the
26
+ // schema's own literal, shown rather than guessed by the page.
27
+ const [values, setValues] = useState(() => Object.fromEntries(fields.map((field) => [field.name, options.initial?.[field.name] ?? field.default])));
28
+ const [errors, setErrors] = useState({});
29
+ const command = useCommand(entity, options.op ?? 'create');
30
+ const setValue = useCallback((name, value) => {
31
+ setValues((current) => ({ ...current, [name]: value }));
32
+ }, []);
33
+ /** Local pre-judgment — same rules as the handler, saves a lost round-trip. */
34
+ const judge = useCallback(() => {
35
+ const result = entity.validate(payloadOf(values));
36
+ setErrors(result.success ? {} : errorsByField(result.errors));
37
+ return result.success;
38
+ }, [entity, values]);
39
+ /**
40
+ * Judge locally, then send through the command. Returns the created/updated value,
41
+ * or null when a judge (either side) rejected — the errors land per field either
42
+ * way, the form never knows who judged.
43
+ */
44
+ const submit = useCallback(async () => {
45
+ if (!judge())
46
+ return null;
47
+ try {
48
+ return await command.execute({ params: options.params, body: payloadOf(values) });
49
+ }
50
+ catch (err) {
51
+ if (err instanceof FougereError && err.code === ErrorCode.VALIDATION_FAILED && Array.isArray(err.details)) {
52
+ setErrors(errorsByField(err.details));
53
+ return null;
54
+ }
55
+ throw err;
56
+ }
57
+ }, [judge, command, options.params, values]);
58
+ const fieldsByName = useMemo(() => Object.fromEntries(fields.map((field) => [field.name, field])), [fields]);
59
+ return {
60
+ fields,
61
+ /**
62
+ * The same fields, keyed by name — a form that lays its inputs out by hand binds
63
+ * one at a time (`{...fieldsByName.email.attrs}`) and still states no rule of its
64
+ * own. Without it, a page retypes `type="email"` next to a card that already says
65
+ * `format: 'email'`, and the browser enforces the page rather than the declaration.
66
+ */
67
+ fieldsByName,
68
+ values,
69
+ setValue,
70
+ errors,
71
+ submit,
72
+ loading: command.loading,
73
+ /** Non-validation failure of the last submit (unreachable host, conflict…). */
74
+ error: command.error,
75
+ valid: Object.keys(errors).length === 0,
76
+ };
77
+ }
78
+ //# sourceMappingURL=useFormFor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFormFor.js","sourceRoot":"","sources":["../src/useFormFor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { FougereError } from '@fougere/core/contract';
2
+ import { type CallInput, type EntityClass } from '@fougere/app/client';
3
+ export declare function useQuery<T = Record<string, unknown>>(entity: EntityClass, op: string, input?: CallInput, opts?: {
4
+ immediate?: boolean;
5
+ }): {
6
+ data: T | null;
7
+ items: T[];
8
+ total: number | undefined;
9
+ hasMore: boolean | undefined;
10
+ loading: boolean;
11
+ error: FougereError | null;
12
+ refresh: () => Promise<void>;
13
+ };
14
+ export declare function useCommand<T = unknown>(entity: EntityClass, op: string): {
15
+ execute: (input?: CallInput) => Promise<T>;
16
+ loading: boolean;
17
+ error: FougereError | null;
18
+ };
19
+ //# sourceMappingURL=useFougereData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFougereData.d.ts","sourceRoot":"","sources":["../src/useFougereData.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAWL,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,qBAAqB,CAAC;AAG7B,wBAAgB,QAAQ,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClD,MAAM,EAAE,WAAW,EACnB,EAAE,EAAE,MAAM,EACV,KAAK,CAAC,EAAE,SAAS,EACjB,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;;;mBAYQ,OAAO,CAAC,IAAI,CAAC;EAiCpD;AAED,wBAAgB,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM;sBAMpD,SAAS,KAAG,OAAO,CAAC,CAAC,CAAC;;;EAqBxC"}
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+ /**
3
+ * The couple — useQuery (reads) and useCommand (writes), the two dual gestures of
4
+ * a page talking to a Frond, in React.
5
+ *
6
+ * What the gestures ARE lives in `@fougere/app/client` and is shared with Vue:
7
+ * designation is class + verb, the cache key derives from designation plus input,
8
+ * and a successful command on an entity revalidates every mounted query on that
9
+ * entity. Nothing of that is restated here — this file is `useState` for the
10
+ * values, `useEffect` for the mount, and the bus in `transport.ts` where Nuxt
11
+ * calls `refreshNuxtData`.
12
+ */
13
+ import { useCallback, useEffect, useState } from 'react';
14
+ import { asFougereError, callOf, entityKeyOf, invocationOf, itemsOf, mountedKeys, pageOf, queryKeyOf, sendCall, trackQuery, } from '@fougere/app/client';
15
+ import { fetcher, onRefetch, revalidate } from './transport.js';
16
+ export function useQuery(entity, op, input, opts) {
17
+ const entityKey = entityKeyOf(entity);
18
+ // The key IS the dependency: an input literal is a new object on every render, so
19
+ // depending on it directly would refetch forever. Its serialization is stable.
20
+ const key = queryKeyOf(entityKey, op, input);
21
+ const immediate = opts?.immediate !== false;
22
+ const [data, setData] = useState(null);
23
+ const [loading, setLoading] = useState(immediate);
24
+ const [error, setError] = useState(null);
25
+ const refresh = useCallback(async () => {
26
+ setLoading(true);
27
+ setError(null);
28
+ try {
29
+ setData((await sendCall(fetcher, callOf(entity, op), invocationOf(input))));
30
+ }
31
+ catch (err) {
32
+ setError(asFougereError(err, entityKey, op));
33
+ }
34
+ finally {
35
+ setLoading(false);
36
+ }
37
+ // `key` stands for (entity, op, input) — see above.
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
39
+ }, [key]);
40
+ useEffect(() => {
41
+ const untrack = trackQuery(entityKey, key);
42
+ const off = onRefetch(key, () => void refresh());
43
+ if (immediate)
44
+ void refresh();
45
+ return () => {
46
+ untrack();
47
+ off();
48
+ };
49
+ }, [entityKey, key, immediate, refresh]);
50
+ return {
51
+ data,
52
+ items: itemsOf(data),
53
+ total: pageOf(data).total,
54
+ hasMore: pageOf(data).hasMore,
55
+ loading,
56
+ error,
57
+ refresh,
58
+ };
59
+ }
60
+ export function useCommand(entity, op) {
61
+ const entityKey = entityKeyOf(entity);
62
+ const [loading, setLoading] = useState(false);
63
+ const [error, setError] = useState(null);
64
+ const execute = useCallback(async (input) => {
65
+ setLoading(true);
66
+ setError(null);
67
+ try {
68
+ const result = (await sendCall(fetcher, callOf(entity, op), invocationOf(input)));
69
+ // The link: same entity designated on both sides → revalidate its queries.
70
+ revalidate(mountedKeys(entityKey));
71
+ return result;
72
+ }
73
+ catch (err) {
74
+ const failure = asFougereError(err, entityKey, op);
75
+ setError(failure);
76
+ throw failure;
77
+ }
78
+ finally {
79
+ setLoading(false);
80
+ }
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [entityKey, op]);
83
+ return { execute, loading, error };
84
+ }
85
+ //# sourceMappingURL=useFougereData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFougereData.js","sourceRoot":"","sources":["../src/useFougereData.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AACb;;;;;;;;;;GAUG;AACH,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEzD,OAAO,EACL,cAAc,EACd,MAAM,EACN,WAAW,EACX,YAAY,EACZ,OAAO,EACP,WAAW,EACX,MAAM,EACN,UAAU,EACV,QAAQ,EACR,UAAU,GAGX,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEhE,MAAM,UAAU,QAAQ,CACtB,MAAmB,EACnB,EAAU,EACV,KAAiB,EACjB,IAA8B;IAE9B,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,kFAAkF;IAClF,+EAA+E;IAC/E,MAAM,GAAG,GAAG,UAAU,CAAC,SAAS,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,IAAI,EAAE,SAAS,KAAK,KAAK,CAAC;IAE5C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAW,IAAI,CAAC,CAAC;IACjD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAC;IAE9D,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAmB,EAAE;QACpD,UAAU,CAAC,IAAI,CAAC,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,IAAI,CAAC;YACH,OAAO,CAAC,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAM,CAAC,CAAC;QACnF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,QAAQ,CAAC,cAAc,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QAC/C,CAAC;gBAAS,CAAC;YACT,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QACD,oDAAoD;QACpD,uDAAuD;IACzD,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAEV,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;QACjD,IAAI,SAAS;YAAE,KAAK,OAAO,EAAE,CAAC;QAC9B,OAAO,GAAG,EAAE;YACV,OAAO,EAAE,CAAC;YACV,GAAG,EAAE,CAAC;QACR,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IAEzC,OAAO;QACL,IAAI;QACJ,KAAK,EAAE,OAAO,CAAI,IAAI,CAAC;QACvB,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK;QACzB,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO;QAC7B,OAAO;QACP,KAAK;QACL,OAAO;KACR,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAc,MAAmB,EAAE,EAAU;IACrE,MAAM,SAAS,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC9C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAC;IAE9D,MAAM,OAAO,GAAG,WAAW,CACzB,KAAK,EAAE,KAAiB,EAAc,EAAE;QACtC,UAAU,CAAC,IAAI,CAAC,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC,CAAC;QACf,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,CAAC,MAAM,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC,CAAM,CAAC;YACvF,2EAA2E;YAC3E,UAAU,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC;YACnC,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC;YACnD,QAAQ,CAAC,OAAO,CAAC,CAAC;YAClB,MAAM,OAAO,CAAC;QAChB,CAAC;gBAAS,CAAC;YACT,UAAU,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QACD,uDAAuD;IACzD,CAAC,EACD,CAAC,SAAS,EAAE,EAAE,CAAC,CAChB,CAAC;IAEF,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AACrC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@fougere/react",
3
+ "version": "0.2.0-alpha.0",
4
+ "description": "Fougere's four primitives as React hooks — host-independent.",
5
+ "keywords": [
6
+ "fougere",
7
+ "react",
8
+ "hooks",
9
+ "typescript",
10
+ "fullstack",
11
+ "rpc"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/chok/fougere.git",
17
+ "directory": "packages/app/react"
18
+ },
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "dependencies": {
32
+ "@fougere/app": "0.2.0-alpha.0",
33
+ "@fougere/core": "0.2.0-alpha.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "^19.2.2",
37
+ "react": "19.2.8",
38
+ "vitest": "^4.1.0",
39
+ "@testing-library/react": "^16.3.0",
40
+ "jsdom": "^27.0.0",
41
+ "react-dom": "19.2.8",
42
+ "@fougere/schema": "0.2.0-alpha.0"
43
+ },
44
+ "peerDependencies": {
45
+ "react": "^18.3.0 || ^19.0.0"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "rm -rf dist && tsc",
52
+ "test": "vitest run",
53
+ "typecheck": "tsc --noEmit -p tsconfig.test.json"
54
+ }
55
+ }