@sylphx/lens-react 1.2.22 → 2.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/dist/index.d.ts +83 -49
- package/dist/index.js +50 -20
- package/package.json +2 -2
- package/src/hooks.test.tsx +127 -169
- package/src/hooks.ts +182 -88
- package/src/index.ts +2 -1
package/src/hooks.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
*
|
|
11
11
|
* function UserProfile({ userId }: { userId: string }) {
|
|
12
12
|
* const client = useLensClient();
|
|
13
|
-
*
|
|
13
|
+
* // Route + Params pattern (recommended)
|
|
14
|
+
* const { data: user, loading } = useQuery(client.user.get, { id: userId });
|
|
14
15
|
* if (loading) return <Spinner />;
|
|
15
16
|
* return <h1>{user?.name}</h1>;
|
|
16
17
|
* }
|
|
@@ -25,18 +26,7 @@
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
import type { MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
28
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
29
|
-
|
|
30
|
-
// =============================================================================
|
|
31
|
-
// Query Input Types
|
|
32
|
-
// =============================================================================
|
|
33
|
-
|
|
34
|
-
/** Query input - can be a query, null/undefined, or an accessor function */
|
|
35
|
-
export type QueryInput<T> =
|
|
36
|
-
| QueryResult<T>
|
|
37
|
-
| null
|
|
38
|
-
| undefined
|
|
39
|
-
| (() => QueryResult<T> | null | undefined);
|
|
29
|
+
import { type DependencyList, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
40
30
|
|
|
41
31
|
// =============================================================================
|
|
42
32
|
// Types
|
|
@@ -69,75 +59,144 @@ export interface UseMutationResult<TInput, TOutput> {
|
|
|
69
59
|
}
|
|
70
60
|
|
|
71
61
|
/** Options for useQuery */
|
|
72
|
-
export interface UseQueryOptions {
|
|
62
|
+
export interface UseQueryOptions<TData = unknown, TSelected = TData> {
|
|
73
63
|
/** Skip the query (don't execute) */
|
|
74
64
|
skip?: boolean;
|
|
65
|
+
/** Transform the query result */
|
|
66
|
+
select?: (data: TData) => TSelected;
|
|
75
67
|
}
|
|
76
68
|
|
|
69
|
+
/** Route function type - takes params and returns QueryResult */
|
|
70
|
+
export type RouteFunction<TParams, TResult> = (params: TParams) => QueryResult<TResult>;
|
|
71
|
+
|
|
72
|
+
/** Accessor function type - returns QueryResult or null */
|
|
73
|
+
export type QueryAccessor<T> = () => QueryResult<T> | null | undefined;
|
|
74
|
+
|
|
75
|
+
/** Mutation function type */
|
|
76
|
+
export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
77
|
+
|
|
77
78
|
// =============================================================================
|
|
78
|
-
// useQuery Hook
|
|
79
|
+
// useQuery Hook - Route + Params (Primary API)
|
|
79
80
|
// =============================================================================
|
|
80
81
|
|
|
81
|
-
/** Helper to resolve query input (handles accessor functions) */
|
|
82
|
-
function resolveQuery<T>(input: QueryInput<T>): QueryResult<T> | null | undefined {
|
|
83
|
-
return typeof input === "function" ? input() : input;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
82
|
/**
|
|
87
|
-
* Subscribe to a query with reactive updates
|
|
83
|
+
* Subscribe to a query with reactive updates.
|
|
84
|
+
*
|
|
85
|
+
* Two usage patterns:
|
|
88
86
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* **1. Route + Params (recommended)** - Stable references, no infinite loops
|
|
88
|
+
* ```tsx
|
|
89
|
+
* const { data } = useQuery(client.user.get, { id: userId });
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* **2. Accessor + Deps (escape hatch)** - For complex/composed queries
|
|
93
|
+
* ```tsx
|
|
94
|
+
* const { data } = useQuery(() => client.user.get({ id }).pipe(transform), [id]);
|
|
95
|
+
* ```
|
|
91
96
|
*
|
|
92
97
|
* @example
|
|
93
98
|
* ```tsx
|
|
94
|
-
* // Basic usage
|
|
99
|
+
* // Basic usage - Route + Params
|
|
95
100
|
* function UserProfile({ userId }: { userId: string }) {
|
|
96
101
|
* const client = useLensClient();
|
|
97
|
-
* const { data: user, loading, error } = useQuery(client.user.get
|
|
102
|
+
* const { data: user, loading, error } = useQuery(client.user.get, { id: userId });
|
|
98
103
|
*
|
|
99
104
|
* if (loading) return <Spinner />;
|
|
100
105
|
* if (error) return <Error message={error.message} />;
|
|
101
|
-
*
|
|
106
|
+
* return <h1>{user?.name}</h1>;
|
|
107
|
+
* }
|
|
102
108
|
*
|
|
103
|
-
*
|
|
109
|
+
* // With select transform
|
|
110
|
+
* function UserName({ userId }: { userId: string }) {
|
|
111
|
+
* const client = useLensClient();
|
|
112
|
+
* const { data: name } = useQuery(client.user.get, { id: userId }, {
|
|
113
|
+
* select: (user) => user.name
|
|
114
|
+
* });
|
|
115
|
+
* return <span>{name}</span>;
|
|
104
116
|
* }
|
|
105
117
|
*
|
|
106
|
-
* // Conditional query
|
|
118
|
+
* // Conditional query
|
|
107
119
|
* function SessionInfo({ sessionId }: { sessionId: string | null }) {
|
|
108
120
|
* const client = useLensClient();
|
|
109
121
|
* const { data } = useQuery(
|
|
110
|
-
* sessionId ? client.session.get
|
|
122
|
+
* sessionId ? client.session.get : null,
|
|
123
|
+
* { id: sessionId ?? '' }
|
|
111
124
|
* );
|
|
112
|
-
* // data is null when sessionId is null
|
|
113
125
|
* return <span>{data?.totalTokens}</span>;
|
|
114
126
|
* }
|
|
115
127
|
*
|
|
116
|
-
* //
|
|
117
|
-
* function
|
|
128
|
+
* // Skip query
|
|
129
|
+
* function ConditionalQuery({ userId, shouldFetch }: { userId: string; shouldFetch: boolean }) {
|
|
118
130
|
* const client = useLensClient();
|
|
119
|
-
* const { data } = useQuery(
|
|
120
|
-
* sessionId.value ? client.session.get({ id: sessionId.value }) : null
|
|
121
|
-
* );
|
|
122
|
-
* return <span>{data?.totalTokens}</span>;
|
|
131
|
+
* const { data } = useQuery(client.user.get, { id: userId }, { skip: !shouldFetch });
|
|
123
132
|
* }
|
|
124
133
|
*
|
|
125
|
-
* //
|
|
126
|
-
* function
|
|
134
|
+
* // Complex queries with accessor (escape hatch)
|
|
135
|
+
* function ComplexQuery({ userId }: { userId: string }) {
|
|
127
136
|
* const client = useLensClient();
|
|
128
|
-
* const { data } = useQuery(
|
|
137
|
+
* const { data } = useQuery(
|
|
138
|
+
* () => client.user.get({ id: userId }),
|
|
139
|
+
* [userId]
|
|
140
|
+
* );
|
|
129
141
|
* }
|
|
130
142
|
* ```
|
|
131
143
|
*/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
|
|
145
|
+
// Overload 1: Route + Params (recommended)
|
|
146
|
+
export function useQuery<TParams, TResult, TSelected = TResult>(
|
|
147
|
+
route: RouteFunction<TParams, TResult> | null,
|
|
148
|
+
params: TParams,
|
|
149
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
150
|
+
): UseQueryResult<TSelected>;
|
|
151
|
+
|
|
152
|
+
// Overload 2: Accessor + Deps (escape hatch for complex queries)
|
|
153
|
+
export function useQuery<TResult, TSelected = TResult>(
|
|
154
|
+
accessor: QueryAccessor<TResult>,
|
|
155
|
+
deps: DependencyList,
|
|
156
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
157
|
+
): UseQueryResult<TSelected>;
|
|
158
|
+
|
|
159
|
+
// Implementation
|
|
160
|
+
export function useQuery<TParams, TResult, TSelected = TResult>(
|
|
161
|
+
routeOrAccessor: RouteFunction<TParams, TResult> | QueryAccessor<TResult> | null,
|
|
162
|
+
paramsOrDeps: TParams | DependencyList,
|
|
163
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
164
|
+
): UseQueryResult<TSelected> {
|
|
165
|
+
// Detect which overload is being used
|
|
166
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
167
|
+
|
|
168
|
+
// Stable params key for Route + Params mode
|
|
169
|
+
const paramsKey = !isAccessorMode ? JSON.stringify(paramsOrDeps) : null;
|
|
170
|
+
|
|
171
|
+
// Create query - memoized based on route/params or deps
|
|
172
|
+
const query = useMemo(
|
|
173
|
+
() => {
|
|
174
|
+
if (options?.skip) return null;
|
|
175
|
+
|
|
176
|
+
if (isAccessorMode) {
|
|
177
|
+
// Accessor mode: call the function
|
|
178
|
+
const accessor = routeOrAccessor as QueryAccessor<TResult>;
|
|
179
|
+
return accessor();
|
|
180
|
+
}
|
|
181
|
+
// Route + Params mode
|
|
182
|
+
if (!routeOrAccessor) return null;
|
|
183
|
+
const route = routeOrAccessor as RouteFunction<TParams, TResult>;
|
|
184
|
+
return route(paramsOrDeps as TParams);
|
|
185
|
+
},
|
|
186
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Dynamic deps based on overload mode - intentional
|
|
187
|
+
isAccessorMode
|
|
188
|
+
? // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
189
|
+
[options?.skip, ...(paramsOrDeps as DependencyList)]
|
|
190
|
+
: // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
191
|
+
[routeOrAccessor, paramsKey, options?.skip],
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Use ref for select to avoid it being a dependency
|
|
195
|
+
const selectRef = useRef(options?.select);
|
|
196
|
+
selectRef.current = options?.select;
|
|
197
|
+
|
|
198
|
+
const [data, setData] = useState<TSelected | null>(null);
|
|
199
|
+
const [loading, setLoading] = useState(query != null && !options?.skip);
|
|
141
200
|
const [error, setError] = useState<Error | null>(null);
|
|
142
201
|
|
|
143
202
|
// Track mounted state
|
|
@@ -147,12 +206,17 @@ export function useQuery<T>(
|
|
|
147
206
|
const queryRef = useRef(query);
|
|
148
207
|
queryRef.current = query;
|
|
149
208
|
|
|
209
|
+
// Transform helper
|
|
210
|
+
const transform = useCallback((value: TResult): TSelected => {
|
|
211
|
+
return selectRef.current ? selectRef.current(value) : (value as unknown as TSelected);
|
|
212
|
+
}, []);
|
|
213
|
+
|
|
150
214
|
// Subscribe to query
|
|
151
215
|
useEffect(() => {
|
|
152
216
|
mountedRef.current = true;
|
|
153
217
|
|
|
154
218
|
// Handle null/undefined query
|
|
155
|
-
if (query == null
|
|
219
|
+
if (query == null) {
|
|
156
220
|
setData(null);
|
|
157
221
|
setLoading(false);
|
|
158
222
|
setError(null);
|
|
@@ -165,7 +229,7 @@ export function useQuery<T>(
|
|
|
165
229
|
// Subscribe to updates
|
|
166
230
|
const unsubscribe = query.subscribe((value) => {
|
|
167
231
|
if (mountedRef.current) {
|
|
168
|
-
setData(value);
|
|
232
|
+
setData(transform(value));
|
|
169
233
|
setLoading(false);
|
|
170
234
|
}
|
|
171
235
|
});
|
|
@@ -174,7 +238,7 @@ export function useQuery<T>(
|
|
|
174
238
|
query.then(
|
|
175
239
|
(value) => {
|
|
176
240
|
if (mountedRef.current) {
|
|
177
|
-
setData(value);
|
|
241
|
+
setData(transform(value));
|
|
178
242
|
setLoading(false);
|
|
179
243
|
}
|
|
180
244
|
},
|
|
@@ -190,12 +254,12 @@ export function useQuery<T>(
|
|
|
190
254
|
mountedRef.current = false;
|
|
191
255
|
unsubscribe();
|
|
192
256
|
};
|
|
193
|
-
}, [query,
|
|
257
|
+
}, [query, transform]);
|
|
194
258
|
|
|
195
259
|
// Refetch function
|
|
196
260
|
const refetch = useCallback(() => {
|
|
197
261
|
const currentQuery = queryRef.current;
|
|
198
|
-
if (currentQuery == null
|
|
262
|
+
if (currentQuery == null) return;
|
|
199
263
|
|
|
200
264
|
setLoading(true);
|
|
201
265
|
setError(null);
|
|
@@ -203,7 +267,7 @@ export function useQuery<T>(
|
|
|
203
267
|
currentQuery.then(
|
|
204
268
|
(value) => {
|
|
205
269
|
if (mountedRef.current) {
|
|
206
|
-
setData(value);
|
|
270
|
+
setData(transform(value));
|
|
207
271
|
setLoading(false);
|
|
208
272
|
}
|
|
209
273
|
},
|
|
@@ -214,7 +278,7 @@ export function useQuery<T>(
|
|
|
214
278
|
}
|
|
215
279
|
},
|
|
216
280
|
);
|
|
217
|
-
}, [
|
|
281
|
+
}, [transform]);
|
|
218
282
|
|
|
219
283
|
return { data, loading, error, refetch };
|
|
220
284
|
}
|
|
@@ -223,9 +287,6 @@ export function useQuery<T>(
|
|
|
223
287
|
// useMutation Hook
|
|
224
288
|
// =============================================================================
|
|
225
289
|
|
|
226
|
-
/** Mutation function type */
|
|
227
|
-
export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
228
|
-
|
|
229
290
|
/**
|
|
230
291
|
* Execute mutations with loading/error state
|
|
231
292
|
*
|
|
@@ -348,57 +409,77 @@ export interface UseLazyQueryResult<T> {
|
|
|
348
409
|
/**
|
|
349
410
|
* Execute a query on demand (not on mount)
|
|
350
411
|
*
|
|
351
|
-
* @param queryInput - QueryResult, null/undefined, or accessor function returning QueryResult
|
|
352
|
-
*
|
|
353
412
|
* @example
|
|
354
413
|
* ```tsx
|
|
414
|
+
* // Route + Params pattern
|
|
355
415
|
* function SearchUsers() {
|
|
356
416
|
* const client = useLensClient();
|
|
357
417
|
* const [searchTerm, setSearchTerm] = useState('');
|
|
358
418
|
* const { execute, data, loading } = useLazyQuery(
|
|
359
|
-
* client.user.search
|
|
419
|
+
* client.user.search,
|
|
420
|
+
* { query: searchTerm }
|
|
360
421
|
* );
|
|
361
422
|
*
|
|
362
|
-
* const handleSearch = async () => {
|
|
363
|
-
* const users = await execute();
|
|
364
|
-
* console.log('Found:', users);
|
|
365
|
-
* };
|
|
366
|
-
*
|
|
367
423
|
* return (
|
|
368
424
|
* <div>
|
|
369
|
-
* <input
|
|
370
|
-
*
|
|
371
|
-
* onChange={e => setSearchTerm(e.target.value)}
|
|
372
|
-
* />
|
|
373
|
-
* <button onClick={handleSearch} disabled={loading}>
|
|
374
|
-
* Search
|
|
375
|
-
* </button>
|
|
425
|
+
* <input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
|
|
426
|
+
* <button onClick={execute} disabled={loading}>Search</button>
|
|
376
427
|
* {data?.map(user => <UserCard key={user.id} user={user} />)}
|
|
377
428
|
* </div>
|
|
378
429
|
* );
|
|
379
430
|
* }
|
|
380
431
|
*
|
|
381
|
-
* //
|
|
382
|
-
* function
|
|
432
|
+
* // Accessor pattern
|
|
433
|
+
* function LazyComplexQuery({ userId }: { userId: string }) {
|
|
383
434
|
* const client = useLensClient();
|
|
384
|
-
* const { execute, data } = useLazyQuery(
|
|
385
|
-
*
|
|
435
|
+
* const { execute, data } = useLazyQuery(
|
|
436
|
+
* () => client.user.get({ id: userId }),
|
|
437
|
+
* [userId]
|
|
386
438
|
* );
|
|
387
439
|
* return <button onClick={execute}>Load</button>;
|
|
388
440
|
* }
|
|
389
441
|
* ```
|
|
390
442
|
*/
|
|
391
|
-
|
|
392
|
-
|
|
443
|
+
|
|
444
|
+
// Overload 1: Route + Params
|
|
445
|
+
export function useLazyQuery<TParams, TResult, TSelected = TResult>(
|
|
446
|
+
route: RouteFunction<TParams, TResult> | null,
|
|
447
|
+
params: TParams,
|
|
448
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
449
|
+
): UseLazyQueryResult<TSelected>;
|
|
450
|
+
|
|
451
|
+
// Overload 2: Accessor + Deps
|
|
452
|
+
export function useLazyQuery<TResult, TSelected = TResult>(
|
|
453
|
+
accessor: QueryAccessor<TResult>,
|
|
454
|
+
deps: DependencyList,
|
|
455
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
456
|
+
): UseLazyQueryResult<TSelected>;
|
|
457
|
+
|
|
458
|
+
// Implementation
|
|
459
|
+
export function useLazyQuery<TParams, TResult, TSelected = TResult>(
|
|
460
|
+
routeOrAccessor: RouteFunction<TParams, TResult> | QueryAccessor<TResult> | null,
|
|
461
|
+
paramsOrDeps: TParams | DependencyList,
|
|
462
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
463
|
+
): UseLazyQueryResult<TSelected> {
|
|
464
|
+
const [data, setData] = useState<TSelected | null>(null);
|
|
393
465
|
const [loading, setLoading] = useState(false);
|
|
394
466
|
const [error, setError] = useState<Error | null>(null);
|
|
395
467
|
|
|
396
468
|
// Track mounted state
|
|
397
469
|
const mountedRef = useRef(true);
|
|
398
470
|
|
|
399
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
471
|
+
// Detect which overload
|
|
472
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
473
|
+
|
|
474
|
+
// Store refs for execute (so it uses latest values)
|
|
475
|
+
const routeOrAccessorRef = useRef(routeOrAccessor);
|
|
476
|
+
routeOrAccessorRef.current = routeOrAccessor;
|
|
477
|
+
|
|
478
|
+
const paramsOrDepsRef = useRef(paramsOrDeps);
|
|
479
|
+
paramsOrDepsRef.current = paramsOrDeps;
|
|
480
|
+
|
|
481
|
+
const selectRef = useRef(options?.select);
|
|
482
|
+
selectRef.current = options?.select;
|
|
402
483
|
|
|
403
484
|
useEffect(() => {
|
|
404
485
|
mountedRef.current = true;
|
|
@@ -408,13 +489,23 @@ export function useLazyQuery<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T
|
|
|
408
489
|
}, []);
|
|
409
490
|
|
|
410
491
|
// Execute function
|
|
411
|
-
const execute = useCallback(async (): Promise<
|
|
412
|
-
|
|
492
|
+
const execute = useCallback(async (): Promise<TSelected> => {
|
|
493
|
+
let query: QueryResult<TResult> | null | undefined;
|
|
494
|
+
|
|
495
|
+
if (isAccessorMode) {
|
|
496
|
+
const accessor = routeOrAccessorRef.current as QueryAccessor<TResult>;
|
|
497
|
+
query = accessor();
|
|
498
|
+
} else {
|
|
499
|
+
const route = routeOrAccessorRef.current as RouteFunction<TParams, TResult> | null;
|
|
500
|
+
if (route) {
|
|
501
|
+
query = route(paramsOrDepsRef.current as TParams);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
413
504
|
|
|
414
505
|
if (query == null) {
|
|
415
506
|
setData(null);
|
|
416
507
|
setLoading(false);
|
|
417
|
-
return null as
|
|
508
|
+
return null as TSelected;
|
|
418
509
|
}
|
|
419
510
|
|
|
420
511
|
setLoading(true);
|
|
@@ -422,12 +513,15 @@ export function useLazyQuery<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T
|
|
|
422
513
|
|
|
423
514
|
try {
|
|
424
515
|
const result = await query;
|
|
516
|
+
const selected = selectRef.current
|
|
517
|
+
? selectRef.current(result)
|
|
518
|
+
: (result as unknown as TSelected);
|
|
425
519
|
|
|
426
520
|
if (mountedRef.current) {
|
|
427
|
-
setData(
|
|
521
|
+
setData(selected);
|
|
428
522
|
}
|
|
429
523
|
|
|
430
|
-
return
|
|
524
|
+
return selected;
|
|
431
525
|
} catch (err) {
|
|
432
526
|
const queryError = err instanceof Error ? err : new Error(String(err));
|
|
433
527
|
if (mountedRef.current) {
|
|
@@ -439,7 +533,7 @@ export function useLazyQuery<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T
|
|
|
439
533
|
setLoading(false);
|
|
440
534
|
}
|
|
441
535
|
}
|
|
442
|
-
}, []);
|
|
536
|
+
}, [isAccessorMode]);
|
|
443
537
|
|
|
444
538
|
// Reset function
|
|
445
539
|
const reset = useCallback(() => {
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,8 @@ export { LensProvider, type LensProviderProps, useLensClient } from "./context.j
|
|
|
18
18
|
export {
|
|
19
19
|
type MutationFn,
|
|
20
20
|
// Types
|
|
21
|
-
type
|
|
21
|
+
type QueryAccessor,
|
|
22
|
+
type RouteFunction,
|
|
22
23
|
type UseLazyQueryResult,
|
|
23
24
|
type UseMutationResult,
|
|
24
25
|
type UseQueryOptions,
|