@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 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
- /** Query input - can be a query, null/undefined, or an accessor function */
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
- * @param queryInput - QueryResult, null/undefined, or accessor function returning QueryResult
86
- * @param options - Query options
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({ id: userId }));
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
- * if (!user) return <NotFound />;
122
+ * return <h1>{user?.name}</h1>;
123
+ * }
98
124
  *
99
- * return <h1>{user.name}</h1>;
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 (null when condition not met)
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({ id: sessionId }) : null
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
- * // Accessor function (reactive inputs)
113
- * function ReactiveQuery({ sessionId }: { sessionId: Signal<string | null> }) {
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
- * // Skip query conditionally
122
- * function ConditionalQuery({ shouldFetch }: { shouldFetch: boolean }) {
150
+ * // Complex queries with accessor (escape hatch)
151
+ * function ComplexQuery({ userId }: { userId: string }) {
123
152
  * const client = useLensClient();
124
- * const { data } = useQuery(client.user.list(), { skip: !shouldFetch });
153
+ * const { data } = useQuery(
154
+ * () => client.user.get({ id: userId }),
155
+ * [userId]
156
+ * );
125
157
  * }
126
158
  * ```
127
159
  */
128
- declare function useQuery<T>(queryInput: QueryInput<T>, options?: UseQueryOptions): UseQueryResult<T>;
129
- /** Mutation function type */
130
- type MutationFn<
131
- TInput,
132
- TOutput
133
- > = (input: TInput) => Promise<MutationResult<TOutput>>;
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({ query: searchTerm })
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
- * value={searchTerm}
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
- * // With accessor function
230
- * function LazyReactiveQuery({ sessionId }: { sessionId: Signal<string | null> }) {
254
+ * // Accessor pattern
255
+ * function LazyComplexQuery({ userId }: { userId: string }) {
231
256
  * const client = useLensClient();
232
- * const { execute, data } = useLazyQuery(() =>
233
- * sessionId.value ? client.session.get({ id: sessionId.value }) : null
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<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T>;
240
- export { useQuery, useMutation, useLensClient, useLazyQuery, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, QueryInput, MutationFn, LensProviderProps, LensProvider };
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 resolveQuery(input) {
21
- return typeof input === "function" ? input() : input;
22
- }
23
- function useQuery(queryInput, options) {
24
- const query = useMemo(() => resolveQuery(queryInput), [queryInput]);
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(!options?.skip && query != null);
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 || options?.skip) {
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, options?.skip]);
77
+ }, [query, transform]);
63
78
  const refetch = useCallback(() => {
64
79
  const currentQuery = queryRef.current;
65
- if (currentQuery == null || options?.skip)
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
- }, [options?.skip]);
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(queryInput) {
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 queryInputRef = useRef(queryInput);
128
- queryInputRef.current = queryInput;
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
- const query = resolveQuery(queryInputRef.current);
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(result);
177
+ setData(selected);
148
178
  }
149
- return result;
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": "1.2.22",
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": "^1.15.3"
33
+ "@sylphx/lens-client": "^2.0.1"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=18.0.0"