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