@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 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
- /** Query input - can be a query, null/undefined, or an accessor function */
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
- * @param queryInput - QueryResult, null/undefined, or accessor function returning QueryResult
86
- * @param options - Query options
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 client = useLensClient();
93
- * const { data: user, loading, error } = useQuery(client.user.get({ id: userId }));
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
- * if (!user) return <NotFound />;
127
+ * return <h1>{user?.name}</h1>;
128
+ * }
98
129
  *
99
- * return <h1>{user.name}</h1>;
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 when condition not met)
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({ id: sessionId }) : null
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
- * // Accessor function (reactive inputs)
113
- * function ReactiveQuery({ sessionId }: { sessionId: Signal<string | null> }) {
114
- * const client = useLensClient();
115
- * const { data } = useQuery(() =>
116
- * sessionId.value ? client.session.get({ id: sessionId.value }) : null
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
- * // Skip query conditionally
122
- * function ConditionalQuery({ shouldFetch }: { shouldFetch: boolean }) {
123
- * const client = useLensClient();
124
- * const { data } = useQuery(client.user.list(), { skip: !shouldFetch });
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<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>>;
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 mutationFn - Mutation function from client API
180
+ * @param selector - Callback that returns mutation function from client
138
181
  *
139
182
  * @example
140
183
  * \`\`\`tsx
141
184
  * function CreatePost() {
142
- * const client = useLensClient();
143
- * const { mutate, loading, error, data } = useMutation(client.post.create);
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 client = useLensClient();
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
- >(mutationFn: MutationFn<TInput, TOutput>): UseMutationResult<TInput, TOutput>;
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({ query: searchTerm })
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
- * value={searchTerm}
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
- * // With accessor function
230
- * function LazyReactiveQuery({ sessionId }: { sessionId: Signal<string | null> }) {
231
- * const client = useLensClient();
232
- * const { execute, data } = useLazyQuery(() =>
233
- * sessionId.value ? client.session.get({ id: sessionId.value }) : null
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<T>(queryInput: QueryInput<T>): UseLazyQueryResult<T>;
240
- export { useQuery, useMutation, useLensClient, useLazyQuery, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, QueryInput, MutationFn, LensProviderProps, LensProvider };
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 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(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(!options?.skip && query != null);
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 || options?.skip) {
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
- setData(value);
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, options?.skip]);
83
+ }, [query, transform]);
63
84
  const refetch = useCallback(() => {
64
85
  const currentQuery = queryRef.current;
65
- if (currentQuery == null || options?.skip)
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
- }, [options?.skip]);
101
+ }, [transform]);
81
102
  return { data, loading, error, refetch };
82
103
  }
83
- function useMutation(mutationFn) {
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 mutationFn(input);
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
- }, [mutationFn]);
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(queryInput) {
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 queryInputRef = useRef(queryInput);
128
- queryInputRef.current = queryInput;
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
- const query = resolveQuery(queryInputRef.current);
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(result);
189
+ setData(selected);
148
190
  }
149
- return result;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-react",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "React bindings for Lens API framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",