@lyeve-labs/client-react 0.2.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 LyEve Labs
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,211 @@
1
+ # @lyeve/cms-client-react
2
+
3
+ React hooks for the LyEve Core. Typed, reactive data fetching built on
4
+ `@lyeve/cms-client`.
5
+
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![React](https://img.shields.io/badge/React-18+-61dafb.svg)](https://react.dev)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6.svg)](https://www.typescriptlang.org)
9
+
10
+ ```bash
11
+ pnpm add @lyeve/cms-client @lyeve/cms-client-react
12
+ ```
13
+
14
+ ```tsx
15
+ import { CmsProvider, useQuery, useMutation } from "@lyeve/cms-client-react";
16
+ import { getSchemas, createSchema } from "@lyeve/cms-client-rest";
17
+
18
+ function App() {
19
+ return (
20
+ <CmsProvider config={{ baseUrl: "https://cms.example.com" }}>
21
+ <SchemaManager />
22
+ </CmsProvider>
23
+ );
24
+ }
25
+
26
+ function SchemaManager() {
27
+ const { data, loading } = useQuery((client) => getSchemas(client));
28
+ const [create, { loading: creating }] = useMutation(
29
+ (client, vars: { name: string }) => createSchema(client, vars),
30
+ );
31
+ // ...
32
+ }
33
+ ```
34
+
35
+ One provider at the root, typed hooks everywhere else. No boilerplate.
36
+
37
+ ---
38
+
39
+ ## What's in the box
40
+
41
+ - **CmsProvider:** context provider that wraps your component tree with CMS client
42
+ configuration. `getHeaders` is called on every request so auth tokens stay fresh.
43
+ - **useQuery:** reactive data fetching hook. Runs on mount, returns `{ data, error,
44
+ loading, refetch }`. Preserves existing data on fetch errors to prevent UI flash.
45
+ - **useMutation:** mutation hook returning `[trigger, state]`. Loading/error/data
46
+ tracked per invocation.
47
+ - **Stale-closure safe:** client re-creates when `baseUrl` or `getHeaders` change,
48
+ so hooks always see the latest config.
49
+
50
+ ## Requirements
51
+
52
+ - **Node 20** or newer
53
+ - **React 18** or newer
54
+ - **[@lyeve/cms-client](https://www.npmjs.com/package/@lyeve/cms-client)** `>=0.1.0`
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pnpm add @lyeve/cms-client @lyeve/cms-client-react
60
+ # or npm install @lyeve/cms-client @lyeve/cms-client-react
61
+ # or yarn add @lyeve/cms-client @lyeve/cms-client-react
62
+ ```
63
+
64
+ ## Use
65
+
66
+ ### Provider
67
+
68
+ Wrap your app once at the root:
69
+
70
+ ```tsx
71
+ import { CmsProvider } from "@lyeve/cms-client-react";
72
+
73
+ function App() {
74
+ return (
75
+ <CmsProvider
76
+ config={{
77
+ baseUrl: "https://cms.example.com",
78
+ getHeaders: () => ({ Authorization: `Bearer ${getToken()}` }),
79
+ }}
80
+ >
81
+ <YourRoutes />
82
+ </CmsProvider>
83
+ );
84
+ }
85
+ ```
86
+
87
+ ### useQuery
88
+
89
+ ```tsx
90
+ import { useQuery } from "@lyeve/cms-client-react";
91
+ import { getSchemas } from "@lyeve/cms-client-rest";
92
+
93
+ function SchemaList() {
94
+ const { data, error, loading, refetch } = useQuery((client) =>
95
+ getSchemas(client),
96
+ );
97
+
98
+ if (loading) return <div>Loading...</div>;
99
+ if (error) return <div>Error: {error.message}</div>;
100
+
101
+ return (
102
+ <ul>
103
+ {data?.map((s) => (
104
+ <li key={s.id}>{s.name}</li>
105
+ ))}
106
+ </ul>
107
+ );
108
+ }
109
+ ```
110
+
111
+ ### useMutation
112
+
113
+ ```tsx
114
+ import { useMutation } from "@lyeve/cms-client-react";
115
+ import { createSchema } from "@lyeve/cms-client-rest";
116
+
117
+ function CreateForm() {
118
+ const [create, { loading, error }] = useMutation(
119
+ (client, vars: { name: string }) => createSchema(client, vars),
120
+ );
121
+
122
+ async function handleSubmit(e: React.FormEvent) {
123
+ e.preventDefault();
124
+ await create({ name: "articles" });
125
+ }
126
+
127
+ return (
128
+ <form onSubmit={handleSubmit}>
129
+ <button type="submit" disabled={loading}>
130
+ {loading ? "Creating..." : "Create"}
131
+ </button>
132
+ {error && <p>{error.message}</p>}
133
+ </form>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ## API
139
+
140
+ ### CmsProvider
141
+
142
+ ```ts
143
+ interface CmsConfig {
144
+ baseUrl?: string;
145
+ getHeaders?: () => Record<string, string>;
146
+ }
147
+
148
+ <CmsProvider config={config}>{children}</CmsProvider>
149
+ ```
150
+
151
+ ### useQuery
152
+
153
+ ```ts
154
+ function useQuery<T>(
155
+ fetcher: (client: HttpClient) => Promise<T>,
156
+ deps?: unknown[],
157
+ ): {
158
+ data: T | null;
159
+ error: Error | null;
160
+ loading: boolean;
161
+ refetch: () => void;
162
+ };
163
+ ```
164
+
165
+ Runs `fetcher` on mount and whenever `deps` change. Returns `refetch` for manual
166
+ re-execution.
167
+
168
+ ### useMutation
169
+
170
+ ```ts
171
+ function useMutation<T, V>(
172
+ mutator: (client: HttpClient, vars: V) => Promise<T>,
173
+ ): [
174
+ (vars: V) => Promise<T>,
175
+ { data: T | null; error: Error | null; loading: boolean },
176
+ ];
177
+ ```
178
+
179
+ Returns a trigger function and the current state.
180
+
181
+ ## Local development
182
+
183
+ ```bash
184
+ pnpm install # install dependencies
185
+ pnpm test # run unit tests
186
+ pnpm check # type-check
187
+ pnpm build # tsup + publint -> dist/
188
+ ```
189
+
190
+ ## Project layout
191
+
192
+ ```
193
+ src/
194
+ index.tsx # CmsProvider, useQuery, useMutation
195
+ tests/ # vitest test suite
196
+ ```
197
+
198
+ ## Versioning
199
+
200
+ `@lyeve/cms-client-react` follows [SemVer](https://semver.org). While under `1.0`,
201
+ breaking changes bump the **minor** version; additive changes bump the **patch**.
202
+ Every release is logged in [`CHANGELOG.md`](CHANGELOG.md).
203
+
204
+ ## Contributing
205
+
206
+ Bug reports and feature requests are welcome. See
207
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) for the development setup and conventions.
208
+
209
+ ## License
210
+
211
+ MIT. See [`LICENSE`](LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.tsx
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ CmsProvider: () => CmsProvider,
24
+ useMutation: () => useMutation,
25
+ useQuery: () => useQuery
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+ var import_cms_client = require("@lyeve/cms-client");
29
+ var import_react = require("react");
30
+ var import_jsx_runtime = require("react/jsx-runtime");
31
+ var CmsContext = (0, import_react.createContext)(null);
32
+ function CmsProvider({
33
+ config,
34
+ children
35
+ }) {
36
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CmsContext.Provider, { value: config, children });
37
+ }
38
+ function useClient() {
39
+ const config = (0, import_react.useContext)(CmsContext);
40
+ if (!config) {
41
+ throw new Error("CmsProvider must wrap your component tree");
42
+ }
43
+ return (0, import_react.useMemo)(() => {
44
+ const base = config.baseUrl ?? "";
45
+ return (0, import_cms_client.createClient)((url, init) => {
46
+ const fullUrl = typeof url === "string" ? `${base}${url}` : url;
47
+ return fetch(fullUrl, {
48
+ ...init,
49
+ headers: { ...init?.headers, ...config.getHeaders?.() }
50
+ });
51
+ });
52
+ }, [config.baseUrl, config.getHeaders]);
53
+ }
54
+ function useQuery(fetcher, deps = []) {
55
+ const client = useClient();
56
+ const [state, setState] = (0, import_react.useState)({
57
+ data: null,
58
+ error: null,
59
+ loading: true
60
+ });
61
+ const run = (0, import_react.useCallback)(() => {
62
+ setState((s) => ({ ...s, loading: true }));
63
+ fetcher(client).then((data) => setState({ data, error: null, loading: false })).catch(
64
+ (error) => setState((s) => ({ ...s, error, loading: false }))
65
+ );
66
+ }, [client, ...deps]);
67
+ (0, import_react.useEffect)(() => {
68
+ run();
69
+ }, [run]);
70
+ return { ...state, refetch: run };
71
+ }
72
+ function useMutation(mutator) {
73
+ const client = useClient();
74
+ const [state, setState] = (0, import_react.useState)({
75
+ data: null,
76
+ error: null,
77
+ loading: false
78
+ });
79
+ const run = (0, import_react.useCallback)(
80
+ async (vars) => {
81
+ setState((s) => ({ ...s, loading: true }));
82
+ try {
83
+ const data = await mutator(client, vars);
84
+ setState({ data, error: null, loading: false });
85
+ return data;
86
+ } catch (error) {
87
+ setState({ data: null, error, loading: false });
88
+ throw error;
89
+ }
90
+ },
91
+ [client]
92
+ );
93
+ return [run, state];
94
+ }
95
+ // Annotate the CommonJS export names for ESM import in node:
96
+ 0 && (module.exports = {
97
+ CmsProvider,
98
+ useMutation,
99
+ useQuery
100
+ });
101
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"sourcesContent":["/**\n * LyEve CMS React hooks - typed, reactive data fetching.\n *\n * Thin wrapper around {@link @lyeve/cms-client} that makes the client\n * available via context and provides `useQuery` / `useMutation` hooks.\n *\n * @example\n * ```tsx\n * import { CmsProvider, useQuery } from '@lyeve/cms-client-react';\n * import { getSchemas } from '@lyeve/cms-client-rest';\n *\n * function App() {\n * return (\n * <CmsProvider config={{ baseUrl: 'https://cms.example.com', getHeaders: () => ({ Authorization: `Bearer ${token}` }) }}>\n * <SchemaManager />\n * </CmsProvider>\n * );\n * }\n *\n * function SchemaManager() {\n * const { data: schemas, loading } = useQuery((client) => getSchemas(client));\n * // ...\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { createClient, type HttpClient } from '@lyeve/cms-client';\nimport {\n createContext,\n useContext,\n useCallback,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\n// Provider\n\nexport interface CmsConfig {\n /** Base URL prepended to every request path. */\n baseUrl?: string;\n /**\n * Callback returning headers added to every request.\n * Called on every request so auth tokens can be refreshed without\n * recreating the provider.\n */\n getHeaders?: () => Record<string, string>;\n}\n\nconst CmsContext = createContext<CmsConfig | null>(null);\n\n/**\n * Context provider that makes the CMS client available to all descendant\n * hooks.\n *\n * @example\n * ```tsx\n * <CmsProvider config={{ baseUrl: 'https://cms.example.com' }}>\n * <App />\n * </CmsProvider>\n * ```\n */\nexport function CmsProvider({\n config,\n children,\n}: {\n config: CmsConfig;\n children: ReactNode;\n}) {\n return <CmsContext.Provider value={config}>{children}</CmsContext.Provider>;\n}\n\n/**\n * Returns a memoized HttpClient instance configured from the nearest\n * {@link CmsProvider}.\n */\nfunction useClient(): HttpClient {\n const config = useContext(CmsContext);\n if (!config) {\n throw new Error('CmsProvider must wrap your component tree');\n }\n // Re-create when config changes so getHeaders/auth tokens stay current.\n return useMemo(() => {\n const base = config.baseUrl ?? '';\n return createClient((url, init) => {\n const fullUrl = typeof url === 'string' ? `${base}${url}` : url;\n return fetch(fullUrl, {\n ...init,\n headers: { ...init?.headers, ...config.getHeaders?.() },\n });\n });\n }, [config.baseUrl, config.getHeaders]);\n}\n\n// Hooks\n\nexport interface AsyncState<T> {\n data: T | null;\n error: Error | null;\n loading: boolean;\n}\n\n/**\n * Reactive query hook. Runs `fetcher` on mount and whenever `deps` change.\n *\n * @param fetcher - Function that receives the configured HttpClient and returns a promise.\n * @param deps - Optional dependency array controlling when to refetch (default: `[]`).\n *\n * @example\n * ```ts\n * const { data, error, loading, refetch } = useQuery(\n * (client) => getSchemas(client),\n * );\n * ```\n */\nexport function useQuery<T>(\n fetcher: (client: HttpClient) => Promise<T>,\n deps: unknown[] = [],\n): AsyncState<T> & { refetch: () => void } {\n const client = useClient();\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n error: null,\n loading: true,\n });\n\n const run = useCallback(() => {\n setState((s) => ({ ...s, loading: true }));\n\n fetcher(client)\n .then((data) => setState({ data, error: null, loading: false }))\n .catch((error) =>\n setState((s) => ({ ...s, error: error as Error, loading: false })),\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, ...deps]);\n\n useEffect(() => {\n run();\n }, [run]);\n\n return { ...state, refetch: run };\n}\n\n/**\n * Mutation hook. Returns a trigger function and the current state.\n *\n * @param mutator - Function that receives the configured HttpClient and variables.\n *\n * @example\n * ```ts\n * const [create, { loading, error }] = useMutation(\n * (client, vars: { title: string }) => createArticle(client, vars),\n * );\n * // await create({ title: 'Hello' })\n * ```\n */\nexport function useMutation<T, V>(\n mutator: (client: HttpClient, vars: V) => Promise<T>,\n): [(vars: V) => Promise<T>, AsyncState<T>] {\n const client = useClient();\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n error: null,\n loading: false,\n });\n\n const run = useCallback(\n async (vars: V) => {\n setState((s) => ({ ...s, loading: true }));\n try {\n const data = await mutator(client, vars);\n setState({ data, error: null, loading: false });\n return data;\n } catch (error) {\n setState({ data: null, error: error as Error, loading: false });\n throw error;\n }\n },\n [client],\n );\n\n return [run, state];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4BA,wBAA8C;AAC9C,mBAQO;AAmCE;AApBT,IAAM,iBAAa,4BAAgC,IAAI;AAahD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AACF,GAGG;AACD,SAAO,4CAAC,WAAW,UAAX,EAAoB,OAAO,QAAS,UAAS;AACvD;AAMA,SAAS,YAAwB;AAC/B,QAAM,aAAS,yBAAW,UAAU;AACpC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AAEA,aAAO,sBAAQ,MAAM;AACnB,UAAM,OAAO,OAAO,WAAW;AAC/B,eAAO,gCAAa,CAAC,KAAK,SAAS;AACjC,YAAM,UAAU,OAAO,QAAQ,WAAW,GAAG,IAAI,GAAG,GAAG,KAAK;AAC5D,aAAO,MAAM,SAAS;AAAA,QACpB,GAAG;AAAA,QACH,SAAS,EAAE,GAAG,MAAM,SAAS,GAAG,OAAO,aAAa,EAAE;AAAA,MACxD,CAAC;AAAA,IACH,CAAC;AAAA,EACH,GAAG,CAAC,OAAO,SAAS,OAAO,UAAU,CAAC;AACxC;AAuBO,SAAS,SACd,SACA,OAAkB,CAAC,GACsB;AACzC,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAwB;AAAA,IAChD,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,EACX,CAAC;AAED,QAAM,UAAM,0BAAY,MAAM;AAC5B,aAAS,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,KAAK,EAAE;AAEzC,YAAQ,MAAM,EACX,KAAK,CAAC,SAAS,SAAS,EAAE,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,CAAC,EAC9D;AAAA,MAAM,CAAC,UACN,SAAS,CAAC,OAAO,EAAE,GAAG,GAAG,OAAuB,SAAS,MAAM,EAAE;AAAA,IACnE;AAAA,EAEJ,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC;AAEpB,8BAAU,MAAM;AACd,QAAI;AAAA,EACN,GAAG,CAAC,GAAG,CAAC;AAER,SAAO,EAAE,GAAG,OAAO,SAAS,IAAI;AAClC;AAeO,SAAS,YACd,SAC0C;AAC1C,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,OAAO,QAAQ,QAAI,uBAAwB;AAAA,IAChD,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,EACX,CAAC;AAED,QAAM,UAAM;AAAA,IACV,OAAO,SAAY;AACjB,eAAS,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,KAAK,EAAE;AACzC,UAAI;AACF,cAAM,OAAO,MAAM,QAAQ,QAAQ,IAAI;AACvC,iBAAS,EAAE,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC;AAC9C,eAAO;AAAA,MACT,SAAS,OAAO;AACd,iBAAS,EAAE,MAAM,MAAM,OAAuB,SAAS,MAAM,CAAC;AAC9D,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,SAAO,CAAC,KAAK,KAAK;AACpB;","names":[]}
@@ -0,0 +1,66 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { HttpClient } from '@lyeve/cms-client';
4
+
5
+ interface CmsConfig {
6
+ /** Base URL prepended to every request path. */
7
+ baseUrl?: string;
8
+ /**
9
+ * Callback returning headers added to every request.
10
+ * Called on every request so auth tokens can be refreshed without
11
+ * recreating the provider.
12
+ */
13
+ getHeaders?: () => Record<string, string>;
14
+ }
15
+ /**
16
+ * Context provider that makes the CMS client available to all descendant
17
+ * hooks.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <CmsProvider config={{ baseUrl: 'https://cms.example.com' }}>
22
+ * <App />
23
+ * </CmsProvider>
24
+ * ```
25
+ */
26
+ declare function CmsProvider({ config, children, }: {
27
+ config: CmsConfig;
28
+ children: ReactNode;
29
+ }): react.JSX.Element;
30
+ interface AsyncState<T> {
31
+ data: T | null;
32
+ error: Error | null;
33
+ loading: boolean;
34
+ }
35
+ /**
36
+ * Reactive query hook. Runs `fetcher` on mount and whenever `deps` change.
37
+ *
38
+ * @param fetcher - Function that receives the configured HttpClient and returns a promise.
39
+ * @param deps - Optional dependency array controlling when to refetch (default: `[]`).
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const { data, error, loading, refetch } = useQuery(
44
+ * (client) => getSchemas(client),
45
+ * );
46
+ * ```
47
+ */
48
+ declare function useQuery<T>(fetcher: (client: HttpClient) => Promise<T>, deps?: unknown[]): AsyncState<T> & {
49
+ refetch: () => void;
50
+ };
51
+ /**
52
+ * Mutation hook. Returns a trigger function and the current state.
53
+ *
54
+ * @param mutator - Function that receives the configured HttpClient and variables.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const [create, { loading, error }] = useMutation(
59
+ * (client, vars: { title: string }) => createArticle(client, vars),
60
+ * );
61
+ * // await create({ title: 'Hello' })
62
+ * ```
63
+ */
64
+ declare function useMutation<T, V>(mutator: (client: HttpClient, vars: V) => Promise<T>): [(vars: V) => Promise<T>, AsyncState<T>];
65
+
66
+ export { type AsyncState, type CmsConfig, CmsProvider, useMutation, useQuery };
@@ -0,0 +1,66 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { HttpClient } from '@lyeve/cms-client';
4
+
5
+ interface CmsConfig {
6
+ /** Base URL prepended to every request path. */
7
+ baseUrl?: string;
8
+ /**
9
+ * Callback returning headers added to every request.
10
+ * Called on every request so auth tokens can be refreshed without
11
+ * recreating the provider.
12
+ */
13
+ getHeaders?: () => Record<string, string>;
14
+ }
15
+ /**
16
+ * Context provider that makes the CMS client available to all descendant
17
+ * hooks.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <CmsProvider config={{ baseUrl: 'https://cms.example.com' }}>
22
+ * <App />
23
+ * </CmsProvider>
24
+ * ```
25
+ */
26
+ declare function CmsProvider({ config, children, }: {
27
+ config: CmsConfig;
28
+ children: ReactNode;
29
+ }): react.JSX.Element;
30
+ interface AsyncState<T> {
31
+ data: T | null;
32
+ error: Error | null;
33
+ loading: boolean;
34
+ }
35
+ /**
36
+ * Reactive query hook. Runs `fetcher` on mount and whenever `deps` change.
37
+ *
38
+ * @param fetcher - Function that receives the configured HttpClient and returns a promise.
39
+ * @param deps - Optional dependency array controlling when to refetch (default: `[]`).
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const { data, error, loading, refetch } = useQuery(
44
+ * (client) => getSchemas(client),
45
+ * );
46
+ * ```
47
+ */
48
+ declare function useQuery<T>(fetcher: (client: HttpClient) => Promise<T>, deps?: unknown[]): AsyncState<T> & {
49
+ refetch: () => void;
50
+ };
51
+ /**
52
+ * Mutation hook. Returns a trigger function and the current state.
53
+ *
54
+ * @param mutator - Function that receives the configured HttpClient and variables.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const [create, { loading, error }] = useMutation(
59
+ * (client, vars: { title: string }) => createArticle(client, vars),
60
+ * );
61
+ * // await create({ title: 'Hello' })
62
+ * ```
63
+ */
64
+ declare function useMutation<T, V>(mutator: (client: HttpClient, vars: V) => Promise<T>): [(vars: V) => Promise<T>, AsyncState<T>];
65
+
66
+ export { type AsyncState, type CmsConfig, CmsProvider, useMutation, useQuery };
package/dist/index.js ADDED
@@ -0,0 +1,81 @@
1
+ // src/index.tsx
2
+ import { createClient } from "@lyeve/cms-client";
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useState
10
+ } from "react";
11
+ import { jsx } from "react/jsx-runtime";
12
+ var CmsContext = createContext(null);
13
+ function CmsProvider({
14
+ config,
15
+ children
16
+ }) {
17
+ return /* @__PURE__ */ jsx(CmsContext.Provider, { value: config, children });
18
+ }
19
+ function useClient() {
20
+ const config = useContext(CmsContext);
21
+ if (!config) {
22
+ throw new Error("CmsProvider must wrap your component tree");
23
+ }
24
+ return useMemo(() => {
25
+ const base = config.baseUrl ?? "";
26
+ return createClient((url, init) => {
27
+ const fullUrl = typeof url === "string" ? `${base}${url}` : url;
28
+ return fetch(fullUrl, {
29
+ ...init,
30
+ headers: { ...init?.headers, ...config.getHeaders?.() }
31
+ });
32
+ });
33
+ }, [config.baseUrl, config.getHeaders]);
34
+ }
35
+ function useQuery(fetcher, deps = []) {
36
+ const client = useClient();
37
+ const [state, setState] = useState({
38
+ data: null,
39
+ error: null,
40
+ loading: true
41
+ });
42
+ const run = useCallback(() => {
43
+ setState((s) => ({ ...s, loading: true }));
44
+ fetcher(client).then((data) => setState({ data, error: null, loading: false })).catch(
45
+ (error) => setState((s) => ({ ...s, error, loading: false }))
46
+ );
47
+ }, [client, ...deps]);
48
+ useEffect(() => {
49
+ run();
50
+ }, [run]);
51
+ return { ...state, refetch: run };
52
+ }
53
+ function useMutation(mutator) {
54
+ const client = useClient();
55
+ const [state, setState] = useState({
56
+ data: null,
57
+ error: null,
58
+ loading: false
59
+ });
60
+ const run = useCallback(
61
+ async (vars) => {
62
+ setState((s) => ({ ...s, loading: true }));
63
+ try {
64
+ const data = await mutator(client, vars);
65
+ setState({ data, error: null, loading: false });
66
+ return data;
67
+ } catch (error) {
68
+ setState({ data: null, error, loading: false });
69
+ throw error;
70
+ }
71
+ },
72
+ [client]
73
+ );
74
+ return [run, state];
75
+ }
76
+ export {
77
+ CmsProvider,
78
+ useMutation,
79
+ useQuery
80
+ };
81
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.tsx"],"sourcesContent":["/**\n * LyEve CMS React hooks - typed, reactive data fetching.\n *\n * Thin wrapper around {@link @lyeve/cms-client} that makes the client\n * available via context and provides `useQuery` / `useMutation` hooks.\n *\n * @example\n * ```tsx\n * import { CmsProvider, useQuery } from '@lyeve/cms-client-react';\n * import { getSchemas } from '@lyeve/cms-client-rest';\n *\n * function App() {\n * return (\n * <CmsProvider config={{ baseUrl: 'https://cms.example.com', getHeaders: () => ({ Authorization: `Bearer ${token}` }) }}>\n * <SchemaManager />\n * </CmsProvider>\n * );\n * }\n *\n * function SchemaManager() {\n * const { data: schemas, loading } = useQuery((client) => getSchemas(client));\n * // ...\n * }\n * ```\n *\n * @packageDocumentation\n */\n\nimport { createClient, type HttpClient } from '@lyeve/cms-client';\nimport {\n createContext,\n useContext,\n useCallback,\n useEffect,\n useMemo,\n useState,\n type ReactNode,\n} from 'react';\n\n// Provider\n\nexport interface CmsConfig {\n /** Base URL prepended to every request path. */\n baseUrl?: string;\n /**\n * Callback returning headers added to every request.\n * Called on every request so auth tokens can be refreshed without\n * recreating the provider.\n */\n getHeaders?: () => Record<string, string>;\n}\n\nconst CmsContext = createContext<CmsConfig | null>(null);\n\n/**\n * Context provider that makes the CMS client available to all descendant\n * hooks.\n *\n * @example\n * ```tsx\n * <CmsProvider config={{ baseUrl: 'https://cms.example.com' }}>\n * <App />\n * </CmsProvider>\n * ```\n */\nexport function CmsProvider({\n config,\n children,\n}: {\n config: CmsConfig;\n children: ReactNode;\n}) {\n return <CmsContext.Provider value={config}>{children}</CmsContext.Provider>;\n}\n\n/**\n * Returns a memoized HttpClient instance configured from the nearest\n * {@link CmsProvider}.\n */\nfunction useClient(): HttpClient {\n const config = useContext(CmsContext);\n if (!config) {\n throw new Error('CmsProvider must wrap your component tree');\n }\n // Re-create when config changes so getHeaders/auth tokens stay current.\n return useMemo(() => {\n const base = config.baseUrl ?? '';\n return createClient((url, init) => {\n const fullUrl = typeof url === 'string' ? `${base}${url}` : url;\n return fetch(fullUrl, {\n ...init,\n headers: { ...init?.headers, ...config.getHeaders?.() },\n });\n });\n }, [config.baseUrl, config.getHeaders]);\n}\n\n// Hooks\n\nexport interface AsyncState<T> {\n data: T | null;\n error: Error | null;\n loading: boolean;\n}\n\n/**\n * Reactive query hook. Runs `fetcher` on mount and whenever `deps` change.\n *\n * @param fetcher - Function that receives the configured HttpClient and returns a promise.\n * @param deps - Optional dependency array controlling when to refetch (default: `[]`).\n *\n * @example\n * ```ts\n * const { data, error, loading, refetch } = useQuery(\n * (client) => getSchemas(client),\n * );\n * ```\n */\nexport function useQuery<T>(\n fetcher: (client: HttpClient) => Promise<T>,\n deps: unknown[] = [],\n): AsyncState<T> & { refetch: () => void } {\n const client = useClient();\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n error: null,\n loading: true,\n });\n\n const run = useCallback(() => {\n setState((s) => ({ ...s, loading: true }));\n\n fetcher(client)\n .then((data) => setState({ data, error: null, loading: false }))\n .catch((error) =>\n setState((s) => ({ ...s, error: error as Error, loading: false })),\n );\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [client, ...deps]);\n\n useEffect(() => {\n run();\n }, [run]);\n\n return { ...state, refetch: run };\n}\n\n/**\n * Mutation hook. Returns a trigger function and the current state.\n *\n * @param mutator - Function that receives the configured HttpClient and variables.\n *\n * @example\n * ```ts\n * const [create, { loading, error }] = useMutation(\n * (client, vars: { title: string }) => createArticle(client, vars),\n * );\n * // await create({ title: 'Hello' })\n * ```\n */\nexport function useMutation<T, V>(\n mutator: (client: HttpClient, vars: V) => Promise<T>,\n): [(vars: V) => Promise<T>, AsyncState<T>] {\n const client = useClient();\n const [state, setState] = useState<AsyncState<T>>({\n data: null,\n error: null,\n loading: false,\n });\n\n const run = useCallback(\n async (vars: V) => {\n setState((s) => ({ ...s, loading: true }));\n try {\n const data = await mutator(client, vars);\n setState({ data, error: null, loading: false });\n return data;\n } catch (error) {\n setState({ data: null, error: error as Error, loading: false });\n throw error;\n }\n },\n [client],\n );\n\n return [run, state];\n}\n"],"mappings":";AA4BA,SAAS,oBAAqC;AAC9C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAmCE;AApBT,IAAM,aAAa,cAAgC,IAAI;AAahD,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AACF,GAGG;AACD,SAAO,oBAAC,WAAW,UAAX,EAAoB,OAAO,QAAS,UAAS;AACvD;AAMA,SAAS,YAAwB;AAC/B,QAAM,SAAS,WAAW,UAAU;AACpC,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,2CAA2C;AAAA,EAC7D;AAEA,SAAO,QAAQ,MAAM;AACnB,UAAM,OAAO,OAAO,WAAW;AAC/B,WAAO,aAAa,CAAC,KAAK,SAAS;AACjC,YAAM,UAAU,OAAO,QAAQ,WAAW,GAAG,IAAI,GAAG,GAAG,KAAK;AAC5D,aAAO,MAAM,SAAS;AAAA,QACpB,GAAG;AAAA,QACH,SAAS,EAAE,GAAG,MAAM,SAAS,GAAG,OAAO,aAAa,EAAE;AAAA,MACxD,CAAC;AAAA,IACH,CAAC;AAAA,EACH,GAAG,CAAC,OAAO,SAAS,OAAO,UAAU,CAAC;AACxC;AAuBO,SAAS,SACd,SACA,OAAkB,CAAC,GACsB;AACzC,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB;AAAA,IAChD,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,EACX,CAAC;AAED,QAAM,MAAM,YAAY,MAAM;AAC5B,aAAS,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,KAAK,EAAE;AAEzC,YAAQ,MAAM,EACX,KAAK,CAAC,SAAS,SAAS,EAAE,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,CAAC,EAC9D;AAAA,MAAM,CAAC,UACN,SAAS,CAAC,OAAO,EAAE,GAAG,GAAG,OAAuB,SAAS,MAAM,EAAE;AAAA,IACnE;AAAA,EAEJ,GAAG,CAAC,QAAQ,GAAG,IAAI,CAAC;AAEpB,YAAU,MAAM;AACd,QAAI;AAAA,EACN,GAAG,CAAC,GAAG,CAAC;AAER,SAAO,EAAE,GAAG,OAAO,SAAS,IAAI;AAClC;AAeO,SAAS,YACd,SAC0C;AAC1C,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwB;AAAA,IAChD,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,EACX,CAAC;AAED,QAAM,MAAM;AAAA,IACV,OAAO,SAAY;AACjB,eAAS,CAAC,OAAO,EAAE,GAAG,GAAG,SAAS,KAAK,EAAE;AACzC,UAAI;AACF,cAAM,OAAO,MAAM,QAAQ,QAAQ,IAAI;AACvC,iBAAS,EAAE,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC;AAC9C,eAAO;AAAA,MACT,SAAS,OAAO;AACd,iBAAS,EAAE,MAAM,MAAM,OAAuB,SAAS,MAAM,CAAC;AAC9D,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAEA,SAAO,CAAC,KAAK,KAAK;AACpB;","names":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@lyeve-labs/client-react",
3
+ "version": "0.2.1",
4
+ "description": "React hooks for LyEve Core - typed, reactive data fetching",
5
+ "license": "MIT",
6
+ "author": "LyEve Labs <hello@lyeve.com>",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "main": "./dist/index.cjs",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "sideEffects": false,
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "!dist/**/*.test.*",
26
+ "!dist/**/*.spec.*"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "peerDependencies": {
32
+ "@lyeve-labs/client-react": ">=0.1.0",
33
+ "react": ">=18.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@lyeve-labs/client-react": "^0.1.0",
37
+ "@testing-library/react": "^16.3.2",
38
+ "@types/react": "^19.2.0",
39
+ "jsdom": "^29.1.1",
40
+ "prettier": "^3.4.0",
41
+ "publint": "^0.3.0",
42
+ "react": "^19.2.8",
43
+ "react-dom": "^19.2.8",
44
+ "tsup": "^8.4.0",
45
+ "typescript": "^5.7.0",
46
+ "vitest": "^2.1.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup && publint",
50
+ "check": "tsc --noEmit",
51
+ "test": "vitest run",
52
+ "dev": "tsup --watch",
53
+ "test:watch": "vitest",
54
+ "format": "prettier --write .",
55
+ "format:check": "prettier --check ."
56
+ }
57
+ }