@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/dist/index.d.ts
CHANGED
|
@@ -44,9 +44,8 @@ declare function LensProvider({ client, children }: LensProviderProps): ReactEle
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
declare function useLensClient<TRouter = any>(): LensClient<any, any> & TRouter;
|
|
47
|
-
import { MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
48
|
-
|
|
49
|
-
type QueryInput<T> = QueryResult<T> | null | undefined | (() => QueryResult<T> | null | undefined);
|
|
47
|
+
import { LensClient as LensClient2, MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
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,72 +74,117 @@ 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
|
+
/** Client type for callbacks */
|
|
87
|
+
type Client = LensClient2<any, any>;
|
|
88
|
+
/** Route selector - callback that returns a route function */
|
|
89
|
+
type RouteSelector<
|
|
90
|
+
TParams,
|
|
91
|
+
TResult
|
|
92
|
+
> = (client: Client) => ((params: TParams) => QueryResult<TResult>) | null;
|
|
93
|
+
/** Query accessor selector - callback that returns QueryResult */
|
|
94
|
+
type QuerySelector<TResult> = (client: Client) => QueryResult<TResult> | null | undefined;
|
|
95
|
+
/** Mutation selector - callback that returns mutation function */
|
|
96
|
+
type MutationSelector<
|
|
97
|
+
TInput,
|
|
98
|
+
TOutput
|
|
99
|
+
> = (client: Client) => (input: TInput) => Promise<MutationResult<TOutput>>;
|
|
82
100
|
/**
|
|
83
|
-
* Subscribe to a query with reactive updates
|
|
101
|
+
* Subscribe to a query with reactive updates.
|
|
102
|
+
* Client is automatically injected from LensProvider context.
|
|
103
|
+
*
|
|
104
|
+
* Two usage patterns:
|
|
105
|
+
*
|
|
106
|
+
* **1. Route + Params (recommended)** - Stable references, no infinite loops
|
|
107
|
+
* ```tsx
|
|
108
|
+
* const { data } = useQuery((client) => client.user.get, { id: userId });
|
|
109
|
+
* ```
|
|
84
110
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
111
|
+
* **2. Accessor + Deps (escape hatch)** - For complex/composed queries
|
|
112
|
+
* ```tsx
|
|
113
|
+
* const { data } = useQuery((client) => client.user.get({ id }), [id]);
|
|
114
|
+
* ```
|
|
87
115
|
*
|
|
88
116
|
* @example
|
|
89
117
|
* ```tsx
|
|
90
|
-
* // Basic usage
|
|
118
|
+
* // Basic usage - Route + Params
|
|
91
119
|
* function UserProfile({ userId }: { userId: string }) {
|
|
92
|
-
* const
|
|
93
|
-
*
|
|
120
|
+
* const { data: user, loading, error } = useQuery(
|
|
121
|
+
* (client) => client.user.get,
|
|
122
|
+
* { id: userId }
|
|
123
|
+
* );
|
|
94
124
|
*
|
|
95
125
|
* if (loading) return <Spinner />;
|
|
96
126
|
* if (error) return <Error message={error.message} />;
|
|
97
|
-
*
|
|
127
|
+
* return <h1>{user?.name}</h1>;
|
|
128
|
+
* }
|
|
98
129
|
*
|
|
99
|
-
*
|
|
130
|
+
* // With select transform
|
|
131
|
+
* function UserName({ userId }: { userId: string }) {
|
|
132
|
+
* const { data: name } = useQuery(
|
|
133
|
+
* (client) => client.user.get,
|
|
134
|
+
* { id: userId },
|
|
135
|
+
* { select: (user) => user.name }
|
|
136
|
+
* );
|
|
137
|
+
* return <span>{name}</span>;
|
|
100
138
|
* }
|
|
101
139
|
*
|
|
102
|
-
* // Conditional query (null
|
|
140
|
+
* // Conditional query (return null to skip)
|
|
103
141
|
* function SessionInfo({ sessionId }: { sessionId: string | null }) {
|
|
104
|
-
* const client = useLensClient();
|
|
105
142
|
* const { data } = useQuery(
|
|
106
|
-
* sessionId ? client.session.get
|
|
143
|
+
* (client) => sessionId ? client.session.get : null,
|
|
144
|
+
* { id: sessionId ?? '' }
|
|
107
145
|
* );
|
|
108
|
-
* // data is null when sessionId is null
|
|
109
146
|
* return <span>{data?.totalTokens}</span>;
|
|
110
147
|
* }
|
|
111
148
|
*
|
|
112
|
-
* //
|
|
113
|
-
* function
|
|
114
|
-
* const
|
|
115
|
-
*
|
|
116
|
-
*
|
|
149
|
+
* // Skip query with option
|
|
150
|
+
* function ConditionalQuery({ userId, shouldFetch }: { userId: string; shouldFetch: boolean }) {
|
|
151
|
+
* const { data } = useQuery(
|
|
152
|
+
* (client) => client.user.get,
|
|
153
|
+
* { id: userId },
|
|
154
|
+
* { skip: !shouldFetch }
|
|
117
155
|
* );
|
|
118
|
-
* return <span>{data?.totalTokens}</span>;
|
|
119
156
|
* }
|
|
120
157
|
*
|
|
121
|
-
* //
|
|
122
|
-
* function
|
|
123
|
-
* const
|
|
124
|
-
*
|
|
158
|
+
* // Complex queries with accessor (escape hatch)
|
|
159
|
+
* function ComplexQuery({ userId }: { userId: string }) {
|
|
160
|
+
* const { data } = useQuery(
|
|
161
|
+
* (client) => client.user.get({ id: userId }),
|
|
162
|
+
* [userId]
|
|
163
|
+
* );
|
|
125
164
|
* }
|
|
126
165
|
* ```
|
|
127
166
|
*/
|
|
128
|
-
declare function useQuery<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
167
|
+
declare function useQuery<
|
|
168
|
+
TParams,
|
|
169
|
+
TResult,
|
|
170
|
+
TSelected = TResult
|
|
171
|
+
>(selector: RouteSelector<TParams, TResult>, params: TParams, options?: UseQueryOptions<TResult, TSelected>): UseQueryResult<TSelected>;
|
|
172
|
+
declare function useQuery<
|
|
173
|
+
TResult,
|
|
174
|
+
TSelected = TResult
|
|
175
|
+
>(selector: QuerySelector<TResult>, deps: DependencyList, options?: UseQueryOptions<TResult, TSelected>): UseQueryResult<TSelected>;
|
|
134
176
|
/**
|
|
135
|
-
* Execute mutations with loading/error state
|
|
177
|
+
* Execute mutations with loading/error state.
|
|
178
|
+
* Client is automatically injected from LensProvider context.
|
|
136
179
|
*
|
|
137
|
-
* @param
|
|
180
|
+
* @param selector - Callback that returns mutation function from client
|
|
138
181
|
*
|
|
139
182
|
* @example
|
|
140
183
|
* \`\`\`tsx
|
|
141
184
|
* function CreatePost() {
|
|
142
|
-
* const
|
|
143
|
-
*
|
|
185
|
+
* const { mutate, loading, error, data } = useMutation(
|
|
186
|
+
* (client) => client.post.create
|
|
187
|
+
* );
|
|
144
188
|
*
|
|
145
189
|
* const handleSubmit = async (formData: FormData) => {
|
|
146
190
|
* try {
|
|
@@ -166,8 +210,7 @@ type MutationFn<
|
|
|
166
210
|
*
|
|
167
211
|
* // With optimistic updates
|
|
168
212
|
* function UpdatePost({ postId }: { postId: string }) {
|
|
169
|
-
* const
|
|
170
|
-
* const { mutate } = useMutation(client.post.update);
|
|
213
|
+
* const { mutate } = useMutation((client) => client.post.update);
|
|
171
214
|
*
|
|
172
215
|
* const handleUpdate = async (title: string) => {
|
|
173
216
|
* const result = await mutate({ id: postId, title });
|
|
@@ -179,7 +222,7 @@ type MutationFn<
|
|
|
179
222
|
declare function useMutation<
|
|
180
223
|
TInput,
|
|
181
224
|
TOutput
|
|
182
|
-
>(
|
|
225
|
+
>(selector: MutationSelector<TInput, TOutput>): UseMutationResult<TInput, TOutput>;
|
|
183
226
|
/** Result of useLazyQuery hook */
|
|
184
227
|
interface UseLazyQueryResult<T> {
|
|
185
228
|
/** Execute the query */
|
|
@@ -194,47 +237,45 @@ interface UseLazyQueryResult<T> {
|
|
|
194
237
|
reset: () => void;
|
|
195
238
|
}
|
|
196
239
|
/**
|
|
197
|
-
* Execute a query on demand (not on mount)
|
|
198
|
-
*
|
|
199
|
-
* @param queryInput - QueryResult, null/undefined, or accessor function returning QueryResult
|
|
240
|
+
* Execute a query on demand (not on mount).
|
|
241
|
+
* Client is automatically injected from LensProvider context.
|
|
200
242
|
*
|
|
201
243
|
* @example
|
|
202
244
|
* ```tsx
|
|
245
|
+
* // Route + Params pattern
|
|
203
246
|
* function SearchUsers() {
|
|
204
|
-
* const client = useLensClient();
|
|
205
247
|
* const [searchTerm, setSearchTerm] = useState('');
|
|
206
248
|
* const { execute, data, loading } = useLazyQuery(
|
|
207
|
-
* client.user.search
|
|
249
|
+
* (client) => client.user.search,
|
|
250
|
+
* { query: searchTerm }
|
|
208
251
|
* );
|
|
209
252
|
*
|
|
210
|
-
* const handleSearch = async () => {
|
|
211
|
-
* const users = await execute();
|
|
212
|
-
* console.log('Found:', users);
|
|
213
|
-
* };
|
|
214
|
-
*
|
|
215
253
|
* return (
|
|
216
254
|
* <div>
|
|
217
|
-
* <input
|
|
218
|
-
*
|
|
219
|
-
* onChange={e => setSearchTerm(e.target.value)}
|
|
220
|
-
* />
|
|
221
|
-
* <button onClick={handleSearch} disabled={loading}>
|
|
222
|
-
* Search
|
|
223
|
-
* </button>
|
|
255
|
+
* <input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} />
|
|
256
|
+
* <button onClick={execute} disabled={loading}>Search</button>
|
|
224
257
|
* {data?.map(user => <UserCard key={user.id} user={user} />)}
|
|
225
258
|
* </div>
|
|
226
259
|
* );
|
|
227
260
|
* }
|
|
228
261
|
*
|
|
229
|
-
* //
|
|
230
|
-
* function
|
|
231
|
-
* const
|
|
232
|
-
*
|
|
233
|
-
*
|
|
262
|
+
* // Accessor pattern
|
|
263
|
+
* function LazyComplexQuery({ userId }: { userId: string }) {
|
|
264
|
+
* const { execute, data } = useLazyQuery(
|
|
265
|
+
* (client) => client.user.get({ id: userId }),
|
|
266
|
+
* [userId]
|
|
234
267
|
* );
|
|
235
268
|
* return <button onClick={execute}>Load</button>;
|
|
236
269
|
* }
|
|
237
270
|
* ```
|
|
238
271
|
*/
|
|
239
|
-
declare function useLazyQuery<
|
|
240
|
-
|
|
272
|
+
declare function useLazyQuery<
|
|
273
|
+
TParams,
|
|
274
|
+
TResult,
|
|
275
|
+
TSelected = TResult
|
|
276
|
+
>(selector: RouteSelector<TParams, TResult>, params: TParams, options?: UseQueryOptions<TResult, TSelected>): UseLazyQueryResult<TSelected>;
|
|
277
|
+
declare function useLazyQuery<
|
|
278
|
+
TResult,
|
|
279
|
+
TSelected = TResult
|
|
280
|
+
>(selector: QuerySelector<TResult>, deps: DependencyList, options?: UseQueryOptions<TResult, TSelected>): UseLazyQueryResult<TSelected>;
|
|
281
|
+
export { useQuery, useMutation, useLensClient, useLazyQuery, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, RouteSelector, QuerySelector, MutationSelector, LensProviderProps, LensProvider };
|
package/dist/index.js
CHANGED
|
@@ -17,20 +17,37 @@ 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
|
-
const query = useMemo(() =>
|
|
20
|
+
function useQuery(selector, paramsOrDeps, options) {
|
|
21
|
+
const client = useLensClient();
|
|
22
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
23
|
+
const paramsKey = !isAccessorMode ? JSON.stringify(paramsOrDeps) : null;
|
|
24
|
+
const query = useMemo(() => {
|
|
25
|
+
if (options?.skip)
|
|
26
|
+
return null;
|
|
27
|
+
if (isAccessorMode) {
|
|
28
|
+
const querySelector = selector;
|
|
29
|
+
return querySelector(client);
|
|
30
|
+
}
|
|
31
|
+
const routeSelector = selector;
|
|
32
|
+
const route = routeSelector(client);
|
|
33
|
+
if (!route)
|
|
34
|
+
return null;
|
|
35
|
+
return route(paramsOrDeps);
|
|
36
|
+
}, isAccessorMode ? [client, options?.skip, ...paramsOrDeps] : [client, selector, paramsKey, options?.skip]);
|
|
37
|
+
const selectRef = useRef(options?.select);
|
|
38
|
+
selectRef.current = options?.select;
|
|
25
39
|
const [data, setData] = useState(null);
|
|
26
|
-
const [loading, setLoading] = useState(
|
|
40
|
+
const [loading, setLoading] = useState(query != null && !options?.skip);
|
|
27
41
|
const [error, setError] = useState(null);
|
|
28
42
|
const mountedRef = useRef(true);
|
|
29
43
|
const queryRef = useRef(query);
|
|
30
44
|
queryRef.current = query;
|
|
45
|
+
const transform = useCallback((value) => {
|
|
46
|
+
return selectRef.current ? selectRef.current(value) : value;
|
|
47
|
+
}, []);
|
|
31
48
|
useEffect(() => {
|
|
32
49
|
mountedRef.current = true;
|
|
33
|
-
if (query == null
|
|
50
|
+
if (query == null) {
|
|
34
51
|
setData(null);
|
|
35
52
|
setLoading(false);
|
|
36
53
|
setError(null);
|
|
@@ -38,15 +55,19 @@ function useQuery(queryInput, options) {
|
|
|
38
55
|
}
|
|
39
56
|
setLoading(true);
|
|
40
57
|
setError(null);
|
|
58
|
+
let hasReceivedData = false;
|
|
41
59
|
const unsubscribe = query.subscribe((value) => {
|
|
42
60
|
if (mountedRef.current) {
|
|
43
|
-
|
|
61
|
+
hasReceivedData = true;
|
|
62
|
+
setData(transform(value));
|
|
44
63
|
setLoading(false);
|
|
45
64
|
}
|
|
46
65
|
});
|
|
47
66
|
query.then((value) => {
|
|
67
|
+
if (mountedRef.current && !hasReceivedData) {
|
|
68
|
+
setData(transform(value));
|
|
69
|
+
}
|
|
48
70
|
if (mountedRef.current) {
|
|
49
|
-
setData(value);
|
|
50
71
|
setLoading(false);
|
|
51
72
|
}
|
|
52
73
|
}, (err) => {
|
|
@@ -59,16 +80,16 @@ function useQuery(queryInput, options) {
|
|
|
59
80
|
mountedRef.current = false;
|
|
60
81
|
unsubscribe();
|
|
61
82
|
};
|
|
62
|
-
}, [query,
|
|
83
|
+
}, [query, transform]);
|
|
63
84
|
const refetch = useCallback(() => {
|
|
64
85
|
const currentQuery = queryRef.current;
|
|
65
|
-
if (currentQuery == null
|
|
86
|
+
if (currentQuery == null)
|
|
66
87
|
return;
|
|
67
88
|
setLoading(true);
|
|
68
89
|
setError(null);
|
|
69
90
|
currentQuery.then((value) => {
|
|
70
91
|
if (mountedRef.current) {
|
|
71
|
-
setData(value);
|
|
92
|
+
setData(transform(value));
|
|
72
93
|
setLoading(false);
|
|
73
94
|
}
|
|
74
95
|
}, (err) => {
|
|
@@ -77,14 +98,18 @@ function useQuery(queryInput, options) {
|
|
|
77
98
|
setLoading(false);
|
|
78
99
|
}
|
|
79
100
|
});
|
|
80
|
-
}, [
|
|
101
|
+
}, [transform]);
|
|
81
102
|
return { data, loading, error, refetch };
|
|
82
103
|
}
|
|
83
|
-
function useMutation(
|
|
104
|
+
function useMutation(selector) {
|
|
105
|
+
const client = useLensClient();
|
|
106
|
+
const mutationFn = selector(client);
|
|
84
107
|
const [loading, setLoading] = useState(false);
|
|
85
108
|
const [error, setError] = useState(null);
|
|
86
109
|
const [data, setData] = useState(null);
|
|
87
110
|
const mountedRef = useRef(true);
|
|
111
|
+
const mutationRef = useRef(mutationFn);
|
|
112
|
+
mutationRef.current = mutationFn;
|
|
88
113
|
useEffect(() => {
|
|
89
114
|
mountedRef.current = true;
|
|
90
115
|
return () => {
|
|
@@ -95,7 +120,7 @@ function useMutation(mutationFn) {
|
|
|
95
120
|
setLoading(true);
|
|
96
121
|
setError(null);
|
|
97
122
|
try {
|
|
98
|
-
const result = await
|
|
123
|
+
const result = await mutationRef.current(input);
|
|
99
124
|
if (mountedRef.current) {
|
|
100
125
|
setData(result.data);
|
|
101
126
|
}
|
|
@@ -111,7 +136,7 @@ function useMutation(mutationFn) {
|
|
|
111
136
|
setLoading(false);
|
|
112
137
|
}
|
|
113
138
|
}
|
|
114
|
-
}, [
|
|
139
|
+
}, []);
|
|
115
140
|
const reset = useCallback(() => {
|
|
116
141
|
setLoading(false);
|
|
117
142
|
setError(null);
|
|
@@ -119,13 +144,19 @@ function useMutation(mutationFn) {
|
|
|
119
144
|
}, []);
|
|
120
145
|
return { mutate, loading, error, data, reset };
|
|
121
146
|
}
|
|
122
|
-
function useLazyQuery(
|
|
147
|
+
function useLazyQuery(selector, paramsOrDeps, options) {
|
|
148
|
+
const client = useLensClient();
|
|
123
149
|
const [data, setData] = useState(null);
|
|
124
150
|
const [loading, setLoading] = useState(false);
|
|
125
151
|
const [error, setError] = useState(null);
|
|
126
152
|
const mountedRef = useRef(true);
|
|
127
|
-
const
|
|
128
|
-
|
|
153
|
+
const isAccessorMode = Array.isArray(paramsOrDeps);
|
|
154
|
+
const selectorRef = useRef(selector);
|
|
155
|
+
selectorRef.current = selector;
|
|
156
|
+
const paramsOrDepsRef = useRef(paramsOrDeps);
|
|
157
|
+
paramsOrDepsRef.current = paramsOrDeps;
|
|
158
|
+
const selectRef = useRef(options?.select);
|
|
159
|
+
selectRef.current = options?.select;
|
|
129
160
|
useEffect(() => {
|
|
130
161
|
mountedRef.current = true;
|
|
131
162
|
return () => {
|
|
@@ -133,7 +164,17 @@ function useLazyQuery(queryInput) {
|
|
|
133
164
|
};
|
|
134
165
|
}, []);
|
|
135
166
|
const execute = useCallback(async () => {
|
|
136
|
-
|
|
167
|
+
let query;
|
|
168
|
+
if (isAccessorMode) {
|
|
169
|
+
const querySelector = selectorRef.current;
|
|
170
|
+
query = querySelector(client);
|
|
171
|
+
} else {
|
|
172
|
+
const routeSelector = selectorRef.current;
|
|
173
|
+
const route = routeSelector(client);
|
|
174
|
+
if (route) {
|
|
175
|
+
query = route(paramsOrDepsRef.current);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
137
178
|
if (query == null) {
|
|
138
179
|
setData(null);
|
|
139
180
|
setLoading(false);
|
|
@@ -143,10 +184,11 @@ function useLazyQuery(queryInput) {
|
|
|
143
184
|
setError(null);
|
|
144
185
|
try {
|
|
145
186
|
const result = await query;
|
|
187
|
+
const selected = selectRef.current ? selectRef.current(result) : result;
|
|
146
188
|
if (mountedRef.current) {
|
|
147
|
-
setData(
|
|
189
|
+
setData(selected);
|
|
148
190
|
}
|
|
149
|
-
return
|
|
191
|
+
return selected;
|
|
150
192
|
} catch (err) {
|
|
151
193
|
const queryError = err instanceof Error ? err : new Error(String(err));
|
|
152
194
|
if (mountedRef.current) {
|
|
@@ -158,7 +200,7 @@ function useLazyQuery(queryInput) {
|
|
|
158
200
|
setLoading(false);
|
|
159
201
|
}
|
|
160
202
|
}
|
|
161
|
-
}, []);
|
|
203
|
+
}, [client, isAccessorMode]);
|
|
162
204
|
const reset = useCallback(() => {
|
|
163
205
|
setLoading(false);
|
|
164
206
|
setError(null);
|