@sylphx/lens-react 2.0.1 → 2.1.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/dist/index.d.ts +104 -63
- package/dist/index.js +65 -23
- package/package.json +1 -1
- package/src/hooks.test.tsx +466 -367
- package/src/hooks.ts +255 -130
- package/src/index.ts +4 -3
package/src/hooks.ts
CHANGED
|
@@ -6,37 +6,29 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```tsx
|
|
9
|
-
* import {
|
|
9
|
+
* import { useQuery, useMutation } from '@sylphx/lens-react';
|
|
10
10
|
*
|
|
11
11
|
* function UserProfile({ userId }: { userId: string }) {
|
|
12
|
-
*
|
|
13
|
-
* const { data: user, loading } = useQuery(
|
|
12
|
+
* // Client is automatically injected from context
|
|
13
|
+
* const { data: user, loading } = useQuery(
|
|
14
|
+
* (client) => client.user.get,
|
|
15
|
+
* { id: userId }
|
|
16
|
+
* );
|
|
14
17
|
* if (loading) return <Spinner />;
|
|
15
18
|
* return <h1>{user?.name}</h1>;
|
|
16
19
|
* }
|
|
17
20
|
*
|
|
18
21
|
* function CreatePost() {
|
|
19
|
-
* const
|
|
20
|
-
* const { mutate, loading } = useMutation(client.post.create);
|
|
22
|
+
* const { mutate, loading } = useMutation((client) => client.post.create);
|
|
21
23
|
* const handleCreate = () => mutate({ title: 'Hello' });
|
|
22
24
|
* return <button onClick={handleCreate} disabled={loading}>Create</button>;
|
|
23
25
|
* }
|
|
24
26
|
* ```
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
|
-
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 { LensClient, MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
30
|
+
import { type DependencyList, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
31
|
+
import { useLensClient } from "./context.js";
|
|
40
32
|
|
|
41
33
|
// =============================================================================
|
|
42
34
|
// Types
|
|
@@ -69,75 +61,159 @@ export interface UseMutationResult<TInput, TOutput> {
|
|
|
69
61
|
}
|
|
70
62
|
|
|
71
63
|
/** Options for useQuery */
|
|
72
|
-
export interface UseQueryOptions {
|
|
64
|
+
export interface UseQueryOptions<TData = unknown, TSelected = TData> {
|
|
73
65
|
/** Skip the query (don't execute) */
|
|
74
66
|
skip?: boolean;
|
|
67
|
+
/** Transform the query result */
|
|
68
|
+
select?: (data: TData) => TSelected;
|
|
75
69
|
}
|
|
76
70
|
|
|
71
|
+
/** Client type for callbacks */
|
|
72
|
+
type Client = LensClient<any, any>;
|
|
73
|
+
|
|
74
|
+
/** Route selector - callback that returns a route function */
|
|
75
|
+
export type RouteSelector<TParams, TResult> = (
|
|
76
|
+
client: Client,
|
|
77
|
+
) => ((params: TParams) => QueryResult<TResult>) | null;
|
|
78
|
+
|
|
79
|
+
/** Query accessor selector - callback that returns QueryResult */
|
|
80
|
+
export type QuerySelector<TResult> = (client: Client) => QueryResult<TResult> | null | undefined;
|
|
81
|
+
|
|
82
|
+
/** Mutation selector - callback that returns mutation function */
|
|
83
|
+
export type MutationSelector<TInput, TOutput> = (
|
|
84
|
+
client: Client,
|
|
85
|
+
) => (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
86
|
+
|
|
77
87
|
// =============================================================================
|
|
78
88
|
// useQuery Hook
|
|
79
89
|
// =============================================================================
|
|
80
90
|
|
|
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
91
|
/**
|
|
87
|
-
* Subscribe to a query with reactive updates
|
|
92
|
+
* Subscribe to a query with reactive updates.
|
|
93
|
+
* Client is automatically injected from LensProvider context.
|
|
88
94
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
95
|
+
* Two usage patterns:
|
|
96
|
+
*
|
|
97
|
+
* **1. Route + Params (recommended)** - Stable references, no infinite loops
|
|
98
|
+
* ```tsx
|
|
99
|
+
* const { data } = useQuery((client) => client.user.get, { id: userId });
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* **2. Accessor + Deps (escape hatch)** - For complex/composed queries
|
|
103
|
+
* ```tsx
|
|
104
|
+
* const { data } = useQuery((client) => client.user.get({ id }), [id]);
|
|
105
|
+
* ```
|
|
91
106
|
*
|
|
92
107
|
* @example
|
|
93
108
|
* ```tsx
|
|
94
|
-
* // Basic usage
|
|
109
|
+
* // Basic usage - Route + Params
|
|
95
110
|
* function UserProfile({ userId }: { userId: string }) {
|
|
96
|
-
* const
|
|
97
|
-
*
|
|
111
|
+
* const { data: user, loading, error } = useQuery(
|
|
112
|
+
* (client) => client.user.get,
|
|
113
|
+
* { id: userId }
|
|
114
|
+
* );
|
|
98
115
|
*
|
|
99
116
|
* if (loading) return <Spinner />;
|
|
100
117
|
* if (error) return <Error message={error.message} />;
|
|
101
|
-
*
|
|
118
|
+
* return <h1>{user?.name}</h1>;
|
|
119
|
+
* }
|
|
102
120
|
*
|
|
103
|
-
*
|
|
121
|
+
* // With select transform
|
|
122
|
+
* function UserName({ userId }: { userId: string }) {
|
|
123
|
+
* const { data: name } = useQuery(
|
|
124
|
+
* (client) => client.user.get,
|
|
125
|
+
* { id: userId },
|
|
126
|
+
* { select: (user) => user.name }
|
|
127
|
+
* );
|
|
128
|
+
* return <span>{name}</span>;
|
|
104
129
|
* }
|
|
105
130
|
*
|
|
106
|
-
* // Conditional query (null
|
|
131
|
+
* // Conditional query (return null to skip)
|
|
107
132
|
* function SessionInfo({ sessionId }: { sessionId: string | null }) {
|
|
108
|
-
* const client = useLensClient();
|
|
109
133
|
* const { data } = useQuery(
|
|
110
|
-
* sessionId ? client.session.get
|
|
134
|
+
* (client) => sessionId ? client.session.get : null,
|
|
135
|
+
* { id: sessionId ?? '' }
|
|
111
136
|
* );
|
|
112
|
-
* // data is null when sessionId is null
|
|
113
137
|
* return <span>{data?.totalTokens}</span>;
|
|
114
138
|
* }
|
|
115
139
|
*
|
|
116
|
-
* //
|
|
117
|
-
* function
|
|
118
|
-
* const
|
|
119
|
-
*
|
|
120
|
-
*
|
|
140
|
+
* // Skip query with option
|
|
141
|
+
* function ConditionalQuery({ userId, shouldFetch }: { userId: string; shouldFetch: boolean }) {
|
|
142
|
+
* const { data } = useQuery(
|
|
143
|
+
* (client) => client.user.get,
|
|
144
|
+
* { id: userId },
|
|
145
|
+
* { skip: !shouldFetch }
|
|
121
146
|
* );
|
|
122
|
-
* return <span>{data?.totalTokens}</span>;
|
|
123
147
|
* }
|
|
124
148
|
*
|
|
125
|
-
* //
|
|
126
|
-
* function
|
|
127
|
-
* const
|
|
128
|
-
*
|
|
149
|
+
* // Complex queries with accessor (escape hatch)
|
|
150
|
+
* function ComplexQuery({ userId }: { userId: string }) {
|
|
151
|
+
* const { data } = useQuery(
|
|
152
|
+
* (client) => client.user.get({ id: userId }),
|
|
153
|
+
* [userId]
|
|
154
|
+
* );
|
|
129
155
|
* }
|
|
130
156
|
* ```
|
|
131
157
|
*/
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
|
|
159
|
+
// Overload 1: Route + Params (recommended)
|
|
160
|
+
export function useQuery<TParams, TResult, TSelected = TResult>(
|
|
161
|
+
selector: RouteSelector<TParams, TResult>,
|
|
162
|
+
params: TParams,
|
|
163
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
164
|
+
): UseQueryResult<TSelected>;
|
|
165
|
+
|
|
166
|
+
// Overload 2: Accessor + Deps (escape hatch for complex queries)
|
|
167
|
+
export function useQuery<TResult, TSelected = TResult>(
|
|
168
|
+
selector: QuerySelector<TResult>,
|
|
169
|
+
deps: DependencyList,
|
|
170
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
171
|
+
): UseQueryResult<TSelected>;
|
|
172
|
+
|
|
173
|
+
// Implementation
|
|
174
|
+
export function useQuery<TParams, TResult, TSelected = TResult>(
|
|
175
|
+
selector: RouteSelector<TParams, TResult> | QuerySelector<TResult>,
|
|
176
|
+
paramsOrDeps: TParams | DependencyList,
|
|
177
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
178
|
+
): UseQueryResult<TSelected> {
|
|
179
|
+
const client = useLensClient();
|
|
180
|
+
|
|
181
|
+
// Detect which overload is being used
|
|
182
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
183
|
+
|
|
184
|
+
// Stable params key for Route + Params mode
|
|
185
|
+
const paramsKey = !isAccessorMode ? JSON.stringify(paramsOrDeps) : null;
|
|
186
|
+
|
|
187
|
+
// Create query - memoized based on route/params or deps
|
|
188
|
+
const query = useMemo(
|
|
189
|
+
() => {
|
|
190
|
+
if (options?.skip) return null;
|
|
191
|
+
|
|
192
|
+
if (isAccessorMode) {
|
|
193
|
+
// Accessor mode: selector returns QueryResult directly
|
|
194
|
+
const querySelector = selector as QuerySelector<TResult>;
|
|
195
|
+
return querySelector(client);
|
|
196
|
+
}
|
|
197
|
+
// Route + Params mode: selector returns route function
|
|
198
|
+
const routeSelector = selector as RouteSelector<TParams, TResult>;
|
|
199
|
+
const route = routeSelector(client);
|
|
200
|
+
if (!route) return null;
|
|
201
|
+
return route(paramsOrDeps as TParams);
|
|
202
|
+
},
|
|
203
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Dynamic deps based on overload mode - intentional
|
|
204
|
+
isAccessorMode
|
|
205
|
+
? // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
|
+
[client, options?.skip, ...(paramsOrDeps as DependencyList)]
|
|
207
|
+
: // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
208
|
+
[client, selector, paramsKey, options?.skip],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Use ref for select to avoid it being a dependency
|
|
212
|
+
const selectRef = useRef(options?.select);
|
|
213
|
+
selectRef.current = options?.select;
|
|
214
|
+
|
|
215
|
+
const [data, setData] = useState<TSelected | null>(null);
|
|
216
|
+
const [loading, setLoading] = useState(query != null && !options?.skip);
|
|
141
217
|
const [error, setError] = useState<Error | null>(null);
|
|
142
218
|
|
|
143
219
|
// Track mounted state
|
|
@@ -147,12 +223,17 @@ export function useQuery<T>(
|
|
|
147
223
|
const queryRef = useRef(query);
|
|
148
224
|
queryRef.current = query;
|
|
149
225
|
|
|
226
|
+
// Transform helper
|
|
227
|
+
const transform = useCallback((value: TResult): TSelected => {
|
|
228
|
+
return selectRef.current ? selectRef.current(value) : (value as unknown as TSelected);
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
150
231
|
// Subscribe to query
|
|
151
232
|
useEffect(() => {
|
|
152
233
|
mountedRef.current = true;
|
|
153
234
|
|
|
154
235
|
// Handle null/undefined query
|
|
155
|
-
if (query == null
|
|
236
|
+
if (query == null) {
|
|
156
237
|
setData(null);
|
|
157
238
|
setLoading(false);
|
|
158
239
|
setError(null);
|
|
@@ -162,19 +243,26 @@ export function useQuery<T>(
|
|
|
162
243
|
setLoading(true);
|
|
163
244
|
setError(null);
|
|
164
245
|
|
|
165
|
-
//
|
|
246
|
+
// Track if subscribe has provided data (to avoid duplicate updates from then)
|
|
247
|
+
let hasReceivedData = false;
|
|
248
|
+
|
|
249
|
+
// Subscribe to updates - primary data source for streaming
|
|
166
250
|
const unsubscribe = query.subscribe((value) => {
|
|
167
251
|
if (mountedRef.current) {
|
|
168
|
-
|
|
252
|
+
hasReceivedData = true;
|
|
253
|
+
setData(transform(value));
|
|
169
254
|
setLoading(false);
|
|
170
255
|
}
|
|
171
256
|
});
|
|
172
257
|
|
|
173
|
-
// Handle
|
|
258
|
+
// Handle completion/error via promise
|
|
259
|
+
// Only setData if subscribe hasn't already provided data (one-shot queries)
|
|
174
260
|
query.then(
|
|
175
261
|
(value) => {
|
|
262
|
+
if (mountedRef.current && !hasReceivedData) {
|
|
263
|
+
setData(transform(value));
|
|
264
|
+
}
|
|
176
265
|
if (mountedRef.current) {
|
|
177
|
-
setData(value);
|
|
178
266
|
setLoading(false);
|
|
179
267
|
}
|
|
180
268
|
},
|
|
@@ -190,12 +278,12 @@ export function useQuery<T>(
|
|
|
190
278
|
mountedRef.current = false;
|
|
191
279
|
unsubscribe();
|
|
192
280
|
};
|
|
193
|
-
}, [query,
|
|
281
|
+
}, [query, transform]);
|
|
194
282
|
|
|
195
283
|
// Refetch function
|
|
196
284
|
const refetch = useCallback(() => {
|
|
197
285
|
const currentQuery = queryRef.current;
|
|
198
|
-
if (currentQuery == null
|
|
286
|
+
if (currentQuery == null) return;
|
|
199
287
|
|
|
200
288
|
setLoading(true);
|
|
201
289
|
setError(null);
|
|
@@ -203,7 +291,7 @@ export function useQuery<T>(
|
|
|
203
291
|
currentQuery.then(
|
|
204
292
|
(value) => {
|
|
205
293
|
if (mountedRef.current) {
|
|
206
|
-
setData(value);
|
|
294
|
+
setData(transform(value));
|
|
207
295
|
setLoading(false);
|
|
208
296
|
}
|
|
209
297
|
},
|
|
@@ -214,7 +302,7 @@ export function useQuery<T>(
|
|
|
214
302
|
}
|
|
215
303
|
},
|
|
216
304
|
);
|
|
217
|
-
}, [
|
|
305
|
+
}, [transform]);
|
|
218
306
|
|
|
219
307
|
return { data, loading, error, refetch };
|
|
220
308
|
}
|
|
@@ -223,19 +311,18 @@ export function useQuery<T>(
|
|
|
223
311
|
// useMutation Hook
|
|
224
312
|
// =============================================================================
|
|
225
313
|
|
|
226
|
-
/** Mutation function type */
|
|
227
|
-
export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
228
|
-
|
|
229
314
|
/**
|
|
230
|
-
* Execute mutations with loading/error state
|
|
315
|
+
* Execute mutations with loading/error state.
|
|
316
|
+
* Client is automatically injected from LensProvider context.
|
|
231
317
|
*
|
|
232
|
-
* @param
|
|
318
|
+
* @param selector - Callback that returns mutation function from client
|
|
233
319
|
*
|
|
234
320
|
* @example
|
|
235
321
|
* ```tsx
|
|
236
322
|
* function CreatePost() {
|
|
237
|
-
* const
|
|
238
|
-
*
|
|
323
|
+
* const { mutate, loading, error, data } = useMutation(
|
|
324
|
+
* (client) => client.post.create
|
|
325
|
+
* );
|
|
239
326
|
*
|
|
240
327
|
* const handleSubmit = async (formData: FormData) => {
|
|
241
328
|
* try {
|
|
@@ -261,8 +348,7 @@ export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationRes
|
|
|
261
348
|
*
|
|
262
349
|
* // With optimistic updates
|
|
263
350
|
* function UpdatePost({ postId }: { postId: string }) {
|
|
264
|
-
* const
|
|
265
|
-
* const { mutate } = useMutation(client.post.update);
|
|
351
|
+
* const { mutate } = useMutation((client) => client.post.update);
|
|
266
352
|
*
|
|
267
353
|
* const handleUpdate = async (title: string) => {
|
|
268
354
|
* const result = await mutate({ id: postId, title });
|
|
@@ -272,8 +358,11 @@ export type MutationFn<TInput, TOutput> = (input: TInput) => Promise<MutationRes
|
|
|
272
358
|
* ```
|
|
273
359
|
*/
|
|
274
360
|
export function useMutation<TInput, TOutput>(
|
|
275
|
-
|
|
361
|
+
selector: MutationSelector<TInput, TOutput>,
|
|
276
362
|
): UseMutationResult<TInput, TOutput> {
|
|
363
|
+
const client = useLensClient();
|
|
364
|
+
const mutationFn = selector(client);
|
|
365
|
+
|
|
277
366
|
const [loading, setLoading] = useState(false);
|
|
278
367
|
const [error, setError] = useState<Error | null>(null);
|
|
279
368
|
const [data, setData] = useState<TOutput | null>(null);
|
|
@@ -281,6 +370,10 @@ export function useMutation<TInput, TOutput>(
|
|
|
281
370
|
// Track mounted state
|
|
282
371
|
const mountedRef = useRef(true);
|
|
283
372
|
|
|
373
|
+
// Store mutation ref for latest version
|
|
374
|
+
const mutationRef = useRef(mutationFn);
|
|
375
|
+
mutationRef.current = mutationFn;
|
|
376
|
+
|
|
284
377
|
useEffect(() => {
|
|
285
378
|
mountedRef.current = true;
|
|
286
379
|
return () => {
|
|
@@ -289,33 +382,30 @@ export function useMutation<TInput, TOutput>(
|
|
|
289
382
|
}, []);
|
|
290
383
|
|
|
291
384
|
// Mutation wrapper
|
|
292
|
-
const mutate = useCallback(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
setError(null);
|
|
385
|
+
const mutate = useCallback(async (input: TInput): Promise<MutationResult<TOutput>> => {
|
|
386
|
+
setLoading(true);
|
|
387
|
+
setError(null);
|
|
296
388
|
|
|
297
|
-
|
|
298
|
-
|
|
389
|
+
try {
|
|
390
|
+
const result = await mutationRef.current(input);
|
|
299
391
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
392
|
+
if (mountedRef.current) {
|
|
393
|
+
setData(result.data);
|
|
394
|
+
}
|
|
303
395
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
throw mutationError;
|
|
311
|
-
} finally {
|
|
312
|
-
if (mountedRef.current) {
|
|
313
|
-
setLoading(false);
|
|
314
|
-
}
|
|
396
|
+
return result;
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const mutationError = err instanceof Error ? err : new Error(String(err));
|
|
399
|
+
if (mountedRef.current) {
|
|
400
|
+
setError(mutationError);
|
|
315
401
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
402
|
+
throw mutationError;
|
|
403
|
+
} finally {
|
|
404
|
+
if (mountedRef.current) {
|
|
405
|
+
setLoading(false);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}, []);
|
|
319
409
|
|
|
320
410
|
// Reset function
|
|
321
411
|
const reset = useCallback(() => {
|
|
@@ -346,59 +436,80 @@ export interface UseLazyQueryResult<T> {
|
|
|
346
436
|
}
|
|
347
437
|
|
|
348
438
|
/**
|
|
349
|
-
* Execute a query on demand (not on mount)
|
|
350
|
-
*
|
|
351
|
-
* @param queryInput - QueryResult, null/undefined, or accessor function returning QueryResult
|
|
439
|
+
* Execute a query on demand (not on mount).
|
|
440
|
+
* Client is automatically injected from LensProvider context.
|
|
352
441
|
*
|
|
353
442
|
* @example
|
|
354
443
|
* ```tsx
|
|
444
|
+
* // Route + Params pattern
|
|
355
445
|
* function SearchUsers() {
|
|
356
|
-
* const client = useLensClient();
|
|
357
446
|
* const [searchTerm, setSearchTerm] = useState('');
|
|
358
447
|
* const { execute, data, loading } = useLazyQuery(
|
|
359
|
-
* client.user.search
|
|
448
|
+
* (client) => client.user.search,
|
|
449
|
+
* { query: searchTerm }
|
|
360
450
|
* );
|
|
361
451
|
*
|
|
362
|
-
* const handleSearch = async () => {
|
|
363
|
-
* const users = await execute();
|
|
364
|
-
* console.log('Found:', users);
|
|
365
|
-
* };
|
|
366
|
-
*
|
|
367
452
|
* return (
|
|
368
453
|
* <div>
|
|
369
|
-
* <input
|
|
370
|
-
*
|
|
371
|
-
* onChange={e => setSearchTerm(e.target.value)}
|
|
372
|
-
* />
|
|
373
|
-
* <button onClick={handleSearch} disabled={loading}>
|
|
374
|
-
* Search
|
|
375
|
-
* </button>
|
|
454
|
+
* <input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
|
|
455
|
+
* <button onClick={execute} disabled={loading}>Search</button>
|
|
376
456
|
* {data?.map(user => <UserCard key={user.id} user={user} />)}
|
|
377
457
|
* </div>
|
|
378
458
|
* );
|
|
379
459
|
* }
|
|
380
460
|
*
|
|
381
|
-
* //
|
|
382
|
-
* function
|
|
383
|
-
* const
|
|
384
|
-
*
|
|
385
|
-
*
|
|
461
|
+
* // Accessor pattern
|
|
462
|
+
* function LazyComplexQuery({ userId }: { userId: string }) {
|
|
463
|
+
* const { execute, data } = useLazyQuery(
|
|
464
|
+
* (client) => client.user.get({ id: userId }),
|
|
465
|
+
* [userId]
|
|
386
466
|
* );
|
|
387
467
|
* return <button onClick={execute}>Load</button>;
|
|
388
468
|
* }
|
|
389
469
|
* ```
|
|
390
470
|
*/
|
|
391
|
-
|
|
392
|
-
|
|
471
|
+
|
|
472
|
+
// Overload 1: Route + Params
|
|
473
|
+
export function useLazyQuery<TParams, TResult, TSelected = TResult>(
|
|
474
|
+
selector: RouteSelector<TParams, TResult>,
|
|
475
|
+
params: TParams,
|
|
476
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
477
|
+
): UseLazyQueryResult<TSelected>;
|
|
478
|
+
|
|
479
|
+
// Overload 2: Accessor + Deps
|
|
480
|
+
export function useLazyQuery<TResult, TSelected = TResult>(
|
|
481
|
+
selector: QuerySelector<TResult>,
|
|
482
|
+
deps: DependencyList,
|
|
483
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
484
|
+
): UseLazyQueryResult<TSelected>;
|
|
485
|
+
|
|
486
|
+
// Implementation
|
|
487
|
+
export function useLazyQuery<TParams, TResult, TSelected = TResult>(
|
|
488
|
+
selector: RouteSelector<TParams, TResult> | QuerySelector<TResult>,
|
|
489
|
+
paramsOrDeps: TParams | DependencyList,
|
|
490
|
+
options?: UseQueryOptions<TResult, TSelected>,
|
|
491
|
+
): UseLazyQueryResult<TSelected> {
|
|
492
|
+
const client = useLensClient();
|
|
493
|
+
|
|
494
|
+
const [data, setData] = useState<TSelected | null>(null);
|
|
393
495
|
const [loading, setLoading] = useState(false);
|
|
394
496
|
const [error, setError] = useState<Error | null>(null);
|
|
395
497
|
|
|
396
498
|
// Track mounted state
|
|
397
499
|
const mountedRef = useRef(true);
|
|
398
500
|
|
|
399
|
-
//
|
|
400
|
-
const
|
|
401
|
-
|
|
501
|
+
// Detect which overload
|
|
502
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
503
|
+
|
|
504
|
+
// Store refs for execute (so it uses latest values)
|
|
505
|
+
const selectorRef = useRef(selector);
|
|
506
|
+
selectorRef.current = selector;
|
|
507
|
+
|
|
508
|
+
const paramsOrDepsRef = useRef(paramsOrDeps);
|
|
509
|
+
paramsOrDepsRef.current = paramsOrDeps;
|
|
510
|
+
|
|
511
|
+
const selectRef = useRef(options?.select);
|
|
512
|
+
selectRef.current = options?.select;
|
|
402
513
|
|
|
403
514
|
useEffect(() => {
|
|
404
515
|
mountedRef.current = true;
|
|
@@ -408,13 +519,24 @@ export function useLazyQuery<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T
|
|
|
408
519
|
}, []);
|
|
409
520
|
|
|
410
521
|
// Execute function
|
|
411
|
-
const execute = useCallback(async (): Promise<
|
|
412
|
-
|
|
522
|
+
const execute = useCallback(async (): Promise<TSelected> => {
|
|
523
|
+
let query: QueryResult<TResult> | null | undefined;
|
|
524
|
+
|
|
525
|
+
if (isAccessorMode) {
|
|
526
|
+
const querySelector = selectorRef.current as QuerySelector<TResult>;
|
|
527
|
+
query = querySelector(client);
|
|
528
|
+
} else {
|
|
529
|
+
const routeSelector = selectorRef.current as RouteSelector<TParams, TResult>;
|
|
530
|
+
const route = routeSelector(client);
|
|
531
|
+
if (route) {
|
|
532
|
+
query = route(paramsOrDepsRef.current as TParams);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
413
535
|
|
|
414
536
|
if (query == null) {
|
|
415
537
|
setData(null);
|
|
416
538
|
setLoading(false);
|
|
417
|
-
return null as
|
|
539
|
+
return null as TSelected;
|
|
418
540
|
}
|
|
419
541
|
|
|
420
542
|
setLoading(true);
|
|
@@ -422,12 +544,15 @@ export function useLazyQuery<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T
|
|
|
422
544
|
|
|
423
545
|
try {
|
|
424
546
|
const result = await query;
|
|
547
|
+
const selected = selectRef.current
|
|
548
|
+
? selectRef.current(result)
|
|
549
|
+
: (result as unknown as TSelected);
|
|
425
550
|
|
|
426
551
|
if (mountedRef.current) {
|
|
427
|
-
setData(
|
|
552
|
+
setData(selected);
|
|
428
553
|
}
|
|
429
554
|
|
|
430
|
-
return
|
|
555
|
+
return selected;
|
|
431
556
|
} catch (err) {
|
|
432
557
|
const queryError = err instanceof Error ? err : new Error(String(err));
|
|
433
558
|
if (mountedRef.current) {
|
|
@@ -439,7 +564,7 @@ export function useLazyQuery<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T
|
|
|
439
564
|
setLoading(false);
|
|
440
565
|
}
|
|
441
566
|
}
|
|
442
|
-
}, []);
|
|
567
|
+
}, [client, isAccessorMode]);
|
|
443
568
|
|
|
444
569
|
// Reset function
|
|
445
570
|
const reset = useCallback(() => {
|
package/src/index.ts
CHANGED
|
@@ -16,16 +16,17 @@ export { LensProvider, type LensProviderProps, useLensClient } from "./context.j
|
|
|
16
16
|
// =============================================================================
|
|
17
17
|
|
|
18
18
|
export {
|
|
19
|
-
type MutationFn,
|
|
20
19
|
// Types
|
|
21
|
-
type
|
|
20
|
+
type MutationSelector,
|
|
21
|
+
type QuerySelector,
|
|
22
|
+
type RouteSelector,
|
|
22
23
|
type UseLazyQueryResult,
|
|
23
24
|
type UseMutationResult,
|
|
24
25
|
type UseQueryOptions,
|
|
25
26
|
type UseQueryResult,
|
|
27
|
+
// Query hooks
|
|
26
28
|
useLazyQuery,
|
|
27
29
|
// Mutation hook
|
|
28
30
|
useMutation,
|
|
29
|
-
// Query hooks
|
|
30
31
|
useQuery,
|
|
31
32
|
} from "./hooks.js";
|