@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 +58 -25
- package/package.json +2 -2
- package/src/context.tsx +28 -3
- package/src/hooks.ts +69 -24
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
|
|
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 {
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
52
|
-
setLoading(false);
|
|
53
|
-
setError(null);
|
|
85
|
+
dispatch({ type: "RESET" });
|
|
54
86
|
return;
|
|
55
87
|
}
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
89
|
-
setError(null);
|
|
121
|
+
dispatch({ type: "START" });
|
|
90
122
|
currentQuery.then((value) => {
|
|
91
123
|
if (mountedRef.current) {
|
|
92
|
-
|
|
93
|
-
setLoading(false);
|
|
124
|
+
dispatch({ type: "SUCCESS", data: transform(value) });
|
|
94
125
|
}
|
|
95
126
|
}, (err) => {
|
|
96
127
|
if (mountedRef.current) {
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
"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.
|
|
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
|
-
*
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
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
|
-
|
|
238
|
-
setLoading(false);
|
|
239
|
-
setError(null);
|
|
283
|
+
dispatch({ type: "RESET" });
|
|
240
284
|
return;
|
|
241
285
|
}
|
|
242
286
|
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
289
|
-
setError(null);
|
|
333
|
+
dispatch({ type: "START" });
|
|
290
334
|
|
|
291
335
|
currentQuery.then(
|
|
292
336
|
(value) => {
|
|
293
337
|
if (mountedRef.current) {
|
|
294
|
-
|
|
295
|
-
setLoading(false);
|
|
338
|
+
dispatch({ type: "SUCCESS", data: transform(value) });
|
|
296
339
|
}
|
|
297
340
|
},
|
|
298
341
|
(err) => {
|
|
299
342
|
if (mountedRef.current) {
|
|
300
|
-
|
|
301
|
-
|
|
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
|
// =============================================================================
|