@variocube/app-ui 1.16.0 → 1.16.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.
@@ -14,7 +14,18 @@ export function useStorage(key, defaultValue, storageType) {
14
14
  storage.addChangeListener(key, updateStateFromStorage);
15
15
  return () => storage.removeChangeListener(key, updateStateFromStorage);
16
16
  }, [key, updateStateFromStorage]);
17
- const typedValue = useMemo(() => JSON.parse(value), [value]);
17
+ const typedValue = useMemo(() => {
18
+ if (value === undefined) {
19
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
20
+ }
21
+ try {
22
+ return JSON.parse(value);
23
+ }
24
+ catch (e) {
25
+ console.warn(`Failed to parse storage value for key "${key}", falling back to default value.`, e);
26
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
27
+ }
28
+ }, [value, key, defaultValueSerialized]);
18
29
  const setTypedValue = useCallback((newValue) => {
19
30
  const value = JSON.stringify(newValue);
20
31
  if (value != defaultValueSerialized) {
@@ -1 +1 @@
1
- {"version":3,"file":"useStorage.js","sourceRoot":"","sources":["../../src/storage/useStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAC;AACtE,OAAO,EAAC,OAAO,EAAC,MAAM,WAAW,CAAC;AAGlC,MAAM,UAAU,UAAU,CAAI,GAAW,EAAE,YAAe,EAAE,WAAyB;IACpF,MAAM,sBAAsB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAE3F,MAAM,oBAAoB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACpD,OAAO,YAAY,aAAZ,YAAY,cAAZ,YAAY,GAAI,sBAAsB,CAAC;IAC/C,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,EAAE,WAAW,CAAC,CAAC,CAAC;IAE/C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;IAEzD,MAAM,sBAAsB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC/C,QAAQ,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAClC,CAAC,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC;IAE3B,eAAe,CAAC,GAAG,EAAE;QACpB,OAAO,CAAC,iBAAiB,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC;QACvD,OAAO,GAAG,EAAE,CAAC,OAAO,CAAC,oBAAoB,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC;IACxE,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC,CAAC;IAElC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAE7D,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,QAAW,EAAE,EAAE;QACjD,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,KAAK,IAAI,sBAAsB,EAAE,CAAC;YACrC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC;QACxC,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAClC,CAAC;IACF,CAAC,EAAE,CAAC,GAAG,EAAE,sBAAsB,EAAE,WAAW,CAAC,CAAC,CAAC;IAE/C,OAAO,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACpC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAI,GAAW,EAAE,YAAe;IAC9D,OAAO,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;AAC/C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAI,GAAW,EAAE,YAAe;IAChE,OAAO,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
1
+ {"version":3,"file":"useStorage.js","sourceRoot":"","sources":["../../src/storage/useStorage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import * as React from "react";
5
+ import { useRef } from "react";
6
+ import { act, create } from "react-test-renderer";
7
+ import { useStorage } from "./useStorage";
8
+ // Reset storage between tests
9
+ beforeEach(() => {
10
+ localStorage.clear();
11
+ sessionStorage.clear();
12
+ });
13
+ function TestComponent({ storageKey }) {
14
+ const renderCount = useRef(0);
15
+ renderCount.current++;
16
+ // Inline object literal: new reference every render (the bug trigger)
17
+ const [value] = useStorage(storageKey, { page: 0, size: 25 });
18
+ return (React.createElement("div", { "data-render-count": renderCount.current, "data-value": JSON.stringify(value) }));
19
+ }
20
+ function getTestResult(renderer) {
21
+ const div = renderer.root.findByType("div");
22
+ return {
23
+ renderCount: Number(div.props["data-render-count"]),
24
+ value: JSON.parse(div.props["data-value"]),
25
+ };
26
+ }
27
+ describe("useStorage", () => {
28
+ test("does not cause infinite re-renders with unstable object defaultValue", () => {
29
+ let renderer;
30
+ act(() => {
31
+ renderer = create(React.createElement(TestComponent, { storageKey: "test-key" }));
32
+ });
33
+ const afterMount = getTestResult(renderer);
34
+ // Force a re-render with the same props (simulates parent re-render)
35
+ act(() => {
36
+ renderer.update(React.createElement(TestComponent, { storageKey: "test-key" }));
37
+ });
38
+ const afterUpdate = getTestResult(renderer);
39
+ // Render count should stay small, not grow unboundedly
40
+ expect(afterUpdate.renderCount).toBeLessThanOrEqual(3);
41
+ expect(afterUpdate.value).toEqual({ page: 0, size: 25 });
42
+ });
43
+ test("returns default value when no stored value exists", () => {
44
+ let renderer;
45
+ act(() => {
46
+ renderer = create(React.createElement(TestComponent, { storageKey: "missing-key" }));
47
+ });
48
+ const result = getTestResult(renderer);
49
+ expect(result.value).toEqual({ page: 0, size: 25 });
50
+ });
51
+ test("returns stored value when it exists", () => {
52
+ localStorage.setItem("stored-key", JSON.stringify({ page: 2, size: 50 }));
53
+ let renderer;
54
+ act(() => {
55
+ renderer = create(React.createElement(TestComponent, { storageKey: "stored-key" }));
56
+ });
57
+ const result = getTestResult(renderer);
58
+ expect(result.value).toEqual({ page: 2, size: 50 });
59
+ });
60
+ });
61
+ //# sourceMappingURL=useStorage.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStorage.spec.js","sourceRoot":"","sources":["../../src/storage/useStorage.spec.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@variocube/app-ui",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "description": "Common UI components for Variocube applications.",
5
5
  "module": "esm/index.js",
6
6
  "types": "esm/index.d.ts",
@@ -58,7 +58,7 @@
58
58
  "react-dom": "^17.0.2",
59
59
  "react-router": "^6.3.0",
60
60
  "react-router-dom": "^6.3.0",
61
- "react-syntax-highlighter": "^15.5.0",
61
+ "react-syntax-highlighter": "^16.1.0",
62
62
  "react-test-renderer": "^17.0.2",
63
63
  "rimraf": "^3.0.2",
64
64
  "ts-jest": "^29.1.2",
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import * as React from "react";
6
+ import {useRef} from "react";
7
+ import {act, create, ReactTestRenderer} from "react-test-renderer";
8
+ import {useStorage} from "./useStorage";
9
+
10
+ // Reset storage between tests
11
+ beforeEach(() => {
12
+ localStorage.clear();
13
+ sessionStorage.clear();
14
+ });
15
+
16
+ interface TestResult<T> {
17
+ value: T;
18
+ renderCount: number;
19
+ }
20
+
21
+ function TestComponent({storageKey}: { storageKey: string }) {
22
+ const renderCount = useRef(0);
23
+ renderCount.current++;
24
+
25
+ // Inline object literal: new reference every render (the bug trigger)
26
+ const [value] = useStorage(storageKey, {page: 0, size: 25});
27
+
28
+ return (
29
+ <div
30
+ data-render-count={renderCount.current}
31
+ data-value={JSON.stringify(value)}
32
+ />
33
+ );
34
+ }
35
+
36
+ function getTestResult<T>(renderer: ReactTestRenderer): TestResult<T> {
37
+ const div = renderer.root.findByType("div");
38
+ return {
39
+ renderCount: Number(div.props["data-render-count"]),
40
+ value: JSON.parse(div.props["data-value"]),
41
+ };
42
+ }
43
+
44
+ describe("useStorage", () => {
45
+ test("does not cause infinite re-renders with unstable object defaultValue", () => {
46
+ let renderer: ReactTestRenderer;
47
+ act(() => {
48
+ renderer = create(<TestComponent storageKey="test-key" />);
49
+ });
50
+
51
+ const afterMount = getTestResult(renderer!);
52
+
53
+ // Force a re-render with the same props (simulates parent re-render)
54
+ act(() => {
55
+ renderer.update(<TestComponent storageKey="test-key" />);
56
+ });
57
+
58
+ const afterUpdate = getTestResult(renderer!);
59
+
60
+ // Render count should stay small, not grow unboundedly
61
+ expect(afterUpdate.renderCount).toBeLessThanOrEqual(3);
62
+ expect(afterUpdate.value).toEqual({page: 0, size: 25});
63
+ });
64
+
65
+ test("returns default value when no stored value exists", () => {
66
+ let renderer: ReactTestRenderer;
67
+ act(() => {
68
+ renderer = create(<TestComponent storageKey="missing-key" />);
69
+ });
70
+
71
+ const result = getTestResult(renderer!);
72
+ expect(result.value).toEqual({page: 0, size: 25});
73
+ });
74
+
75
+ test("returns stored value when it exists", () => {
76
+ localStorage.setItem("stored-key", JSON.stringify({page: 2, size: 50}));
77
+
78
+ let renderer: ReactTestRenderer;
79
+ act(() => {
80
+ renderer = create(<TestComponent storageKey="stored-key" />);
81
+ });
82
+
83
+ const result = getTestResult(renderer!);
84
+ expect(result.value).toEqual({page: 2, size: 50});
85
+ });
86
+ });
@@ -21,7 +21,17 @@ export function useStorage<T>(key: string, defaultValue: T, storageType?: Storag
21
21
  return () => storage.removeChangeListener(key, updateStateFromStorage);
22
22
  }, [key, updateStateFromStorage]);
23
23
 
24
- const typedValue = useMemo(() => JSON.parse(value), [value]);
24
+ const typedValue = useMemo(() => {
25
+ if (value === undefined) {
26
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
27
+ }
28
+ try {
29
+ return JSON.parse(value);
30
+ } catch (e) {
31
+ console.warn(`Failed to parse storage value for key "${key}", falling back to default value.`, e);
32
+ return defaultValueSerialized !== undefined ? JSON.parse(defaultValueSerialized) : undefined;
33
+ }
34
+ }, [value, key, defaultValueSerialized]);
25
35
 
26
36
  const setTypedValue = useCallback((newValue: T) => {
27
37
  const value = JSON.stringify(newValue);