@sylphx/lens-react 1.0.2

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/src/hooks.ts ADDED
@@ -0,0 +1,392 @@
1
+ /**
2
+ * @sylphx/lens-react - Hooks
3
+ *
4
+ * React hooks for Lens queries and mutations.
5
+ * Works with QueryResult from @sylphx/lens-client.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useLensClient, useQuery, useMutation } from '@sylphx/lens-react';
10
+ *
11
+ * function UserProfile({ userId }: { userId: string }) {
12
+ * const client = useLensClient();
13
+ * const { data: user, loading } = useQuery(client.user.get({ id: userId }));
14
+ * if (loading) return <Spinner />;
15
+ * return <h1>{user?.name}</h1>;
16
+ * }
17
+ *
18
+ * function CreatePost() {
19
+ * const client = useLensClient();
20
+ * const { mutate, loading } = useMutation(client.post.create);
21
+ * const handleCreate = () => mutate({ title: 'Hello' });
22
+ * return <button onClick={handleCreate} disabled={loading}>Create</button>;
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ import type { MutationResult, QueryResult } from "@sylphx/lens-client";
28
+ import { useCallback, useEffect, useRef, useState } from "react";
29
+
30
+ // =============================================================================
31
+ // Types
32
+ // =============================================================================
33
+
34
+ /** Result of useQuery hook */
35
+ export interface UseQueryResult<T> {
36
+ /** Query data (null if loading or error) */
37
+ data: T | null;
38
+ /** Loading state */
39
+ loading: boolean;
40
+ /** Error state */
41
+ error: Error | null;
42
+ /** Refetch the query */
43
+ refetch: () => void;
44
+ }
45
+
46
+ /** Result of useMutation hook */
47
+ export interface UseMutationResult<TInput, TOutput> {
48
+ /** Execute the mutation */
49
+ mutate: (input: TInput) => Promise<MutationResult<TOutput>>;
50
+ /** Mutation is in progress */
51
+ loading: boolean;
52
+ /** Mutation error */
53
+ error: Error | null;
54
+ /** Last mutation result */
55
+ data: TOutput | null;
56
+ /** Reset mutation state */
57
+ reset: () => void;
58
+ }
59
+
60
+ /** Options for useQuery */
61
+ export interface UseQueryOptions {
62
+ /** Skip the query (don't execute) */
63
+ skip?: boolean;
64
+ }
65
+
66
+ // =============================================================================
67
+ // useQuery Hook
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Subscribe to a query with reactive updates
72
+ *
73
+ * @param query - QueryResult from client API call
74
+ * @param options - Query options
75
+ *
76
+ * @example
77
+ * ```tsx
78
+ * // Basic usage
79
+ * function UserProfile({ userId }: { userId: string }) {
80
+ * const client = useLensClient();
81
+ * const { data: user, loading, error } = useQuery(client.user.get({ id: userId }));
82
+ *
83
+ * if (loading) return <Spinner />;
84
+ * if (error) return <Error message={error.message} />;
85
+ * if (!user) return <NotFound />;
86
+ *
87
+ * return <h1>{user.name}</h1>;
88
+ * }
89
+ *
90
+ * // With select (type-safe field selection)
91
+ * function UserName({ userId }: { userId: string }) {
92
+ * const client = useLensClient();
93
+ * const { data } = useQuery(
94
+ * client.user.get({ id: userId }).select({ name: true })
95
+ * );
96
+ * // data is { name: string } | null
97
+ * return <span>{data?.name}</span>;
98
+ * }
99
+ *
100
+ * // Skip query conditionally
101
+ * function ConditionalQuery({ shouldFetch }: { shouldFetch: boolean }) {
102
+ * const client = useLensClient();
103
+ * const { data } = useQuery(client.user.list(), { skip: !shouldFetch });
104
+ * }
105
+ * ```
106
+ */
107
+ export function useQuery<T>(query: QueryResult<T>, options?: UseQueryOptions): UseQueryResult<T> {
108
+ const [data, setData] = useState<T | null>(null);
109
+ const [loading, setLoading] = useState(!options?.skip);
110
+ const [error, setError] = useState<Error | null>(null);
111
+
112
+ // Track mounted state
113
+ const mountedRef = useRef(true);
114
+
115
+ // Subscribe to query
116
+ useEffect(() => {
117
+ mountedRef.current = true;
118
+
119
+ if (options?.skip) {
120
+ setLoading(false);
121
+ return;
122
+ }
123
+
124
+ setLoading(true);
125
+ setError(null);
126
+
127
+ // Subscribe to updates
128
+ const unsubscribe = query.subscribe((value) => {
129
+ if (mountedRef.current) {
130
+ setData(value);
131
+ setLoading(false);
132
+ }
133
+ });
134
+
135
+ // Handle initial load via promise (for one-shot queries)
136
+ query.then(
137
+ (value) => {
138
+ if (mountedRef.current) {
139
+ setData(value);
140
+ setLoading(false);
141
+ }
142
+ },
143
+ (err) => {
144
+ if (mountedRef.current) {
145
+ setError(err instanceof Error ? err : new Error(String(err)));
146
+ setLoading(false);
147
+ }
148
+ },
149
+ );
150
+
151
+ return () => {
152
+ mountedRef.current = false;
153
+ unsubscribe();
154
+ };
155
+ }, [query, options?.skip]);
156
+
157
+ // Refetch function
158
+ const refetch = useCallback(() => {
159
+ if (options?.skip) return;
160
+
161
+ setLoading(true);
162
+ setError(null);
163
+
164
+ query.then(
165
+ (value) => {
166
+ if (mountedRef.current) {
167
+ setData(value);
168
+ setLoading(false);
169
+ }
170
+ },
171
+ (err) => {
172
+ if (mountedRef.current) {
173
+ setError(err instanceof Error ? err : new Error(String(err)));
174
+ setLoading(false);
175
+ }
176
+ },
177
+ );
178
+ }, [query, options?.skip]);
179
+
180
+ return { data, loading, error, refetch };
181
+ }
182
+
183
+ // =============================================================================
184
+ // useMutation Hook
185
+ // =============================================================================
186
+
187
+ /** Mutation function type */
188
+ export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationResult<TOutput>>;
189
+
190
+ /**
191
+ * Execute mutations with loading/error state
192
+ *
193
+ * @param mutationFn - Mutation function from client API
194
+ *
195
+ * @example
196
+ * ```tsx
197
+ * function CreatePost() {
198
+ * const client = useLensClient();
199
+ * const { mutate, loading, error, data } = useMutation(client.post.create);
200
+ *
201
+ * const handleSubmit = async (formData: FormData) => {
202
+ * try {
203
+ * const result = await mutate({
204
+ * title: formData.get('title'),
205
+ * content: formData.get('content'),
206
+ * });
207
+ * console.log('Created:', result.data);
208
+ * } catch (err) {
209
+ * console.error('Failed:', err);
210
+ * }
211
+ * };
212
+ *
213
+ * return (
214
+ * <form onSubmit={handleSubmit}>
215
+ * <button type="submit" disabled={loading}>
216
+ * {loading ? 'Creating...' : 'Create'}
217
+ * </button>
218
+ * {error && <p className="error">{error.message}</p>}
219
+ * </form>
220
+ * );
221
+ * }
222
+ *
223
+ * // With optimistic updates
224
+ * function UpdatePost({ postId }: { postId: string }) {
225
+ * const client = useLensClient();
226
+ * const { mutate } = useMutation(client.post.update);
227
+ *
228
+ * const handleUpdate = async (title: string) => {
229
+ * const result = await mutate({ id: postId, title });
230
+ * // result.rollback?.() can undo optimistic update
231
+ * };
232
+ * }
233
+ * ```
234
+ */
235
+ export function useMutation<TInput, TOutput>(
236
+ mutationFn: MutationFn<TInput, TOutput>,
237
+ ): UseMutationResult<TInput, TOutput> {
238
+ const [loading, setLoading] = useState(false);
239
+ const [error, setError] = useState<Error | null>(null);
240
+ const [data, setData] = useState<TOutput | null>(null);
241
+
242
+ // Track mounted state
243
+ const mountedRef = useRef(true);
244
+
245
+ useEffect(() => {
246
+ mountedRef.current = true;
247
+ return () => {
248
+ mountedRef.current = false;
249
+ };
250
+ }, []);
251
+
252
+ // Mutation wrapper
253
+ const mutate = useCallback(
254
+ async (input: TInput): Promise<MutationResult<TOutput>> => {
255
+ setLoading(true);
256
+ setError(null);
257
+
258
+ try {
259
+ const result = await mutationFn(input);
260
+
261
+ if (mountedRef.current) {
262
+ setData(result.data);
263
+ }
264
+
265
+ return result;
266
+ } catch (err) {
267
+ const mutationError = err instanceof Error ? err : new Error(String(err));
268
+ if (mountedRef.current) {
269
+ setError(mutationError);
270
+ }
271
+ throw mutationError;
272
+ } finally {
273
+ if (mountedRef.current) {
274
+ setLoading(false);
275
+ }
276
+ }
277
+ },
278
+ [mutationFn],
279
+ );
280
+
281
+ // Reset function
282
+ const reset = useCallback(() => {
283
+ setLoading(false);
284
+ setError(null);
285
+ setData(null);
286
+ }, []);
287
+
288
+ return { mutate, loading, error, data, reset };
289
+ }
290
+
291
+ // =============================================================================
292
+ // useLazyQuery Hook
293
+ // =============================================================================
294
+
295
+ /** Result of useLazyQuery hook */
296
+ export interface UseLazyQueryResult<T> {
297
+ /** Execute the query */
298
+ execute: () => Promise<T>;
299
+ /** Query data (null if not executed or error) */
300
+ data: T | null;
301
+ /** Loading state */
302
+ loading: boolean;
303
+ /** Error state */
304
+ error: Error | null;
305
+ /** Reset query state */
306
+ reset: () => void;
307
+ }
308
+
309
+ /**
310
+ * Execute a query on demand (not on mount)
311
+ *
312
+ * @param query - QueryResult from client API call
313
+ *
314
+ * @example
315
+ * ```tsx
316
+ * function SearchUsers() {
317
+ * const client = useLensClient();
318
+ * const [searchTerm, setSearchTerm] = useState('');
319
+ * const { execute, data, loading } = useLazyQuery(
320
+ * client.user.search({ query: searchTerm })
321
+ * );
322
+ *
323
+ * const handleSearch = async () => {
324
+ * const users = await execute();
325
+ * console.log('Found:', users);
326
+ * };
327
+ *
328
+ * return (
329
+ * <div>
330
+ * <input
331
+ * value={searchTerm}
332
+ * onChange={e => setSearchTerm(e.target.value)}
333
+ * />
334
+ * <button onClick={handleSearch} disabled={loading}>
335
+ * Search
336
+ * </button>
337
+ * {data?.map(user => <UserCard key={user.id} user={user} />)}
338
+ * </div>
339
+ * );
340
+ * }
341
+ * ```
342
+ */
343
+ export function useLazyQuery<T>(query: QueryResult<T>): UseLazyQueryResult<T> {
344
+ const [data, setData] = useState<T | null>(null);
345
+ const [loading, setLoading] = useState(false);
346
+ const [error, setError] = useState<Error | null>(null);
347
+
348
+ // Track mounted state
349
+ const mountedRef = useRef(true);
350
+
351
+ useEffect(() => {
352
+ mountedRef.current = true;
353
+ return () => {
354
+ mountedRef.current = false;
355
+ };
356
+ }, []);
357
+
358
+ // Execute function
359
+ const execute = useCallback(async (): Promise<T> => {
360
+ setLoading(true);
361
+ setError(null);
362
+
363
+ try {
364
+ const result = await query;
365
+
366
+ if (mountedRef.current) {
367
+ setData(result);
368
+ }
369
+
370
+ return result;
371
+ } catch (err) {
372
+ const queryError = err instanceof Error ? err : new Error(String(err));
373
+ if (mountedRef.current) {
374
+ setError(queryError);
375
+ }
376
+ throw queryError;
377
+ } finally {
378
+ if (mountedRef.current) {
379
+ setLoading(false);
380
+ }
381
+ }
382
+ }, [query]);
383
+
384
+ // Reset function
385
+ const reset = useCallback(() => {
386
+ setLoading(false);
387
+ setError(null);
388
+ setData(null);
389
+ }, []);
390
+
391
+ return { execute, data, loading, error, reset };
392
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @sylphx/lens-react
3
+ *
4
+ * React bindings for Lens API framework.
5
+ * Hooks and context provider for reactive data access.
6
+ */
7
+
8
+ // =============================================================================
9
+ // Context & Provider
10
+ // =============================================================================
11
+
12
+ export { LensProvider, useLensClient, type LensProviderProps } from "./context";
13
+
14
+ // =============================================================================
15
+ // Hooks (Operations-based API)
16
+ // =============================================================================
17
+
18
+ export {
19
+ // Query hooks
20
+ useQuery,
21
+ useLazyQuery,
22
+ // Mutation hook
23
+ useMutation,
24
+ // Types
25
+ type UseQueryResult,
26
+ type UseLazyQueryResult,
27
+ type UseMutationResult,
28
+ type UseQueryOptions,
29
+ type MutationFn,
30
+ } from "./hooks";