@sylphx/lens-react 2.0.1 → 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 +1 -1
- package/src/hooks.test.tsx +127 -139
- package/src/hooks.ts +182 -88
- package/src/index.ts +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -45,8 +45,7 @@ declare function LensProvider({ client, children }: LensProviderProps): ReactEle
|
|
|
45
45
|
*/
|
|
46
46
|
declare function useLensClient<TRouter = any>(): LensClient<any, any> & TRouter;
|
|
47
47
|
import { MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
48
|
-
|
|
49
|
-
type QueryInput<T> = QueryResult<T> | null | undefined | (() => QueryResult<T> | null | undefined);
|
|
48
|
+
import { DependencyList } from "react";
|
|
50
49
|
/** Result of useQuery hook */
|
|
51
50
|
interface UseQueryResult<T> {
|
|
52
51
|
/** Query data (null if loading or error) */
|
|
@@ -75,62 +74,98 @@ interface UseMutationResult<
|
|
|
75
74
|
reset: () => void;
|
|
76
75
|
}
|
|
77
76
|
/** Options for useQuery */
|
|
78
|
-
interface UseQueryOptions
|
|
77
|
+
interface UseQueryOptions<
|
|
78
|
+
TData = unknown,
|
|
79
|
+
TSelected = TData
|
|
80
|
+
> {
|
|
79
81
|
/** Skip the query (don't execute) */
|
|
80
82
|
skip?: boolean;
|
|
83
|
+
/** Transform the query result */
|
|
84
|
+
select?: (data: TData) => TSelected;
|
|
81
85
|
}
|
|
86
|
+
/** Route function type - takes params and returns QueryResult */
|
|
87
|
+
type RouteFunction<
|
|
88
|
+
TParams,
|
|
89
|
+
TResult
|
|
90
|
+
> = (params: TParams) => QueryResult<TResult>;
|
|
91
|
+
/** Accessor function type - returns QueryResult or null */
|
|
92
|
+
type QueryAccessor<T> = () => QueryResult<T> | null | undefined;
|
|
93
|
+
/** Mutation function type */
|
|
94
|
+
type MutationFn<
|
|
95
|
+
TInput,
|
|
96
|
+
TOutput
|
|
97
|
+
> = (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
82
98
|
/**
|
|
83
|
-
* Subscribe to a query with reactive updates
|
|
99
|
+
* Subscribe to a query with reactive updates.
|
|
100
|
+
*
|
|
101
|
+
* Two usage patterns:
|
|
84
102
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
103
|
+
* **1. Route + Params (recommended)** - Stable references, no infinite loops
|
|
104
|
+
* ```tsx
|
|
105
|
+
* const { data } = useQuery(client.user.get, { id: userId });
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* **2. Accessor + Deps (escape hatch)** - For complex/composed queries
|
|
109
|
+
* ```tsx
|
|
110
|
+
* const { data } = useQuery(() => client.user.get({ id }).pipe(transform), [id]);
|
|
111
|
+
* ```
|
|
87
112
|
*
|
|
88
113
|
* @example
|
|
89
114
|
* ```tsx
|
|
90
|
-
* // Basic usage
|
|
115
|
+
* // Basic usage - Route + Params
|
|
91
116
|
* function UserProfile({ userId }: { userId: string }) {
|
|
92
117
|
* const client = useLensClient();
|
|
93
|
-
* const { data: user, loading, error } = useQuery(client.user.get
|
|
118
|
+
* const { data: user, loading, error } = useQuery(client.user.get, { id: userId });
|
|
94
119
|
*
|
|
95
120
|
* if (loading) return <Spinner />;
|
|
96
121
|
* if (error) return <Error message={error.message} />;
|
|
97
|
-
*
|
|
122
|
+
* return <h1>{user?.name}</h1>;
|
|
123
|
+
* }
|
|
98
124
|
*
|
|
99
|
-
*
|
|
125
|
+
* // With select transform
|
|
126
|
+
* function UserName({ userId }: { userId: string }) {
|
|
127
|
+
* const client = useLensClient();
|
|
128
|
+
* const { data: name } = useQuery(client.user.get, { id: userId }, {
|
|
129
|
+
* select: (user) => user.name
|
|
130
|
+
* });
|
|
131
|
+
* return <span>{name}</span>;
|
|
100
132
|
* }
|
|
101
133
|
*
|
|
102
|
-
* // Conditional query
|
|
134
|
+
* // Conditional query
|
|
103
135
|
* function SessionInfo({ sessionId }: { sessionId: string | null }) {
|
|
104
136
|
* const client = useLensClient();
|
|
105
137
|
* const { data } = useQuery(
|
|
106
|
-
* sessionId ? client.session.get
|
|
138
|
+
* sessionId ? client.session.get : null,
|
|
139
|
+
* { id: sessionId ?? '' }
|
|
107
140
|
* );
|
|
108
|
-
* // data is null when sessionId is null
|
|
109
141
|
* return <span>{data?.totalTokens}</span>;
|
|
110
142
|
* }
|
|
111
143
|
*
|
|
112
|
-
* //
|
|
113
|
-
* function
|
|
144
|
+
* // Skip query
|
|
145
|
+
* function ConditionalQuery({ userId, shouldFetch }: { userId: string; shouldFetch: boolean }) {
|
|
114
146
|
* const client = useLensClient();
|
|
115
|
-
* const { data } = useQuery(
|
|
116
|
-
* sessionId.value ? client.session.get({ id: sessionId.value }) : null
|
|
117
|
-
* );
|
|
118
|
-
* return <span>{data?.totalTokens}</span>;
|
|
147
|
+
* const { data } = useQuery(client.user.get, { id: userId }, { skip: !shouldFetch });
|
|
119
148
|
* }
|
|
120
149
|
*
|
|
121
|
-
* //
|
|
122
|
-
* function
|
|
150
|
+
* // Complex queries with accessor (escape hatch)
|
|
151
|
+
* function ComplexQuery({ userId }: { userId: string }) {
|
|
123
152
|
* const client = useLensClient();
|
|
124
|
-
* const { data } = useQuery(
|
|
153
|
+
* const { data } = useQuery(
|
|
154
|
+
* () => client.user.get({ id: userId }),
|
|
155
|
+
* [userId]
|
|
156
|
+
* );
|
|
125
157
|
* }
|
|
126
158
|
* ```
|
|
127
159
|
*/
|
|
128
|
-
declare function useQuery<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
160
|
+
declare function useQuery<
|
|
161
|
+
TParams,
|
|
162
|
+
TResult,
|
|
163
|
+
TSelected = TResult
|
|
164
|
+
>(route: RouteFunction<TParams, TResult> | null, params: TParams, options?: UseQueryOptions<TResult, TSelected>): UseQueryResult<TSelected>;
|
|
165
|
+
declare function useQuery<
|
|
166
|
+
TResult,
|
|
167
|
+
TSelected = TResult
|
|
168
|
+
>(accessor: QueryAccessor<TResult>, deps: DependencyList, options?: UseQueryOptions<TResult, TSelected>): UseQueryResult<TSelected>;
|
|
134
169
|
/**
|
|
135
170
|
* Execute mutations with loading/error state
|
|
136
171
|
*
|
|
@@ -196,45 +231,44 @@ interface UseLazyQueryResult<T> {
|
|
|
196
231
|
/**
|
|
197
232
|
* Execute a query on demand (not on mount)
|
|
198
233
|
*
|
|
199
|
-
* @param queryInput - QueryResult, null/undefined, or accessor function returning QueryResult
|
|
200
|
-
*
|
|
201
234
|
* @example
|
|
202
235
|
* ```tsx
|
|
236
|
+
* // Route + Params pattern
|
|
203
237
|
* function SearchUsers() {
|
|
204
238
|
* const client = useLensClient();
|
|
205
239
|
* const [searchTerm, setSearchTerm] = useState('');
|
|
206
240
|
* const { execute, data, loading } = useLazyQuery(
|
|
207
|
-
* client.user.search
|
|
241
|
+
* client.user.search,
|
|
242
|
+
* { query: searchTerm }
|
|
208
243
|
* );
|
|
209
244
|
*
|
|
210
|
-
* const handleSearch = async () => {
|
|
211
|
-
* const users = await execute();
|
|
212
|
-
* console.log('Found:', users);
|
|
213
|
-
* };
|
|
214
|
-
*
|
|
215
245
|
* return (
|
|
216
246
|
* <div>
|
|
217
|
-
* <input
|
|
218
|
-
*
|
|
219
|
-
* onChange={e => setSearchTerm(e.target.value)}
|
|
220
|
-
* />
|
|
221
|
-
* <button onClick={handleSearch} disabled={loading}>
|
|
222
|
-
* Search
|
|
223
|
-
* </button>
|
|
247
|
+
* <input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
|
|
248
|
+
* <button onClick={execute} disabled={loading}>Search</button>
|
|
224
249
|
* {data?.map(user => <UserCard key={user.id} user={user} />)}
|
|
225
250
|
* </div>
|
|
226
251
|
* );
|
|
227
252
|
* }
|
|
228
253
|
*
|
|
229
|
-
* //
|
|
230
|
-
* function
|
|
254
|
+
* // Accessor pattern
|
|
255
|
+
* function LazyComplexQuery({ userId }: { userId: string }) {
|
|
231
256
|
* const client = useLensClient();
|
|
232
|
-
* const { execute, data } = useLazyQuery(
|
|
233
|
-
*
|
|
257
|
+
* const { execute, data } = useLazyQuery(
|
|
258
|
+
* () => client.user.get({ id: userId }),
|
|
259
|
+
* [userId]
|
|
234
260
|
* );
|
|
235
261
|
* return <button onClick={execute}>Load</button>;
|
|
236
262
|
* }
|
|
237
263
|
* ```
|
|
238
264
|
*/
|
|
239
|
-
declare function useLazyQuery<
|
|
240
|
-
|
|
265
|
+
declare function useLazyQuery<
|
|
266
|
+
TParams,
|
|
267
|
+
TResult,
|
|
268
|
+
TSelected = TResult
|
|
269
|
+
>(route: RouteFunction<TParams, TResult> | null, params: TParams, options?: UseQueryOptions<TResult, TSelected>): UseLazyQueryResult<TSelected>;
|
|
270
|
+
declare function useLazyQuery<
|
|
271
|
+
TResult,
|
|
272
|
+
TSelected = TResult
|
|
273
|
+
>(accessor: QueryAccessor<TResult>, deps: DependencyList, options?: UseQueryOptions<TResult, TSelected>): UseLazyQueryResult<TSelected>;
|
|
274
|
+
export { useQuery, useMutation, useLensClient, useLazyQuery, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, RouteFunction, QueryAccessor, MutationFn, LensProviderProps, LensProvider };
|
package/dist/index.js
CHANGED
|
@@ -17,20 +17,35 @@ function useLensClient() {
|
|
|
17
17
|
}
|
|
18
18
|
// src/hooks.ts
|
|
19
19
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
function useQuery(routeOrAccessor, paramsOrDeps, options) {
|
|
21
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
22
|
+
const paramsKey = !isAccessorMode ? JSON.stringify(paramsOrDeps) : null;
|
|
23
|
+
const query = useMemo(() => {
|
|
24
|
+
if (options?.skip)
|
|
25
|
+
return null;
|
|
26
|
+
if (isAccessorMode) {
|
|
27
|
+
const accessor = routeOrAccessor;
|
|
28
|
+
return accessor();
|
|
29
|
+
}
|
|
30
|
+
if (!routeOrAccessor)
|
|
31
|
+
return null;
|
|
32
|
+
const route = routeOrAccessor;
|
|
33
|
+
return route(paramsOrDeps);
|
|
34
|
+
}, isAccessorMode ? [options?.skip, ...paramsOrDeps] : [routeOrAccessor, paramsKey, options?.skip]);
|
|
35
|
+
const selectRef = useRef(options?.select);
|
|
36
|
+
selectRef.current = options?.select;
|
|
25
37
|
const [data, setData] = useState(null);
|
|
26
|
-
const [loading, setLoading] = useState(
|
|
38
|
+
const [loading, setLoading] = useState(query != null && !options?.skip);
|
|
27
39
|
const [error, setError] = useState(null);
|
|
28
40
|
const mountedRef = useRef(true);
|
|
29
41
|
const queryRef = useRef(query);
|
|
30
42
|
queryRef.current = query;
|
|
43
|
+
const transform = useCallback((value) => {
|
|
44
|
+
return selectRef.current ? selectRef.current(value) : value;
|
|
45
|
+
}, []);
|
|
31
46
|
useEffect(() => {
|
|
32
47
|
mountedRef.current = true;
|
|
33
|
-
if (query == null
|
|
48
|
+
if (query == null) {
|
|
34
49
|
setData(null);
|
|
35
50
|
setLoading(false);
|
|
36
51
|
setError(null);
|
|
@@ -40,13 +55,13 @@ function useQuery(queryInput, options) {
|
|
|
40
55
|
setError(null);
|
|
41
56
|
const unsubscribe = query.subscribe((value) => {
|
|
42
57
|
if (mountedRef.current) {
|
|
43
|
-
setData(value);
|
|
58
|
+
setData(transform(value));
|
|
44
59
|
setLoading(false);
|
|
45
60
|
}
|
|
46
61
|
});
|
|
47
62
|
query.then((value) => {
|
|
48
63
|
if (mountedRef.current) {
|
|
49
|
-
setData(value);
|
|
64
|
+
setData(transform(value));
|
|
50
65
|
setLoading(false);
|
|
51
66
|
}
|
|
52
67
|
}, (err) => {
|
|
@@ -59,16 +74,16 @@ function useQuery(queryInput, options) {
|
|
|
59
74
|
mountedRef.current = false;
|
|
60
75
|
unsubscribe();
|
|
61
76
|
};
|
|
62
|
-
}, [query,
|
|
77
|
+
}, [query, transform]);
|
|
63
78
|
const refetch = useCallback(() => {
|
|
64
79
|
const currentQuery = queryRef.current;
|
|
65
|
-
if (currentQuery == null
|
|
80
|
+
if (currentQuery == null)
|
|
66
81
|
return;
|
|
67
82
|
setLoading(true);
|
|
68
83
|
setError(null);
|
|
69
84
|
currentQuery.then((value) => {
|
|
70
85
|
if (mountedRef.current) {
|
|
71
|
-
setData(value);
|
|
86
|
+
setData(transform(value));
|
|
72
87
|
setLoading(false);
|
|
73
88
|
}
|
|
74
89
|
}, (err) => {
|
|
@@ -77,7 +92,7 @@ function useQuery(queryInput, options) {
|
|
|
77
92
|
setLoading(false);
|
|
78
93
|
}
|
|
79
94
|
});
|
|
80
|
-
}, [
|
|
95
|
+
}, [transform]);
|
|
81
96
|
return { data, loading, error, refetch };
|
|
82
97
|
}
|
|
83
98
|
function useMutation(mutationFn) {
|
|
@@ -119,13 +134,18 @@ function useMutation(mutationFn) {
|
|
|
119
134
|
}, []);
|
|
120
135
|
return { mutate, loading, error, data, reset };
|
|
121
136
|
}
|
|
122
|
-
function useLazyQuery(
|
|
137
|
+
function useLazyQuery(routeOrAccessor, paramsOrDeps, options) {
|
|
123
138
|
const [data, setData] = useState(null);
|
|
124
139
|
const [loading, setLoading] = useState(false);
|
|
125
140
|
const [error, setError] = useState(null);
|
|
126
141
|
const mountedRef = useRef(true);
|
|
127
|
-
const
|
|
128
|
-
|
|
142
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
143
|
+
const routeOrAccessorRef = useRef(routeOrAccessor);
|
|
144
|
+
routeOrAccessorRef.current = routeOrAccessor;
|
|
145
|
+
const paramsOrDepsRef = useRef(paramsOrDeps);
|
|
146
|
+
paramsOrDepsRef.current = paramsOrDeps;
|
|
147
|
+
const selectRef = useRef(options?.select);
|
|
148
|
+
selectRef.current = options?.select;
|
|
129
149
|
useEffect(() => {
|
|
130
150
|
mountedRef.current = true;
|
|
131
151
|
return () => {
|
|
@@ -133,7 +153,16 @@ function useLazyQuery(queryInput) {
|
|
|
133
153
|
};
|
|
134
154
|
}, []);
|
|
135
155
|
const execute = useCallback(async () => {
|
|
136
|
-
|
|
156
|
+
let query;
|
|
157
|
+
if (isAccessorMode) {
|
|
158
|
+
const accessor = routeOrAccessorRef.current;
|
|
159
|
+
query = accessor();
|
|
160
|
+
} else {
|
|
161
|
+
const route = routeOrAccessorRef.current;
|
|
162
|
+
if (route) {
|
|
163
|
+
query = route(paramsOrDepsRef.current);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
137
166
|
if (query == null) {
|
|
138
167
|
setData(null);
|
|
139
168
|
setLoading(false);
|
|
@@ -143,10 +172,11 @@ function useLazyQuery(queryInput) {
|
|
|
143
172
|
setError(null);
|
|
144
173
|
try {
|
|
145
174
|
const result = await query;
|
|
175
|
+
const selected = selectRef.current ? selectRef.current(result) : result;
|
|
146
176
|
if (mountedRef.current) {
|
|
147
|
-
setData(
|
|
177
|
+
setData(selected);
|
|
148
178
|
}
|
|
149
|
-
return
|
|
179
|
+
return selected;
|
|
150
180
|
} catch (err) {
|
|
151
181
|
const queryError = err instanceof Error ? err : new Error(String(err));
|
|
152
182
|
if (mountedRef.current) {
|
|
@@ -158,7 +188,7 @@ function useLazyQuery(queryInput) {
|
|
|
158
188
|
setLoading(false);
|
|
159
189
|
}
|
|
160
190
|
}
|
|
161
|
-
}, []);
|
|
191
|
+
}, [isAccessorMode]);
|
|
162
192
|
const reset = useCallback(() => {
|
|
163
193
|
setLoading(false);
|
|
164
194
|
setError(null);
|
package/package.json
CHANGED
package/src/hooks.test.tsx
CHANGED
|
@@ -88,14 +88,14 @@ function createMockQueryResult<T>(initialValue: T | null = null): QueryResult<T>
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// =============================================================================
|
|
91
|
-
// Tests: useQuery
|
|
91
|
+
// Tests: useQuery (Accessor + Deps pattern)
|
|
92
92
|
// =============================================================================
|
|
93
93
|
|
|
94
94
|
describe("useQuery", () => {
|
|
95
95
|
test("returns loading state initially", () => {
|
|
96
96
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
97
97
|
|
|
98
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
98
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
99
99
|
|
|
100
100
|
expect(result.current.loading).toBe(true);
|
|
101
101
|
expect(result.current.data).toBe(null);
|
|
@@ -105,7 +105,7 @@ describe("useQuery", () => {
|
|
|
105
105
|
test("returns data when query resolves", async () => {
|
|
106
106
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
107
107
|
|
|
108
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
108
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
109
109
|
|
|
110
110
|
// Simulate data loading
|
|
111
111
|
act(() => {
|
|
@@ -123,7 +123,7 @@ describe("useQuery", () => {
|
|
|
123
123
|
test("returns error when query fails", async () => {
|
|
124
124
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
125
125
|
|
|
126
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
126
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
127
127
|
|
|
128
128
|
// Simulate error
|
|
129
129
|
act(() => {
|
|
@@ -147,7 +147,7 @@ describe("useQuery", () => {
|
|
|
147
147
|
},
|
|
148
148
|
} as unknown as QueryResult<{ id: string }>;
|
|
149
149
|
|
|
150
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
150
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
151
151
|
|
|
152
152
|
await waitFor(() => {
|
|
153
153
|
expect(result.current.error?.message).toBe("String error");
|
|
@@ -157,65 +157,32 @@ describe("useQuery", () => {
|
|
|
157
157
|
test("skips query when skip option is true", () => {
|
|
158
158
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
159
159
|
|
|
160
|
-
const { result } = renderHook(() => useQuery(mockQuery, { skip: true }));
|
|
160
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true }));
|
|
161
161
|
|
|
162
162
|
expect(result.current.loading).toBe(false);
|
|
163
163
|
expect(result.current.data).toBe(null);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
test("handles null query", () => {
|
|
167
|
-
const { result } = renderHook(() => useQuery(null));
|
|
166
|
+
test("handles null query from accessor", () => {
|
|
167
|
+
const { result } = renderHook(() => useQuery(() => null, []));
|
|
168
168
|
|
|
169
169
|
expect(result.current.loading).toBe(false);
|
|
170
170
|
expect(result.current.data).toBe(null);
|
|
171
171
|
expect(result.current.error).toBe(null);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
test("handles undefined query", () => {
|
|
175
|
-
const { result } = renderHook(() => useQuery(undefined));
|
|
174
|
+
test("handles undefined query from accessor", () => {
|
|
175
|
+
const { result } = renderHook(() => useQuery(() => undefined, []));
|
|
176
176
|
|
|
177
177
|
expect(result.current.loading).toBe(false);
|
|
178
178
|
expect(result.current.data).toBe(null);
|
|
179
179
|
expect(result.current.error).toBe(null);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
-
test("handles accessor function returning query", async () => {
|
|
183
|
-
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
184
|
-
const accessor = () => mockQuery;
|
|
185
|
-
|
|
186
|
-
const { result } = renderHook(() => useQuery(accessor));
|
|
187
|
-
|
|
188
|
-
act(() => {
|
|
189
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
await waitFor(() => {
|
|
193
|
-
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("handles accessor function returning null", () => {
|
|
198
|
-
const accessor = () => null;
|
|
199
|
-
|
|
200
|
-
const { result } = renderHook(() => useQuery(accessor));
|
|
201
|
-
|
|
202
|
-
expect(result.current.loading).toBe(false);
|
|
203
|
-
expect(result.current.data).toBe(null);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("handles accessor function returning undefined", () => {
|
|
207
|
-
const accessor = () => undefined;
|
|
208
|
-
|
|
209
|
-
const { result } = renderHook(() => useQuery(accessor));
|
|
210
|
-
|
|
211
|
-
expect(result.current.loading).toBe(false);
|
|
212
|
-
expect(result.current.data).toBe(null);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
182
|
test("updates when query subscription emits", async () => {
|
|
216
183
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
217
184
|
|
|
218
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
185
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
219
186
|
|
|
220
187
|
// First value
|
|
221
188
|
act(() => {
|
|
@@ -239,7 +206,7 @@ describe("useQuery", () => {
|
|
|
239
206
|
test("refetch reloads the query", async () => {
|
|
240
207
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
241
208
|
|
|
242
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
209
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
243
210
|
|
|
244
211
|
// Initial load
|
|
245
212
|
act(() => {
|
|
@@ -267,7 +234,7 @@ describe("useQuery", () => {
|
|
|
267
234
|
test("refetch handles errors", async () => {
|
|
268
235
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
269
236
|
|
|
270
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
237
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
271
238
|
|
|
272
239
|
// Initial load succeeds
|
|
273
240
|
act(() => {
|
|
@@ -287,7 +254,7 @@ describe("useQuery", () => {
|
|
|
287
254
|
} as unknown as QueryResult<{ id: string; name: string }>;
|
|
288
255
|
|
|
289
256
|
// Update the query to use failing query
|
|
290
|
-
const { result: result2 } = renderHook(() => useQuery(failingQuery));
|
|
257
|
+
const { result: result2 } = renderHook(() => useQuery(() => failingQuery, []));
|
|
291
258
|
|
|
292
259
|
await waitFor(() => {
|
|
293
260
|
expect(result2.current.error?.message).toBe("Refetch failed");
|
|
@@ -295,7 +262,7 @@ describe("useQuery", () => {
|
|
|
295
262
|
});
|
|
296
263
|
|
|
297
264
|
test("refetch does nothing when query is null", () => {
|
|
298
|
-
const { result } = renderHook(() => useQuery(null));
|
|
265
|
+
const { result } = renderHook(() => useQuery(() => null, []));
|
|
299
266
|
|
|
300
267
|
act(() => {
|
|
301
268
|
result.current.refetch();
|
|
@@ -308,7 +275,7 @@ describe("useQuery", () => {
|
|
|
308
275
|
test("refetch does nothing when skip is true", () => {
|
|
309
276
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
310
277
|
|
|
311
|
-
const { result } = renderHook(() => useQuery(mockQuery, { skip: true }));
|
|
278
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true }));
|
|
312
279
|
|
|
313
280
|
act(() => {
|
|
314
281
|
result.current.refetch();
|
|
@@ -330,7 +297,7 @@ describe("useQuery", () => {
|
|
|
330
297
|
},
|
|
331
298
|
} as unknown as QueryResult<{ id: string; name: string }>;
|
|
332
299
|
|
|
333
|
-
const { result } = renderHook(() => useQuery(mockQuery));
|
|
300
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
334
301
|
|
|
335
302
|
await waitFor(() => {
|
|
336
303
|
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
@@ -362,7 +329,7 @@ describe("useQuery", () => {
|
|
|
362
329
|
};
|
|
363
330
|
};
|
|
364
331
|
|
|
365
|
-
const { unmount } = renderHook(() => useQuery(mockQuery));
|
|
332
|
+
const { unmount } = renderHook(() => useQuery(() => mockQuery, []));
|
|
366
333
|
|
|
367
334
|
unmount();
|
|
368
335
|
|
|
@@ -372,7 +339,7 @@ describe("useQuery", () => {
|
|
|
372
339
|
test("does not update state after unmount", async () => {
|
|
373
340
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
374
341
|
|
|
375
|
-
const { unmount } = renderHook(() => useQuery(mockQuery));
|
|
342
|
+
const { unmount } = renderHook(() => useQuery(() => mockQuery, []));
|
|
376
343
|
|
|
377
344
|
// Unmount before query resolves
|
|
378
345
|
unmount();
|
|
@@ -387,12 +354,12 @@ describe("useQuery", () => {
|
|
|
387
354
|
expect(true).toBe(true);
|
|
388
355
|
});
|
|
389
356
|
|
|
390
|
-
test("handles query change", async () => {
|
|
357
|
+
test("handles query change via deps", async () => {
|
|
391
358
|
const mockQuery1 = createMockQueryResult<{ id: string; name: string }>();
|
|
392
359
|
const mockQuery2 = createMockQueryResult<{ id: string; name: string }>();
|
|
393
360
|
|
|
394
|
-
let
|
|
395
|
-
const { result, rerender } = renderHook(() => useQuery(
|
|
361
|
+
let queryId = 1;
|
|
362
|
+
const { result, rerender } = renderHook(() => useQuery(() => (queryId === 1 ? mockQuery1 : mockQuery2), [queryId]));
|
|
396
363
|
|
|
397
364
|
// Load first query
|
|
398
365
|
act(() => {
|
|
@@ -404,7 +371,7 @@ describe("useQuery", () => {
|
|
|
404
371
|
});
|
|
405
372
|
|
|
406
373
|
// Change to second query
|
|
407
|
-
|
|
374
|
+
queryId = 2;
|
|
408
375
|
rerender();
|
|
409
376
|
|
|
410
377
|
expect(result.current.loading).toBe(true);
|
|
@@ -423,7 +390,7 @@ describe("useQuery", () => {
|
|
|
423
390
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
424
391
|
|
|
425
392
|
let skip = true;
|
|
426
|
-
const { result, rerender } = renderHook(() => useQuery(mockQuery, { skip }));
|
|
393
|
+
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip }));
|
|
427
394
|
|
|
428
395
|
expect(result.current.loading).toBe(false);
|
|
429
396
|
|
|
@@ -446,7 +413,7 @@ describe("useQuery", () => {
|
|
|
446
413
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
447
414
|
|
|
448
415
|
let skip = false;
|
|
449
|
-
const { result, rerender } = renderHook(() => useQuery(mockQuery, { skip }));
|
|
416
|
+
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip }));
|
|
450
417
|
|
|
451
418
|
act(() => {
|
|
452
419
|
mockQuery._setValue({ id: "123", name: "John" });
|
|
@@ -464,6 +431,46 @@ describe("useQuery", () => {
|
|
|
464
431
|
expect(result.current.data).toBe(null);
|
|
465
432
|
expect(result.current.error).toBe(null);
|
|
466
433
|
});
|
|
434
|
+
|
|
435
|
+
test("select transforms the data", async () => {
|
|
436
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
437
|
+
|
|
438
|
+
const { result } = renderHook(() =>
|
|
439
|
+
useQuery(() => mockQuery, [], {
|
|
440
|
+
select: (data) => data.name.toUpperCase(),
|
|
441
|
+
}),
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
act(() => {
|
|
445
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
await waitFor(() => {
|
|
449
|
+
expect(result.current.data).toBe("JOHN");
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test("Route + Params pattern works", async () => {
|
|
454
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
455
|
+
const route = (_params: { id: string }) => mockQuery;
|
|
456
|
+
|
|
457
|
+
const { result } = renderHook(() => useQuery(route, { id: "123" }));
|
|
458
|
+
|
|
459
|
+
act(() => {
|
|
460
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await waitFor(() => {
|
|
464
|
+
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("Route + Params with null route", () => {
|
|
469
|
+
const { result } = renderHook(() => useQuery(null, { id: "123" }));
|
|
470
|
+
|
|
471
|
+
expect(result.current.loading).toBe(false);
|
|
472
|
+
expect(result.current.data).toBe(null);
|
|
473
|
+
});
|
|
467
474
|
});
|
|
468
475
|
|
|
469
476
|
// =============================================================================
|
|
@@ -657,14 +664,14 @@ describe("useMutation", () => {
|
|
|
657
664
|
});
|
|
658
665
|
|
|
659
666
|
// =============================================================================
|
|
660
|
-
// Tests: useLazyQuery
|
|
667
|
+
// Tests: useLazyQuery (Accessor + Deps pattern)
|
|
661
668
|
// =============================================================================
|
|
662
669
|
|
|
663
670
|
describe("useLazyQuery", () => {
|
|
664
671
|
test("does not execute query on mount", () => {
|
|
665
672
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
666
673
|
|
|
667
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
674
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []));
|
|
668
675
|
|
|
669
676
|
expect(result.current.loading).toBe(false);
|
|
670
677
|
expect(result.current.data).toBe(null);
|
|
@@ -676,7 +683,7 @@ describe("useLazyQuery", () => {
|
|
|
676
683
|
name: "John",
|
|
677
684
|
});
|
|
678
685
|
|
|
679
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
686
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []));
|
|
680
687
|
|
|
681
688
|
let queryResult: { id: string; name: string } | undefined;
|
|
682
689
|
await act(async () => {
|
|
@@ -691,7 +698,7 @@ describe("useLazyQuery", () => {
|
|
|
691
698
|
// Create a mock query that rejects
|
|
692
699
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
693
700
|
|
|
694
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
701
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []));
|
|
695
702
|
|
|
696
703
|
// Set error before execute
|
|
697
704
|
act(() => {
|
|
@@ -717,7 +724,7 @@ describe("useLazyQuery", () => {
|
|
|
717
724
|
},
|
|
718
725
|
} as unknown as QueryResult<{ id: string }>;
|
|
719
726
|
|
|
720
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
727
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []));
|
|
721
728
|
|
|
722
729
|
await act(async () => {
|
|
723
730
|
try {
|
|
@@ -736,7 +743,7 @@ describe("useLazyQuery", () => {
|
|
|
736
743
|
name: "John",
|
|
737
744
|
});
|
|
738
745
|
|
|
739
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
746
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []));
|
|
740
747
|
|
|
741
748
|
await act(async () => {
|
|
742
749
|
await result.current.execute();
|
|
@@ -753,8 +760,8 @@ describe("useLazyQuery", () => {
|
|
|
753
760
|
expect(result.current.loading).toBe(false);
|
|
754
761
|
});
|
|
755
762
|
|
|
756
|
-
test("handles null query", async () => {
|
|
757
|
-
const { result } = renderHook(() => useLazyQuery(null));
|
|
763
|
+
test("handles null query from accessor", async () => {
|
|
764
|
+
const { result } = renderHook(() => useLazyQuery(() => null, []));
|
|
758
765
|
|
|
759
766
|
let queryResult: any;
|
|
760
767
|
await act(async () => {
|
|
@@ -766,8 +773,8 @@ describe("useLazyQuery", () => {
|
|
|
766
773
|
expect(result.current.loading).toBe(false);
|
|
767
774
|
});
|
|
768
775
|
|
|
769
|
-
test("handles undefined query", async () => {
|
|
770
|
-
const { result } = renderHook(() => useLazyQuery(undefined));
|
|
776
|
+
test("handles undefined query from accessor", async () => {
|
|
777
|
+
const { result } = renderHook(() => useLazyQuery(() => undefined, []));
|
|
771
778
|
|
|
772
779
|
let queryResult: any;
|
|
773
780
|
await act(async () => {
|
|
@@ -779,36 +786,6 @@ describe("useLazyQuery", () => {
|
|
|
779
786
|
expect(result.current.loading).toBe(false);
|
|
780
787
|
});
|
|
781
788
|
|
|
782
|
-
test("handles accessor function returning query", async () => {
|
|
783
|
-
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
784
|
-
id: "123",
|
|
785
|
-
name: "John",
|
|
786
|
-
});
|
|
787
|
-
const accessor = () => mockQuery;
|
|
788
|
-
|
|
789
|
-
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
790
|
-
|
|
791
|
-
let queryResult: { id: string; name: string } | undefined;
|
|
792
|
-
await act(async () => {
|
|
793
|
-
queryResult = await result.current.execute();
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
expect(queryResult).toEqual({ id: "123", name: "John" });
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
test("handles accessor function returning null", async () => {
|
|
800
|
-
const accessor = () => null;
|
|
801
|
-
|
|
802
|
-
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
803
|
-
|
|
804
|
-
let queryResult: any;
|
|
805
|
-
await act(async () => {
|
|
806
|
-
queryResult = await result.current.execute();
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
expect(queryResult).toBe(null);
|
|
810
|
-
});
|
|
811
|
-
|
|
812
789
|
test("uses latest query value from accessor on execute", async () => {
|
|
813
790
|
let currentValue = "first";
|
|
814
791
|
const mockQuery1 = createMockQueryResult<string>("first");
|
|
@@ -816,7 +793,7 @@ describe("useLazyQuery", () => {
|
|
|
816
793
|
|
|
817
794
|
const accessor = () => (currentValue === "first" ? mockQuery1 : mockQuery2);
|
|
818
795
|
|
|
819
|
-
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
796
|
+
const { result } = renderHook(() => useLazyQuery(accessor, []));
|
|
820
797
|
|
|
821
798
|
// First execute
|
|
822
799
|
let queryResult1: string | undefined;
|
|
@@ -841,7 +818,7 @@ describe("useLazyQuery", () => {
|
|
|
841
818
|
test("shows loading state during execution", async () => {
|
|
842
819
|
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
843
820
|
|
|
844
|
-
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
821
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []));
|
|
845
822
|
|
|
846
823
|
// Execute and set value
|
|
847
824
|
let executePromise: Promise<{ id: string }>;
|
|
@@ -858,7 +835,7 @@ describe("useLazyQuery", () => {
|
|
|
858
835
|
test("does not update state after unmount", async () => {
|
|
859
836
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
860
837
|
|
|
861
|
-
const { result, unmount } = renderHook(() => useLazyQuery(mockQuery));
|
|
838
|
+
const { result, unmount } = renderHook(() => useLazyQuery(() => mockQuery, []));
|
|
862
839
|
|
|
863
840
|
// Start execution, unmount, then resolve
|
|
864
841
|
const executePromise = result.current.execute();
|
|
@@ -867,72 +844,83 @@ describe("useLazyQuery", () => {
|
|
|
867
844
|
// Resolve after unmount
|
|
868
845
|
await act(async () => {
|
|
869
846
|
mockQuery._setValue({ id: "123", name: "John" });
|
|
870
|
-
|
|
847
|
+
try {
|
|
848
|
+
await executePromise;
|
|
849
|
+
} catch {
|
|
850
|
+
// May reject due to unmount
|
|
851
|
+
}
|
|
871
852
|
});
|
|
872
853
|
|
|
873
854
|
// Test passes if no error is thrown (state update after unmount would cause error)
|
|
874
855
|
expect(true).toBe(true);
|
|
875
856
|
});
|
|
876
857
|
|
|
877
|
-
test("
|
|
878
|
-
|
|
879
|
-
const
|
|
858
|
+
test("can execute multiple times", async () => {
|
|
859
|
+
let count = 0;
|
|
860
|
+
const createQuery = () => {
|
|
861
|
+
count++;
|
|
862
|
+
return createMockQueryResult<{ count: number }>({ count });
|
|
863
|
+
};
|
|
880
864
|
|
|
881
|
-
const { result
|
|
882
|
-
initialProps: { query: mockQuery1 },
|
|
883
|
-
});
|
|
865
|
+
const { result } = renderHook(() => useLazyQuery(() => createQuery(), []));
|
|
884
866
|
|
|
885
|
-
// First execution
|
|
867
|
+
// First execution
|
|
886
868
|
await act(async () => {
|
|
887
|
-
|
|
888
|
-
mockQuery1._setError(new Error("Query failed"));
|
|
889
|
-
try {
|
|
890
|
-
await executePromise;
|
|
891
|
-
} catch {
|
|
892
|
-
// Expected error
|
|
893
|
-
}
|
|
869
|
+
await result.current.execute();
|
|
894
870
|
});
|
|
895
871
|
|
|
896
|
-
expect(result.current.
|
|
897
|
-
|
|
898
|
-
// Switch to successful query
|
|
899
|
-
rerender({ query: mockQuery2 });
|
|
872
|
+
expect(result.current.data?.count).toBe(1);
|
|
900
873
|
|
|
901
|
-
// Second execution
|
|
874
|
+
// Second execution
|
|
902
875
|
await act(async () => {
|
|
903
876
|
await result.current.execute();
|
|
904
877
|
});
|
|
905
878
|
|
|
906
|
-
expect(result.current.
|
|
907
|
-
expect(result.current.data).toEqual({ id: "123" });
|
|
879
|
+
expect(result.current.data?.count).toBe(2);
|
|
908
880
|
});
|
|
909
881
|
|
|
910
|
-
test("
|
|
911
|
-
const
|
|
912
|
-
|
|
882
|
+
test("Route + Params pattern works", async () => {
|
|
883
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
884
|
+
id: "123",
|
|
885
|
+
name: "John",
|
|
886
|
+
});
|
|
887
|
+
const route = (_params: { id: string }) => mockQuery;
|
|
888
|
+
|
|
889
|
+
const { result } = renderHook(() => useLazyQuery(route, { id: "123" }));
|
|
913
890
|
|
|
914
|
-
|
|
915
|
-
|
|
891
|
+
await act(async () => {
|
|
892
|
+
await result.current.execute();
|
|
916
893
|
});
|
|
917
894
|
|
|
918
|
-
|
|
895
|
+
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
test("Route + Params with null route", async () => {
|
|
899
|
+
const { result } = renderHook(() => useLazyQuery(null, { id: "123" }));
|
|
900
|
+
|
|
919
901
|
await act(async () => {
|
|
920
|
-
|
|
921
|
-
mockQuery1._setValue({ count: 1 });
|
|
922
|
-
await executePromise;
|
|
902
|
+
await result.current.execute();
|
|
923
903
|
});
|
|
924
904
|
|
|
925
|
-
expect(result.current.data
|
|
905
|
+
expect(result.current.data).toBe(null);
|
|
906
|
+
});
|
|
926
907
|
|
|
927
|
-
|
|
928
|
-
|
|
908
|
+
test("select transforms the data", async () => {
|
|
909
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
910
|
+
id: "123",
|
|
911
|
+
name: "John",
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
const { result } = renderHook(() =>
|
|
915
|
+
useLazyQuery(() => mockQuery, [], {
|
|
916
|
+
select: (data) => data.name.toUpperCase(),
|
|
917
|
+
}),
|
|
918
|
+
);
|
|
929
919
|
|
|
930
920
|
await act(async () => {
|
|
931
|
-
|
|
932
|
-
mockQuery2._setValue({ count: 2 });
|
|
933
|
-
await executePromise;
|
|
921
|
+
await result.current.execute();
|
|
934
922
|
});
|
|
935
923
|
|
|
936
|
-
expect(result.current.data
|
|
924
|
+
expect(result.current.data).toBe("JOHN");
|
|
937
925
|
});
|
|
938
926
|
});
|
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,
|