@sylphx/lens-react 2.3.0 → 2.3.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
@@ -1,4 +1,4 @@
1
- import { LensClientConfig, SelectionObject, TypedClientConfig } from "@sylphx/lens-client";
1
+ import { LensClientConfig, QueryResult, SelectionObject, TypedClientConfig } from "@sylphx/lens-client";
2
2
  import { MutationDef, QueryDef, RouterDef, RouterRoutes } from "@sylphx/lens-core";
3
3
  /** Query hook options */
4
4
  interface QueryHookOptions<TInput> {
@@ -48,34 +48,33 @@ interface MutationHookResult<
48
48
  /** Reset mutation state */
49
49
  reset: () => void;
50
50
  }
51
- /** Query endpoint type */
51
+ /** Query endpoint with React hooks */
52
52
  interface QueryEndpoint<
53
53
  TInput,
54
54
  TOutput
55
55
  > {
56
- /** Hook call (in component) */
57
- <_TSelect extends SelectionObject = Record<string, never>>(options: TInput extends void ? QueryHookOptions<void> | void : QueryHookOptions<TInput>): QueryHookResult<TOutput>;
58
- /** Promise call (SSR) */
59
- fetch: <TSelect extends SelectionObject = Record<string, never>>(options: TInput extends void ? {
60
- input?: void;
61
- select?: TSelect;
62
- } | void : {
63
- input: TInput;
64
- select?: TSelect;
65
- }) => Promise<TOutput>;
56
+ /** Vanilla JS call - returns QueryResult (Promise + Observable) */
57
+ (options?: {
58
+ input?: TInput;
59
+ select?: SelectionObject;
60
+ }): QueryResult<TOutput>;
61
+ /** React hook for reactive queries */
62
+ useQuery: (options?: TInput extends void ? QueryHookOptions<void> | void : QueryHookOptions<TInput>) => QueryHookResult<TOutput>;
66
63
  }
67
- /** Mutation endpoint type */
64
+ /** Mutation endpoint with React hooks */
68
65
  interface MutationEndpoint<
69
66
  TInput,
70
67
  TOutput
71
68
  > {
72
- /** Hook call (in component) */
73
- (options?: MutationHookOptions<TOutput>): MutationHookResult<TInput, TOutput>;
74
- /** Promise call (SSR) */
75
- fetch: <TSelect extends SelectionObject = Record<string, never>>(options: {
69
+ /** Vanilla JS call - returns Promise */
70
+ (options: {
76
71
  input: TInput;
77
- select?: TSelect;
78
- }) => Promise<TOutput>;
72
+ select?: SelectionObject;
73
+ }): Promise<{
74
+ data: TOutput;
75
+ }>;
76
+ /** React hook for mutations */
77
+ useMutation: (options?: MutationHookOptions<TOutput>) => MutationHookResult<TInput, TOutput>;
79
78
  }
80
79
  /** Infer client type from router routes */
81
80
  type InferTypedClient<TRoutes extends RouterRoutes> = { [K in keyof TRoutes] : TRoutes[K] extends RouterDef<infer TNestedRoutes> ? InferTypedClient<TNestedRoutes> : TRoutes[K] extends QueryDef<infer TInput, infer TOutput> ? QueryEndpoint<TInput, TOutput> : TRoutes[K] extends MutationDef<infer TInput, infer TOutput> ? MutationEndpoint<TInput, TOutput> : never };
@@ -84,9 +83,8 @@ type TypedClient<TRouter extends RouterDef> = TRouter extends RouterDef<infer TR
84
83
  /**
85
84
  * Create a Lens client with React hooks.
86
85
  *
87
- * Each endpoint can be called:
88
- * - Directly as a hook: `client.user.get({ input: { id } })`
89
- * - Via .fetch() for promises: `await client.user.get.fetch({ input: { id } })`
86
+ * Base client methods work in vanilla JS (SSR, utilities, event handlers).
87
+ * React hooks are available as `.useQuery()` and `.useMutation()`.
90
88
  *
91
89
  * @example
92
90
  * ```tsx
@@ -99,20 +97,14 @@ type TypedClient<TRouter extends RouterDef> = TRouter extends RouterDef<infer TR
99
97
  * transport: httpTransport({ url: '/api/lens' }),
100
98
  * });
101
99
  *
102
- * // Component usage
103
- * function UserProfile({ id }: { id: string }) {
104
- * // Query hook - auto-subscribes
105
- * const { data, loading, error } = client.user.get({
106
- * input: { id },
107
- * select: { name: true },
108
- * });
100
+ * // Vanilla JS (anywhere)
101
+ * const user = await client.user.get({ input: { id } });
109
102
  *
110
- * // Mutation hook - returns mutate function
111
- * const { mutate, loading: saving } = client.user.update({
112
- * onSuccess: () => toast('Updated!'),
113
- * });
103
+ * // React component
104
+ * function UserProfile({ id }: { id: string }) {
105
+ * const { data, loading } = client.user.get.useQuery({ input: { id } });
106
+ * const { mutate } = client.user.update.useMutation();
114
107
  *
115
- * if (loading) return <Spinner />;
116
108
  * return (
117
109
  * <div>
118
110
  * <h1>{data?.name}</h1>
@@ -122,12 +114,6 @@ type TypedClient<TRouter extends RouterDef> = TRouter extends RouterDef<infer TR
122
114
  * </div>
123
115
  * );
124
116
  * }
125
- *
126
- * // SSR usage
127
- * async function UserPage({ id }: { id: string }) {
128
- * const user = await client.user.get.fetch({ input: { id } });
129
- * return <div>{user.name}</div>;
130
- * }
131
117
  * ```
132
118
  */
133
119
  declare function createClient<TRouter extends RouterDef>(config: LensClientConfig | TypedClientConfig<{
@@ -179,7 +165,7 @@ declare function LensProvider({ client, children }: LensProviderProps): ReactEle
179
165
  * ```
180
166
  */
181
167
  declare function useLensClient<TRouter = any>(): LensClient<any, any> & TRouter;
182
- import { LensClient as LensClient2, MutationResult, QueryResult } from "@sylphx/lens-client";
168
+ import { LensClient as LensClient2, MutationResult, QueryResult as QueryResult2 } from "@sylphx/lens-client";
183
169
  import { DependencyList } from "react";
184
170
  /** Result of useQuery hook */
185
171
  interface UseQueryResult<T> {
@@ -224,9 +210,9 @@ type Client = LensClient2<any, any>;
224
210
  type RouteSelector<
225
211
  TParams,
226
212
  TResult
227
- > = (client: Client) => ((params: TParams) => QueryResult<TResult>) | null;
213
+ > = (client: Client) => ((params: TParams) => QueryResult2<TResult>) | null;
228
214
  /** Query accessor selector - callback that returns QueryResult */
229
- type QuerySelector<TResult> = (client: Client) => QueryResult<TResult> | null | undefined;
215
+ type QuerySelector<TResult> = (client: Client) => QueryResult2<TResult> | null | undefined;
230
216
  /** Mutation selector - callback that returns mutation function */
231
217
  type MutationSelector<
232
218
  TInput,
@@ -299,12 +285,12 @@ type MutationSelector<
299
285
  * }
300
286
  * ```
301
287
  */
302
- declare function useQuery<
288
+ declare function useQuery2<
303
289
  TParams,
304
290
  TResult,
305
291
  TSelected = TResult
306
292
  >(selector: RouteSelector<TParams, TResult>, params: TParams, options?: UseQueryOptions<TResult, TSelected>): UseQueryResult<TSelected>;
307
- declare function useQuery<
293
+ declare function useQuery2<
308
294
  TResult,
309
295
  TSelected = TResult
310
296
  >(selector: QuerySelector<TResult>, deps: DependencyList, options?: UseQueryOptions<TResult, TSelected>): UseQueryResult<TSelected>;
@@ -354,7 +340,7 @@ declare function useQuery<
354
340
  * }
355
341
  * \`\`\`
356
342
  */
357
- declare function useMutation<
343
+ declare function useMutation2<
358
344
  TInput,
359
345
  TOutput
360
346
  >(selector: MutationSelector<TInput, TOutput>): UseMutationResult<TInput, TOutput>;
@@ -413,4 +399,4 @@ declare function useLazyQuery<
413
399
  TResult,
414
400
  TSelected = TResult
415
401
  >(selector: QuerySelector<TResult>, deps: DependencyList, options?: UseQueryOptions<TResult, TSelected>): UseLazyQueryResult<TSelected>;
416
- export { useQuery, useMutation, useLensClient, useLazyQuery, createClient, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, TypedClient, RouteSelector, QuerySelector, QueryHookResult, QueryHookOptions, QueryEndpoint, MutationSelector, MutationHookResult, MutationHookOptions, MutationEndpoint, LensProviderProps, LensProvider };
402
+ export { useQuery2 as useQuery, useMutation2 as useMutation, useLensClient, useLazyQuery, createClient, UseQueryResult, UseQueryOptions, UseMutationResult, UseLazyQueryResult, TypedClient, RouteSelector, QuerySelector, QueryHookResult, QueryHookOptions, QueryEndpoint, MutationSelector, MutationHookResult, MutationHookOptions, MutationEndpoint, LensProviderProps, LensProvider };
package/dist/index.js CHANGED
@@ -2,8 +2,7 @@
2
2
  import {
3
3
  createClient as createBaseClient
4
4
  } from "@sylphx/lens-client";
5
- import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
6
- import * as React from "react";
5
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
7
6
  function queryReducer(state, action) {
8
7
  switch (action.type) {
9
8
  case "RESET":
@@ -20,24 +19,14 @@ function queryReducer(state, action) {
20
19
  return state;
21
20
  }
22
21
  }
23
- function createQueryHook(baseClient, path) {
24
- let cachedHook = null;
25
- const getEndpoint = (p) => {
26
- const parts = p.split(".");
27
- let current = baseClient;
28
- for (const part of parts) {
29
- current = current[part];
30
- }
31
- return current;
32
- };
33
- const useQueryHook = (options) => {
34
- const _optionsKey = JSON.stringify(options ?? {});
22
+ function createUseQueryHook(getEndpoint) {
23
+ return function useQuery(options) {
35
24
  const query = useMemo(() => {
36
25
  if (options?.skip)
37
26
  return null;
38
- const endpoint2 = getEndpoint(path);
39
- return endpoint2({ input: options?.input, select: options?.select });
40
- }, [options?.input, options?.select, options?.skip, path]);
27
+ const endpoint = getEndpoint();
28
+ return endpoint({ input: options?.input, select: options?.select });
29
+ }, [options?.input, options?.select, options?.skip, getEndpoint]);
41
30
  const initialState = {
42
31
  data: null,
43
32
  loading: query != null && !options?.skip,
@@ -102,30 +91,12 @@ function createQueryHook(baseClient, path) {
102
91
  }, []);
103
92
  return { data: state.data, loading: state.loading, error: state.error, refetch };
104
93
  };
105
- const fetch = async (options) => {
106
- const endpoint2 = getEndpoint(path);
107
- const queryResult = endpoint2({ input: options?.input, select: options?.select });
108
- return queryResult.then((data) => data);
109
- };
110
- const endpoint = useQueryHook;
111
- endpoint.fetch = fetch;
112
- cachedHook = endpoint;
113
- return cachedHook;
114
94
  }
115
- function createMutationHook(baseClient, path) {
116
- let cachedHook = null;
117
- const getEndpoint = (p) => {
118
- const parts = p.split(".");
119
- let current = baseClient;
120
- for (const part of parts) {
121
- current = current[part];
122
- }
123
- return current;
124
- };
125
- const useMutationHook = (hookOptions) => {
126
- const [loading, setLoading] = React.useState(false);
127
- const [error, setError] = React.useState(null);
128
- const [data, setData] = React.useState(null);
95
+ function createUseMutationHook(getEndpoint) {
96
+ return function useMutation(hookOptions) {
97
+ const [loading, setLoading] = useState(false);
98
+ const [error, setError] = useState(null);
99
+ const [data, setData] = useState(null);
129
100
  const mountedRef = useRef(true);
130
101
  const hookOptionsRef = useRef(hookOptions);
131
102
  hookOptionsRef.current = hookOptions;
@@ -139,16 +110,15 @@ function createMutationHook(baseClient, path) {
139
110
  setLoading(true);
140
111
  setError(null);
141
112
  try {
142
- const endpoint2 = getEndpoint(path);
143
- const result = await endpoint2({ input: options.input, select: options.select });
144
- const mutationResult = result;
113
+ const endpoint = getEndpoint();
114
+ const result = await endpoint({ input: options.input, select: options.select });
145
115
  if (mountedRef.current) {
146
- setData(mutationResult.data);
116
+ setData(result.data);
147
117
  setLoading(false);
148
118
  }
149
- hookOptionsRef.current?.onSuccess?.(mutationResult.data);
119
+ hookOptionsRef.current?.onSuccess?.(result.data);
150
120
  hookOptionsRef.current?.onSettled?.();
151
- return mutationResult.data;
121
+ return result.data;
152
122
  } catch (err) {
153
123
  const mutationError = err instanceof Error ? err : new Error(String(err));
154
124
  if (mountedRef.current) {
@@ -159,7 +129,7 @@ function createMutationHook(baseClient, path) {
159
129
  hookOptionsRef.current?.onSettled?.();
160
130
  throw mutationError;
161
131
  }
162
- }, [path]);
132
+ }, [getEndpoint]);
163
133
  const reset = useCallback(() => {
164
134
  setLoading(false);
165
135
  setError(null);
@@ -167,46 +137,45 @@ function createMutationHook(baseClient, path) {
167
137
  }, []);
168
138
  return { mutate, loading, error, data, reset };
169
139
  };
170
- const fetch = async (options) => {
171
- const endpoint2 = getEndpoint(path);
172
- const result = await endpoint2({ input: options.input, select: options.select });
173
- const mutationResult = result;
174
- return mutationResult.data;
175
- };
176
- const endpoint = useMutationHook;
177
- endpoint.fetch = fetch;
178
- cachedHook = endpoint;
179
- return cachedHook;
180
140
  }
181
141
  var hookCache = new Map;
182
142
  function createClient(config) {
183
143
  const baseClient = createBaseClient(config);
184
- const _endpointTypes = new Map;
185
144
  function createProxy(path) {
186
- const cacheKey = path;
187
- if (hookCache.has(cacheKey)) {
188
- return hookCache.get(cacheKey);
189
- }
190
145
  const handler = {
191
146
  get(_target, prop) {
192
147
  if (typeof prop === "symbol")
193
148
  return;
194
149
  const key = prop;
195
- if (key === "fetch") {
196
- return async (options) => {
197
- const parts = path.split(".");
198
- let current = baseClient;
199
- for (const part of parts) {
200
- current = current[part];
201
- }
202
- const endpointFn = current;
203
- const queryResult = endpointFn(options);
204
- const result = await queryResult;
205
- if (result && typeof result === "object" && "data" in result && Object.keys(result).length === 1) {
206
- return result.data;
207
- }
208
- return result;
209
- };
150
+ if (key === "useQuery") {
151
+ const cacheKey = `${path}:useQuery`;
152
+ if (!hookCache.has(cacheKey)) {
153
+ const getEndpoint = () => {
154
+ const parts = path.split(".");
155
+ let current = baseClient;
156
+ for (const part of parts) {
157
+ current = current[part];
158
+ }
159
+ return current;
160
+ };
161
+ hookCache.set(cacheKey, createUseQueryHook(getEndpoint));
162
+ }
163
+ return hookCache.get(cacheKey);
164
+ }
165
+ if (key === "useMutation") {
166
+ const cacheKey = `${path}:useMutation`;
167
+ if (!hookCache.has(cacheKey)) {
168
+ const getEndpoint = () => {
169
+ const parts = path.split(".");
170
+ let current = baseClient;
171
+ for (const part of parts) {
172
+ current = current[part];
173
+ }
174
+ return current;
175
+ };
176
+ hookCache.set(cacheKey, createUseMutationHook(getEndpoint));
177
+ }
178
+ return hookCache.get(cacheKey);
210
179
  }
211
180
  if (key === "then")
212
181
  return;
@@ -216,30 +185,13 @@ function createClient(config) {
216
185
  return createProxy(newPath);
217
186
  },
218
187
  apply(_target, _thisArg, args) {
219
- const options = args[0];
220
- const isQueryOptions = options && (("input" in options) || ("select" in options) || ("skip" in options));
221
- const isMutationOptions = !options || !isQueryOptions && (Object.keys(options).length === 0 || ("onSuccess" in options) || ("onError" in options) || ("onSettled" in options));
222
- const cacheKeyQuery = `${path}:query`;
223
- const cacheKeyMutation = `${path}:mutation`;
224
- if (isQueryOptions) {
225
- if (!hookCache.has(cacheKeyQuery)) {
226
- hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
227
- }
228
- const hook2 = hookCache.get(cacheKeyQuery);
229
- return hook2(options);
230
- }
231
- if (isMutationOptions) {
232
- if (!hookCache.has(cacheKeyMutation)) {
233
- hookCache.set(cacheKeyMutation, createMutationHook(baseClient, path));
234
- }
235
- const hook2 = hookCache.get(cacheKeyMutation);
236
- return hook2(options);
237
- }
238
- if (!hookCache.has(cacheKeyQuery)) {
239
- hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
188
+ const parts = path.split(".");
189
+ let current = baseClient;
190
+ for (const part of parts) {
191
+ current = current[part];
240
192
  }
241
- const hook = hookCache.get(cacheKeyQuery);
242
- return hook(options);
193
+ const endpoint = current;
194
+ return endpoint(args[0]);
243
195
  }
244
196
  };
245
197
  const proxy = new Proxy(() => {}, handler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-react",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "description": "React bindings for Lens API framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -30,8 +30,8 @@
30
30
  "author": "SylphxAI",
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
- "@sylphx/lens-client": "^2.2.0",
34
- "@sylphx/lens-core": "^2.1.0"
33
+ "@sylphx/lens-client": "^2.3.0",
34
+ "@sylphx/lens-core": "^2.2.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": ">=18.0.0"
package/src/create.tsx CHANGED
@@ -2,7 +2,7 @@
2
2
  * @sylphx/lens-react - Create Client
3
3
  *
4
4
  * Creates a typed Lens client with React hooks.
5
- * Each endpoint can be called directly as a hook or via .fetch() for promises.
5
+ * Base client methods work in vanilla JS, hooks are extensions.
6
6
  *
7
7
  * @example
8
8
  * ```tsx
@@ -15,14 +15,13 @@
15
15
  * transport: httpTransport({ url: '/api/lens' }),
16
16
  * });
17
17
  *
18
- * // In component
19
- * function UserProfile({ id }: { id: string }) {
20
- * const { data, loading } = client.user.get({ input: { id } });
21
- * return <div>{data?.name}</div>;
22
- * }
18
+ * // Vanilla JS (anywhere - SSR, utilities, event handlers)
19
+ * const user = await client.user.get({ input: { id } });
20
+ * client.user.get({ input: { id } }).subscribe(data => console.log(data));
23
21
  *
24
- * // In SSR
25
- * const user = await client.user.get.fetch({ input: { id } });
22
+ * // React hooks (in components)
23
+ * const { data, loading } = client.user.get.useQuery({ input: { id } });
24
+ * const { mutate, loading } = client.user.create.useMutation();
26
25
  * ```
27
26
  */
28
27
 
@@ -34,7 +33,7 @@ import {
34
33
  type TypedClientConfig,
35
34
  } from "@sylphx/lens-client";
36
35
  import type { MutationDef, QueryDef, RouterDef, RouterRoutes } from "@sylphx/lens-core";
37
- import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
36
+ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
38
37
 
39
38
  // =============================================================================
40
39
  // Types
@@ -86,31 +85,24 @@ export interface MutationHookResult<TInput, TOutput> {
86
85
  reset: () => void;
87
86
  }
88
87
 
89
- /** Query endpoint type */
88
+ /** Query endpoint with React hooks */
90
89
  export interface QueryEndpoint<TInput, TOutput> {
91
- /** Hook call (in component) */
92
- <_TSelect extends SelectionObject = Record<string, never>>(
93
- options: TInput extends void ? QueryHookOptions<void> | void : QueryHookOptions<TInput>,
94
- ): QueryHookResult<TOutput>;
95
-
96
- /** Promise call (SSR) */
97
- fetch: <TSelect extends SelectionObject = Record<string, never>>(
98
- options: TInput extends void
99
- ? { input?: void; select?: TSelect } | void
100
- : { input: TInput; select?: TSelect },
101
- ) => Promise<TOutput>;
90
+ /** Vanilla JS call - returns QueryResult (Promise + Observable) */
91
+ (options?: { input?: TInput; select?: SelectionObject }): QueryResult<TOutput>;
92
+
93
+ /** React hook for reactive queries */
94
+ useQuery: (
95
+ options?: TInput extends void ? QueryHookOptions<void> | void : QueryHookOptions<TInput>,
96
+ ) => QueryHookResult<TOutput>;
102
97
  }
103
98
 
104
- /** Mutation endpoint type */
99
+ /** Mutation endpoint with React hooks */
105
100
  export interface MutationEndpoint<TInput, TOutput> {
106
- /** Hook call (in component) */
107
- (options?: MutationHookOptions<TOutput>): MutationHookResult<TInput, TOutput>;
108
-
109
- /** Promise call (SSR) */
110
- fetch: <TSelect extends SelectionObject = Record<string, never>>(options: {
111
- input: TInput;
112
- select?: TSelect;
113
- }) => Promise<TOutput>;
101
+ /** Vanilla JS call - returns Promise */
102
+ (options: { input: TInput; select?: SelectionObject }): Promise<{ data: TOutput }>;
103
+
104
+ /** React hook for mutations */
105
+ useMutation: (options?: MutationHookOptions<TOutput>) => MutationHookResult<TInput, TOutput>;
114
106
  }
115
107
 
116
108
  /** Infer client type from router routes */
@@ -167,33 +159,18 @@ function queryReducer<T>(state: QueryState<T>, action: QueryAction<T>): QuerySta
167
159
  // =============================================================================
168
160
 
169
161
  /**
170
- * Create a query hook for a specific endpoint
162
+ * Create useQuery hook for a specific endpoint
171
163
  */
172
- function createQueryHook<TInput, TOutput>(
173
- baseClient: unknown,
174
- path: string,
175
- ): QueryEndpoint<TInput, TOutput> {
176
- // Cache for stable hook reference
177
- let cachedHook: QueryEndpoint<TInput, TOutput> | null = null;
178
-
179
- const getEndpoint = (p: string) => {
180
- const parts = p.split(".");
181
- let current: unknown = baseClient;
182
- for (const part of parts) {
183
- current = (current as Record<string, unknown>)[part];
184
- }
185
- return current as (options: unknown) => QueryResult<TOutput>;
186
- };
187
-
188
- const useQueryHook = (options?: QueryHookOptions<TInput>): QueryHookResult<TOutput> => {
189
- const _optionsKey = JSON.stringify(options ?? {});
190
-
164
+ function createUseQueryHook<TInput, TOutput>(
165
+ getEndpoint: () => (options: unknown) => QueryResult<TOutput>,
166
+ ) {
167
+ return function useQuery(options?: QueryHookOptions<TInput>): QueryHookResult<TOutput> {
191
168
  // Get query result from base client
192
169
  const query = useMemo(() => {
193
170
  if (options?.skip) return null;
194
- const endpoint = getEndpoint(path);
171
+ const endpoint = getEndpoint();
195
172
  return endpoint({ input: options?.input, select: options?.select });
196
- }, [options?.input, options?.select, options?.skip, path]);
173
+ }, [options?.input, options?.select, options?.skip, getEndpoint]);
197
174
 
198
175
  // State management
199
176
  const initialState: QueryState<TOutput> = {
@@ -280,49 +257,20 @@ function createQueryHook<TInput, TOutput>(
280
257
 
281
258
  return { data: state.data, loading: state.loading, error: state.error, refetch };
282
259
  };
283
-
284
- // Fetch method for promises (SSR)
285
- const fetch = async (options?: {
286
- input?: TInput;
287
- select?: SelectionObject;
288
- }): Promise<TOutput> => {
289
- const endpoint = getEndpoint(path);
290
- const queryResult = endpoint({ input: options?.input, select: options?.select });
291
- return queryResult.then((data) => data);
292
- };
293
-
294
- // Create the endpoint object with hook + fetch
295
- const endpoint = useQueryHook as unknown as QueryEndpoint<TInput, TOutput>;
296
- endpoint.fetch = fetch as QueryEndpoint<TInput, TOutput>["fetch"];
297
-
298
- cachedHook = endpoint;
299
- return cachedHook;
300
260
  }
301
261
 
302
262
  /**
303
- * Create a mutation hook for a specific endpoint
263
+ * Create useMutation hook for a specific endpoint
304
264
  */
305
- function createMutationHook<TInput, TOutput>(
306
- baseClient: unknown,
307
- path: string,
308
- ): MutationEndpoint<TInput, TOutput> {
309
- let cachedHook: MutationEndpoint<TInput, TOutput> | null = null;
310
-
311
- const getEndpoint = (p: string) => {
312
- const parts = p.split(".");
313
- let current: unknown = baseClient;
314
- for (const part of parts) {
315
- current = (current as Record<string, unknown>)[part];
316
- }
317
- return current as (options: unknown) => QueryResult<{ data: TOutput }>;
318
- };
319
-
320
- const useMutationHook = (
265
+ function createUseMutationHook<TInput, TOutput>(
266
+ getEndpoint: () => (options: unknown) => Promise<{ data: TOutput }>,
267
+ ) {
268
+ return function useMutation(
321
269
  hookOptions?: MutationHookOptions<TOutput>,
322
- ): MutationHookResult<TInput, TOutput> => {
323
- const [loading, setLoading] = React.useState(false);
324
- const [error, setError] = React.useState<Error | null>(null);
325
- const [data, setData] = React.useState<TOutput | null>(null);
270
+ ): MutationHookResult<TInput, TOutput> {
271
+ const [loading, setLoading] = useState(false);
272
+ const [error, setError] = useState<Error | null>(null);
273
+ const [data, setData] = useState<TOutput | null>(null);
326
274
 
327
275
  const mountedRef = useRef(true);
328
276
  const hookOptionsRef = useRef(hookOptions);
@@ -341,19 +289,18 @@ function createMutationHook<TInput, TOutput>(
341
289
  setError(null);
342
290
 
343
291
  try {
344
- const endpoint = getEndpoint(path);
292
+ const endpoint = getEndpoint();
345
293
  const result = await endpoint({ input: options.input, select: options.select });
346
- const mutationResult = result as unknown as { data: TOutput };
347
294
 
348
295
  if (mountedRef.current) {
349
- setData(mutationResult.data);
296
+ setData(result.data);
350
297
  setLoading(false);
351
298
  }
352
299
 
353
- hookOptionsRef.current?.onSuccess?.(mutationResult.data);
300
+ hookOptionsRef.current?.onSuccess?.(result.data);
354
301
  hookOptionsRef.current?.onSettled?.();
355
302
 
356
- return mutationResult.data;
303
+ return result.data;
357
304
  } catch (err) {
358
305
  const mutationError = err instanceof Error ? err : new Error(String(err));
359
306
 
@@ -368,7 +315,7 @@ function createMutationHook<TInput, TOutput>(
368
315
  throw mutationError;
369
316
  }
370
317
  },
371
- [path],
318
+ [getEndpoint],
372
319
  );
373
320
 
374
321
  const reset = useCallback(() => {
@@ -379,28 +326,8 @@ function createMutationHook<TInput, TOutput>(
379
326
 
380
327
  return { mutate, loading, error, data, reset };
381
328
  };
382
-
383
- // Fetch method for promises (SSR)
384
- const fetch = async (options: { input: TInput; select?: SelectionObject }): Promise<TOutput> => {
385
- const endpoint = getEndpoint(path);
386
- const result = await endpoint({ input: options.input, select: options.select });
387
- const mutationResult = result as unknown as { data: TOutput };
388
- return mutationResult.data;
389
- };
390
-
391
- const endpoint = useMutationHook as MutationEndpoint<TInput, TOutput>;
392
- endpoint.fetch = fetch;
393
-
394
- cachedHook = endpoint;
395
- return cachedHook;
396
329
  }
397
330
 
398
- // =============================================================================
399
- // React import for useState (needed in mutation hook)
400
- // =============================================================================
401
-
402
- import * as React from "react";
403
-
404
331
  // =============================================================================
405
332
  // Create Client
406
333
  // =============================================================================
@@ -411,9 +338,8 @@ const hookCache = new Map<string, unknown>();
411
338
  /**
412
339
  * Create a Lens client with React hooks.
413
340
  *
414
- * Each endpoint can be called:
415
- * - Directly as a hook: `client.user.get({ input: { id } })`
416
- * - Via .fetch() for promises: `await client.user.get.fetch({ input: { id } })`
341
+ * Base client methods work in vanilla JS (SSR, utilities, event handlers).
342
+ * React hooks are available as `.useQuery()` and `.useMutation()`.
417
343
  *
418
344
  * @example
419
345
  * ```tsx
@@ -426,20 +352,14 @@ const hookCache = new Map<string, unknown>();
426
352
  * transport: httpTransport({ url: '/api/lens' }),
427
353
  * });
428
354
  *
429
- * // Component usage
430
- * function UserProfile({ id }: { id: string }) {
431
- * // Query hook - auto-subscribes
432
- * const { data, loading, error } = client.user.get({
433
- * input: { id },
434
- * select: { name: true },
435
- * });
355
+ * // Vanilla JS (anywhere)
356
+ * const user = await client.user.get({ input: { id } });
436
357
  *
437
- * // Mutation hook - returns mutate function
438
- * const { mutate, loading: saving } = client.user.update({
439
- * onSuccess: () => toast('Updated!'),
440
- * });
358
+ * // React component
359
+ * function UserProfile({ id }: { id: string }) {
360
+ * const { data, loading } = client.user.get.useQuery({ input: { id } });
361
+ * const { mutate } = client.user.update.useMutation();
441
362
  *
442
- * if (loading) return <Spinner />;
443
363
  * return (
444
364
  * <div>
445
365
  * <h1>{data?.name}</h1>
@@ -449,12 +369,6 @@ const hookCache = new Map<string, unknown>();
449
369
  * </div>
450
370
  * );
451
371
  * }
452
- *
453
- * // SSR usage
454
- * async function UserPage({ id }: { id: string }) {
455
- * const user = await client.user.get.fetch({ input: { id } });
456
- * return <div>{user.name}</div>;
457
- * }
458
372
  * ```
459
373
  */
460
374
  export function createClient<TRouter extends RouterDef>(
@@ -463,50 +377,44 @@ export function createClient<TRouter extends RouterDef>(
463
377
  // Create base client for transport
464
378
  const baseClient = createBaseClient(config as LensClientConfig);
465
379
 
466
- // Track endpoint types (query vs mutation) - determined at runtime via metadata
467
- // For now, we'll detect based on the operation result
468
- const _endpointTypes = new Map<string, "query" | "mutation">();
469
-
470
380
  function createProxy(path: string): unknown {
471
- const cacheKey = path;
472
-
473
- // Return cached hook if available
474
- if (hookCache.has(cacheKey)) {
475
- return hookCache.get(cacheKey);
476
- }
477
-
478
381
  const handler: ProxyHandler<(...args: unknown[]) => unknown> = {
479
382
  get(_target, prop) {
480
383
  if (typeof prop === "symbol") return undefined;
481
384
  const key = prop as string;
482
385
 
483
- // Handle .fetch() method - returns a promise
484
- if (key === "fetch") {
485
- return async (options: unknown) => {
486
- // Navigate to the endpoint in base client
487
- const parts = path.split(".");
488
- let current: unknown = baseClient;
489
- for (const part of parts) {
490
- current = (current as Record<string, unknown>)[part];
491
- }
492
- const endpointFn = current as (opts: unknown) => QueryResult<unknown>;
493
- const queryResult = endpointFn(options);
494
-
495
- // Await the result
496
- const result = await queryResult;
497
-
498
- // For mutations, the result is { data: ... }
499
- // For queries, the result is the data directly
500
- if (
501
- result &&
502
- typeof result === "object" &&
503
- "data" in result &&
504
- Object.keys(result).length === 1
505
- ) {
506
- return (result as { data: unknown }).data;
507
- }
508
- return result;
509
- };
386
+ // Handle .useQuery() - React hook for queries
387
+ if (key === "useQuery") {
388
+ const cacheKey = `${path}:useQuery`;
389
+ if (!hookCache.has(cacheKey)) {
390
+ const getEndpoint = () => {
391
+ const parts = path.split(".");
392
+ let current: unknown = baseClient;
393
+ for (const part of parts) {
394
+ current = (current as Record<string, unknown>)[part];
395
+ }
396
+ return current as (options: unknown) => QueryResult<unknown>;
397
+ };
398
+ hookCache.set(cacheKey, createUseQueryHook(getEndpoint));
399
+ }
400
+ return hookCache.get(cacheKey);
401
+ }
402
+
403
+ // Handle .useMutation() - React hook for mutations
404
+ if (key === "useMutation") {
405
+ const cacheKey = `${path}:useMutation`;
406
+ if (!hookCache.has(cacheKey)) {
407
+ const getEndpoint = () => {
408
+ const parts = path.split(".");
409
+ let current: unknown = baseClient;
410
+ for (const part of parts) {
411
+ current = (current as Record<string, unknown>)[part];
412
+ }
413
+ return current as (options: unknown) => Promise<{ data: unknown }>;
414
+ };
415
+ hookCache.set(cacheKey, createUseMutationHook(getEndpoint));
416
+ }
417
+ return hookCache.get(cacheKey);
510
418
  }
511
419
 
512
420
  if (key === "then") return undefined;
@@ -517,51 +425,14 @@ export function createClient<TRouter extends RouterDef>(
517
425
  },
518
426
 
519
427
  apply(_target, _thisArg, args) {
520
- // This is called when the endpoint is invoked as a function
521
- // Detect query vs mutation based on options shape:
522
- // - Query: has `input` or `select` or `skip` (QueryHookOptions)
523
- // - Mutation: has `onSuccess`, `onError`, `onSettled` or no options (MutationHookOptions)
524
-
525
- const options = args[0] as Record<string, unknown> | undefined;
526
-
527
- // Detect based on option keys
528
- const isQueryOptions =
529
- options && ("input" in options || "select" in options || "skip" in options);
530
-
531
- const isMutationOptions =
532
- !options ||
533
- (!isQueryOptions &&
534
- (Object.keys(options).length === 0 ||
535
- "onSuccess" in options ||
536
- "onError" in options ||
537
- "onSettled" in options));
538
-
539
- // Check cache - but we need to know the type first
540
- const cacheKeyQuery = `${path}:query`;
541
- const cacheKeyMutation = `${path}:mutation`;
542
-
543
- if (isQueryOptions) {
544
- if (!hookCache.has(cacheKeyQuery)) {
545
- hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
546
- }
547
- const hook = hookCache.get(cacheKeyQuery) as (opts: unknown) => unknown;
548
- return hook(options);
549
- }
550
-
551
- if (isMutationOptions) {
552
- if (!hookCache.has(cacheKeyMutation)) {
553
- hookCache.set(cacheKeyMutation, createMutationHook(baseClient, path));
554
- }
555
- const hook = hookCache.get(cacheKeyMutation) as (opts: unknown) => unknown;
556
- return hook(options);
557
- }
558
-
559
- // Fallback to query
560
- if (!hookCache.has(cacheKeyQuery)) {
561
- hookCache.set(cacheKeyQuery, createQueryHook(baseClient, path));
428
+ // Direct call - delegate to base client (returns QueryResult or Promise)
429
+ const parts = path.split(".");
430
+ let current: unknown = baseClient;
431
+ for (const part of parts) {
432
+ current = (current as Record<string, unknown>)[part];
562
433
  }
563
- const hook = hookCache.get(cacheKeyQuery) as (opts: unknown) => unknown;
564
- return hook(options);
434
+ const endpoint = current as (options: unknown) => unknown;
435
+ return endpoint(args[0]);
565
436
  },
566
437
  };
567
438
 
package/src/index.ts CHANGED
@@ -15,14 +15,13 @@
15
15
  * transport: httpTransport({ url: '/api/lens' }),
16
16
  * });
17
17
  *
18
- * // Component usage
19
- * function UserProfile({ id }: { id: string }) {
20
- * const { data, loading } = client.user.get({ input: { id } });
21
- * return <div>{data?.name}</div>;
22
- * }
18
+ * // Vanilla JS (anywhere - SSR, utilities, event handlers)
19
+ * const user = await client.user.get({ input: { id } });
20
+ * client.user.get({ input: { id } }).subscribe(data => console.log(data));
23
21
  *
24
- * // SSR usage
25
- * const user = await client.user.get.fetch({ input: { id } });
22
+ * // React hooks (in components)
23
+ * const { data, loading } = client.user.get.useQuery({ input: { id } });
24
+ * const { mutate, loading } = client.user.create.useMutation();
26
25
  * ```
27
26
  */
28
27