@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/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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "React bindings for Lens API framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "SylphxAI",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@sylphx/lens-client": "^
|
|
33
|
+
"@sylphx/lens-client": "^2.0.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": ">=18.0.0"
|