@sylphx/lens-react 2.1.3 → 2.1.5

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.js CHANGED
@@ -1,7 +1,15 @@
1
1
  // src/context.tsx
2
2
  import { createContext, useContext } from "react";
3
3
  import { jsxDEV } from "react/jsx-dev-runtime";
4
- var LensContext = createContext(null);
4
+ var LENS_CONTEXT_KEY = Symbol.for("@sylphx/lens-react/context");
5
+ function getOrCreateContext() {
6
+ const globalObj = globalThis;
7
+ if (!globalObj[LENS_CONTEXT_KEY]) {
8
+ globalObj[LENS_CONTEXT_KEY] = createContext(null);
9
+ }
10
+ return globalObj[LENS_CONTEXT_KEY];
11
+ }
12
+ var LensContext = getOrCreateContext();
5
13
  function LensProvider({ client, children }) {
6
14
  return /* @__PURE__ */ jsxDEV(LensContext.Provider, {
7
15
  value: client,
@@ -16,7 +24,30 @@ function useLensClient() {
16
24
  return client;
17
25
  }
18
26
  // src/hooks.ts
19
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
27
+ import {
28
+ useCallback,
29
+ useEffect,
30
+ useMemo,
31
+ useReducer,
32
+ useRef,
33
+ useState
34
+ } from "react";
35
+ function queryReducer(state, action) {
36
+ switch (action.type) {
37
+ case "RESET":
38
+ return { data: null, loading: false, error: null };
39
+ case "START":
40
+ return { ...state, loading: true, error: null };
41
+ case "SUCCESS":
42
+ return { data: action.data, loading: false, error: null };
43
+ case "ERROR":
44
+ return { ...state, loading: false, error: action.error };
45
+ case "LOADING_DONE":
46
+ return { ...state, loading: false };
47
+ default:
48
+ return state;
49
+ }
50
+ }
20
51
  function useQuery(selector, paramsOrDeps, options) {
21
52
  const client = useLensClient();
22
53
  const isAccessorMode = Array.isArray(paramsOrDeps);
@@ -36,9 +67,12 @@ function useQuery(selector, paramsOrDeps, options) {
36
67
  }, isAccessorMode ? [client, options?.skip, ...paramsOrDeps] : [client, selector, paramsKey, options?.skip]);
37
68
  const selectRef = useRef(options?.select);
38
69
  selectRef.current = options?.select;
39
- const [data, setData] = useState(null);
40
- const [loading, setLoading] = useState(query != null && !options?.skip);
41
- const [error, setError] = useState(null);
70
+ const initialState = {
71
+ data: null,
72
+ loading: query != null && !options?.skip,
73
+ error: null
74
+ };
75
+ const [state, dispatch] = useReducer(queryReducer, initialState);
42
76
  const mountedRef = useRef(true);
43
77
  const queryRef = useRef(query);
44
78
  queryRef.current = query;
@@ -48,32 +82,31 @@ function useQuery(selector, paramsOrDeps, options) {
48
82
  useEffect(() => {
49
83
  mountedRef.current = true;
50
84
  if (query == null) {
51
- setData(null);
52
- setLoading(false);
53
- setError(null);
85
+ dispatch({ type: "RESET" });
54
86
  return;
55
87
  }
56
- setLoading(true);
57
- setError(null);
88
+ dispatch({ type: "START" });
58
89
  let hasReceivedData = false;
59
90
  const unsubscribe = query.subscribe((value) => {
60
91
  if (mountedRef.current) {
61
92
  hasReceivedData = true;
62
- setData(transform(value));
63
- setLoading(false);
93
+ dispatch({ type: "SUCCESS", data: transform(value) });
64
94
  }
65
95
  });
66
96
  query.then((value) => {
67
- if (mountedRef.current && !hasReceivedData) {
68
- setData(transform(value));
69
- }
70
97
  if (mountedRef.current) {
71
- setLoading(false);
98
+ if (!hasReceivedData) {
99
+ dispatch({ type: "SUCCESS", data: transform(value) });
100
+ } else {
101
+ dispatch({ type: "LOADING_DONE" });
102
+ }
72
103
  }
73
104
  }, (err) => {
74
105
  if (mountedRef.current) {
75
- setError(err instanceof Error ? err : new Error(String(err)));
76
- setLoading(false);
106
+ dispatch({
107
+ type: "ERROR",
108
+ error: err instanceof Error ? err : new Error(String(err))
109
+ });
77
110
  }
78
111
  });
79
112
  return () => {
@@ -85,21 +118,21 @@ function useQuery(selector, paramsOrDeps, options) {
85
118
  const currentQuery = queryRef.current;
86
119
  if (currentQuery == null)
87
120
  return;
88
- setLoading(true);
89
- setError(null);
121
+ dispatch({ type: "START" });
90
122
  currentQuery.then((value) => {
91
123
  if (mountedRef.current) {
92
- setData(transform(value));
93
- setLoading(false);
124
+ dispatch({ type: "SUCCESS", data: transform(value) });
94
125
  }
95
126
  }, (err) => {
96
127
  if (mountedRef.current) {
97
- setError(err instanceof Error ? err : new Error(String(err)));
98
- setLoading(false);
128
+ dispatch({
129
+ type: "ERROR",
130
+ error: err instanceof Error ? err : new Error(String(err))
131
+ });
99
132
  }
100
133
  });
101
134
  }, [transform]);
102
- return { data, loading, error, refetch };
135
+ return { data: state.data, loading: state.loading, error: state.error, refetch };
103
136
  }
104
137
  function useMutation(selector) {
105
138
  const client = useLensClient();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sylphx/lens-react",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
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": "^2.0.4"
33
+ "@sylphx/lens-client": "^2.0.5"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": ">=18.0.0"
package/src/context.tsx CHANGED
@@ -2,20 +2,45 @@
2
2
  * @sylphx/lens-react - Context Provider
3
3
  *
4
4
  * Provides Lens client to React component tree.
5
+ *
6
+ * Uses global singleton pattern to ensure the same context is shared
7
+ * across multiple module instances (important for monorepos where
8
+ * the same package may be resolved to different paths).
5
9
  */
6
10
 
7
11
  import type { LensClient } from "@sylphx/lens-client";
8
12
  import { createContext, type ReactElement, type ReactNode, useContext } from "react";
9
13
 
10
14
  // =============================================================================
11
- // Context
15
+ // Context (Global Singleton)
12
16
  // =============================================================================
13
17
 
14
18
  /**
15
- * Context for Lens client
19
+ * Global key for storing the singleton context.
20
+ * Using a Symbol ensures no collision with other globals.
21
+ */
22
+ const LENS_CONTEXT_KEY = Symbol.for("@sylphx/lens-react/context");
23
+
24
+ /**
25
+ * Get or create the global singleton context.
26
+ * This ensures that even if the module is loaded multiple times
27
+ * (common in monorepos), all instances share the same React context.
28
+ */
29
+ function getOrCreateContext(): React.Context<LensClient<any, any> | null> {
30
+ const globalObj = globalThis as unknown as Record<symbol, React.Context<LensClient<any, any> | null>>;
31
+
32
+ if (!globalObj[LENS_CONTEXT_KEY]) {
33
+ globalObj[LENS_CONTEXT_KEY] = createContext<LensClient<any, any> | null>(null);
34
+ }
35
+
36
+ return globalObj[LENS_CONTEXT_KEY];
37
+ }
38
+
39
+ /**
40
+ * Context for Lens client (singleton)
16
41
  * Using any for internal storage to avoid type constraint issues
17
42
  */
18
- const LensContext = createContext<LensClient<any, any> | null>(null);
43
+ const LensContext = getOrCreateContext();
19
44
 
20
45
  // =============================================================================
21
46
  // Provider
package/src/hooks.ts CHANGED
@@ -27,7 +27,15 @@
27
27
  */
28
28
 
29
29
  import type { LensClient, MutationResult, QueryResult } from "@sylphx/lens-client";
30
- import { type DependencyList, useCallback, useEffect, useMemo, useRef, useState } from "react";
30
+ import {
31
+ type DependencyList,
32
+ useCallback,
33
+ useEffect,
34
+ useMemo,
35
+ useReducer,
36
+ useRef,
37
+ useState,
38
+ } from "react";
31
39
  import { useLensClient } from "./context.js";
32
40
 
33
41
  // =============================================================================
@@ -68,6 +76,40 @@ export interface UseQueryOptions<TData = unknown, TSelected = TData> {
68
76
  select?: (data: TData) => TSelected;
69
77
  }
70
78
 
79
+ // =============================================================================
80
+ // Query State Reducer (for atomic state updates)
81
+ // =============================================================================
82
+
83
+ interface QueryState<T> {
84
+ data: T | null;
85
+ loading: boolean;
86
+ error: Error | null;
87
+ }
88
+
89
+ type QueryAction<T> =
90
+ | { type: "RESET" }
91
+ | { type: "START" }
92
+ | { type: "SUCCESS"; data: T }
93
+ | { type: "ERROR"; error: Error }
94
+ | { type: "LOADING_DONE" };
95
+
96
+ function queryReducer<T>(state: QueryState<T>, action: QueryAction<T>): QueryState<T> {
97
+ switch (action.type) {
98
+ case "RESET":
99
+ return { data: null, loading: false, error: null };
100
+ case "START":
101
+ return { ...state, loading: true, error: null };
102
+ case "SUCCESS":
103
+ return { data: action.data, loading: false, error: null };
104
+ case "ERROR":
105
+ return { ...state, loading: false, error: action.error };
106
+ case "LOADING_DONE":
107
+ return { ...state, loading: false };
108
+ default:
109
+ return state;
110
+ }
111
+ }
112
+
71
113
  /** Client type for callbacks */
72
114
  type Client = LensClient<any, any>;
73
115
 
@@ -212,9 +254,13 @@ export function useQuery<TParams, TResult, TSelected = TResult>(
212
254
  const selectRef = useRef(options?.select);
213
255
  selectRef.current = options?.select;
214
256
 
215
- const [data, setData] = useState<TSelected | null>(null);
216
- const [loading, setLoading] = useState(query != null && !options?.skip);
217
- const [error, setError] = useState<Error | null>(null);
257
+ // Use reducer for atomic state updates (prevents cascading re-renders)
258
+ const initialState: QueryState<TSelected> = {
259
+ data: null,
260
+ loading: query != null && !options?.skip,
261
+ error: null,
262
+ };
263
+ const [state, dispatch] = useReducer(queryReducer<TSelected>, initialState);
218
264
 
219
265
  // Track mounted state
220
266
  const mountedRef = useRef(true);
@@ -234,14 +280,11 @@ export function useQuery<TParams, TResult, TSelected = TResult>(
234
280
 
235
281
  // Handle null/undefined query
236
282
  if (query == null) {
237
- setData(null);
238
- setLoading(false);
239
- setError(null);
283
+ dispatch({ type: "RESET" });
240
284
  return;
241
285
  }
242
286
 
243
- setLoading(true);
244
- setError(null);
287
+ dispatch({ type: "START" });
245
288
 
246
289
  // Track if subscribe has provided data (to avoid duplicate updates from then)
247
290
  let hasReceivedData = false;
@@ -250,8 +293,7 @@ export function useQuery<TParams, TResult, TSelected = TResult>(
250
293
  const unsubscribe = query.subscribe((value) => {
251
294
  if (mountedRef.current) {
252
295
  hasReceivedData = true;
253
- setData(transform(value));
254
- setLoading(false);
296
+ dispatch({ type: "SUCCESS", data: transform(value) });
255
297
  }
256
298
  });
257
299
 
@@ -259,17 +301,20 @@ export function useQuery<TParams, TResult, TSelected = TResult>(
259
301
  // Only setData if subscribe hasn't already provided data (one-shot queries)
260
302
  query.then(
261
303
  (value) => {
262
- if (mountedRef.current && !hasReceivedData) {
263
- setData(transform(value));
264
- }
265
304
  if (mountedRef.current) {
266
- setLoading(false);
305
+ if (!hasReceivedData) {
306
+ dispatch({ type: "SUCCESS", data: transform(value) });
307
+ } else {
308
+ dispatch({ type: "LOADING_DONE" });
309
+ }
267
310
  }
268
311
  },
269
312
  (err) => {
270
313
  if (mountedRef.current) {
271
- setError(err instanceof Error ? err : new Error(String(err)));
272
- setLoading(false);
314
+ dispatch({
315
+ type: "ERROR",
316
+ error: err instanceof Error ? err : new Error(String(err)),
317
+ });
273
318
  }
274
319
  },
275
320
  );
@@ -285,26 +330,26 @@ export function useQuery<TParams, TResult, TSelected = TResult>(
285
330
  const currentQuery = queryRef.current;
286
331
  if (currentQuery == null) return;
287
332
 
288
- setLoading(true);
289
- setError(null);
333
+ dispatch({ type: "START" });
290
334
 
291
335
  currentQuery.then(
292
336
  (value) => {
293
337
  if (mountedRef.current) {
294
- setData(transform(value));
295
- setLoading(false);
338
+ dispatch({ type: "SUCCESS", data: transform(value) });
296
339
  }
297
340
  },
298
341
  (err) => {
299
342
  if (mountedRef.current) {
300
- setError(err instanceof Error ? err : new Error(String(err)));
301
- setLoading(false);
343
+ dispatch({
344
+ type: "ERROR",
345
+ error: err instanceof Error ? err : new Error(String(err)),
346
+ });
302
347
  }
303
348
  },
304
349
  );
305
350
  }, [transform]);
306
351
 
307
- return { data, loading, error, refetch };
352
+ return { data: state.data, loading: state.loading, error: state.error, refetch };
308
353
  }
309
354
 
310
355
  // =============================================================================