@assistant-ui/store 0.2.9 → 0.2.10
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/RenderChildrenWithAccessor.d.ts.map +1 -1
- package/dist/RenderChildrenWithAccessor.js +11 -7
- package/dist/RenderChildrenWithAccessor.js.map +1 -1
- package/package.json +5 -5
- package/src/RenderChildrenWithAccessor.tsx +10 -8
- package/src/__tests__/RenderChildrenWithAccessor.test.tsx +136 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RenderChildrenWithAccessor.d.ts","sourceRoot":"","sources":["../src/RenderChildrenWithAccessor.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAmB,MAAM,OAAO,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,0BAAuB;AAItD,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAClC,cAAc,CAAC,GAAG,EAAE,eAAe,KAAK,CAAC,
|
|
1
|
+
{"version":3,"file":"RenderChildrenWithAccessor.d.ts","sourceRoot":"","sources":["../src/RenderChildrenWithAccessor.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAmB,MAAM,OAAO,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,0BAAuB;AAItD,eAAO,MAAM,kBAAkB,GAAI,CAAC,EAClC,cAAc,CAAC,GAAG,EAAE,eAAe,KAAK,CAAC,YAoB1C,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,EAC5C,YAAY,EACZ,QAAQ,GACT,EAAE;IACD,YAAY,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,CAAC,CAAC;IAC1C,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,SAAS,CAAC;CAC3C,GAAG,SAAS,CAGZ"}
|
|
@@ -4,16 +4,20 @@ import { useAuiState } from "./useAuiState.js";
|
|
|
4
4
|
import { useAui } from "./useAui.js";
|
|
5
5
|
export const useGetItemAccessor = (getItemState) => {
|
|
6
6
|
const aui = useAui();
|
|
7
|
-
//
|
|
8
|
-
|
|
7
|
+
// Track access with a dedicated flag:
|
|
8
|
+
// useSyncExternalStore may call getSnapshot() after commit (tearing checks),
|
|
9
|
+
// which would re-cache the current state and mask later real updates.
|
|
10
|
+
// Use the current state as the pre-access snapshot so the post-commit check
|
|
11
|
+
// matches getItemState(aui) and doesn't schedule an unnecessary re-render.
|
|
12
|
+
const accessedRef = useRef(false);
|
|
13
|
+
const currentValue = accessedRef.current ? null : getItemState(aui);
|
|
9
14
|
useAuiState(() => {
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return cacheRef.current;
|
|
15
|
+
if (!accessedRef.current)
|
|
16
|
+
return currentValue;
|
|
17
|
+
return getItemState(aui);
|
|
14
18
|
});
|
|
15
19
|
return () => {
|
|
16
|
-
|
|
20
|
+
accessedRef.current = true;
|
|
17
21
|
return getItemState(aui);
|
|
18
22
|
};
|
|
19
23
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RenderChildrenWithAccessor.js","sourceRoot":"","sources":["../src/RenderChildrenWithAccessor.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAkB,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,yBAAsB;AAC5C,OAAO,EAAE,MAAM,EAAE,oBAAiB;AAElC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,YAAyC,EACzC,EAAE;IACF,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IAErB,
|
|
1
|
+
{"version":3,"file":"RenderChildrenWithAccessor.js","sourceRoot":"","sources":["../src/RenderChildrenWithAccessor.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAkB,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAExD,OAAO,EAAE,WAAW,EAAE,yBAAsB;AAC5C,OAAO,EAAE,MAAM,EAAE,oBAAiB;AAElC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,YAAyC,EACzC,EAAE;IACF,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC;IAErB,sCAAsC;IACtC,6EAA6E;IAC7E,sEAAsE;IACtE,4EAA4E;IAC5E,2EAA2E;IAC3E,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,YAAY,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACpE,WAAW,CAAC,GAAG,EAAE;QACf,IAAI,CAAC,WAAW,CAAC,OAAO;YAAE,OAAO,YAAY,CAAC;QAC9C,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,EAAE;QACV,WAAW,CAAC,OAAO,GAAG,IAAI,CAAC;QAC3B,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAEvC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,0BAA0B,CAAI,EAC5C,YAAY,EACZ,QAAQ,GAIT;IACC,MAAM,OAAO,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;IACjD,OAAO,4BAA4B,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,4BAA4B,GAAG,CAAC,IAAe,EAAE,EAAE;IACvD,MAAM,EAAE,GACN,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC3E,MAAM,UAAU,GAAG,EAAE,EAAE,IAAI,CAAC;IAC5B,MAAM,SAAS,GAAG,EAAE,EAAE,GAAG,CAAC;IAC1B,MAAM,WAAW,GACf,OAAO,EAAE,EAAE,KAAK,KAAK,QAAQ;QAC7B,EAAE,CAAC,KAAK,IAAI,IAAI;QAChB,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC;QACnC,CAAC,CAAC,YAAY;QACd,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC;IAEhB,OAAO;IACL,wEAAwE;IACxE,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC,IAAI,IAAI,CAChE,CAAC;AACJ,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/store",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Tap-based state management for @assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"state-management",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"use-effect-event": "^2.0.3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@assistant-ui/tap": "^0.5.
|
|
33
|
+
"@assistant-ui/tap": "^0.5.11",
|
|
34
34
|
"@types/react": "*",
|
|
35
35
|
"react": "^18 || ^19"
|
|
36
36
|
},
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"@testing-library/react": "^16.3.2",
|
|
44
44
|
"@types/react": "^19.2.14",
|
|
45
45
|
"@types/react-dom": "^19.2.3",
|
|
46
|
-
"jsdom": "^29.1.
|
|
46
|
+
"jsdom": "^29.1.1",
|
|
47
47
|
"react": "^19.2.5",
|
|
48
48
|
"vitest": "^4.1.5",
|
|
49
|
-
"@assistant-ui/tap": "0.5.
|
|
50
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
49
|
+
"@assistant-ui/tap": "0.5.11",
|
|
50
|
+
"@assistant-ui/x-buildutils": "0.0.7"
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public",
|
|
@@ -10,18 +10,20 @@ export const useGetItemAccessor = <T,>(
|
|
|
10
10
|
) => {
|
|
11
11
|
const aui = useAui();
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
|
|
13
|
+
// Track access with a dedicated flag:
|
|
14
|
+
// useSyncExternalStore may call getSnapshot() after commit (tearing checks),
|
|
15
|
+
// which would re-cache the current state and mask later real updates.
|
|
16
|
+
// Use the current state as the pre-access snapshot so the post-commit check
|
|
17
|
+
// matches getItemState(aui) and doesn't schedule an unnecessary re-render.
|
|
18
|
+
const accessedRef = useRef(false);
|
|
19
|
+
const currentValue = accessedRef.current ? null : getItemState(aui);
|
|
15
20
|
useAuiState(() => {
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
return cacheRef.current;
|
|
21
|
+
if (!accessedRef.current) return currentValue;
|
|
22
|
+
return getItemState(aui);
|
|
20
23
|
});
|
|
21
24
|
|
|
22
25
|
return () => {
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
accessedRef.current = true;
|
|
25
27
|
return getItemState(aui);
|
|
26
28
|
};
|
|
27
29
|
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { act, render } from "@testing-library/react";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { AuiProvider } from "../utils/react-assistant-context";
|
|
7
|
+
import { RenderChildrenWithAccessor } from "../RenderChildrenWithAccessor";
|
|
8
|
+
import { PROXIED_ASSISTANT_STATE_SYMBOL } from "../utils/proxied-assistant-state";
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
type Listener = () => void;
|
|
15
|
+
|
|
16
|
+
const createTestAuiClient = () => {
|
|
17
|
+
const listeners = new Set<Listener>();
|
|
18
|
+
let itemState: { value: number; isEditing: boolean } = {
|
|
19
|
+
value: 1,
|
|
20
|
+
isEditing: false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const proxiedState = {
|
|
24
|
+
item: itemState,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const client = {
|
|
28
|
+
subscribe: (listener: Listener) => {
|
|
29
|
+
listeners.add(listener);
|
|
30
|
+
return () => listeners.delete(listener);
|
|
31
|
+
},
|
|
32
|
+
on: () => () => {},
|
|
33
|
+
[PROXIED_ASSISTANT_STATE_SYMBOL]: proxiedState,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
client,
|
|
38
|
+
getItemState: () => itemState,
|
|
39
|
+
update: (next: Partial<typeof itemState>) => {
|
|
40
|
+
itemState = { ...itemState, ...next };
|
|
41
|
+
proxiedState.item = itemState;
|
|
42
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
|
|
43
|
+
listeners.forEach((listener) => listener());
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("RenderChildrenWithAccessor", () => {
|
|
49
|
+
it("re-renders when accessed state updates (regression: issue #3838)", () => {
|
|
50
|
+
const testClient = createTestAuiClient();
|
|
51
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
52
|
+
<AuiProvider value={testClient.client as never}>{children}</AuiProvider>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<RenderChildrenWithAccessor
|
|
57
|
+
getItemState={() => testClient.getItemState()}
|
|
58
|
+
>
|
|
59
|
+
{(getItem) => {
|
|
60
|
+
const item = getItem();
|
|
61
|
+
return <div>{item.isEditing ? "editing" : "viewing"}</div>;
|
|
62
|
+
}}
|
|
63
|
+
</RenderChildrenWithAccessor>,
|
|
64
|
+
{ wrapper },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(container.textContent).toBe("viewing");
|
|
68
|
+
|
|
69
|
+
act(() => {
|
|
70
|
+
testClient.update({ isEditing: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(container.textContent).toBe("editing");
|
|
74
|
+
|
|
75
|
+
act(() => {
|
|
76
|
+
testClient.update({ isEditing: false });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(container.textContent).toBe("viewing");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("does not schedule an extra render on first access (initial snapshot matches getItemState)", () => {
|
|
83
|
+
const testClient = createTestAuiClient();
|
|
84
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
85
|
+
<AuiProvider value={testClient.client as never}>{children}</AuiProvider>
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const renderSpy = vi.fn();
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<RenderChildrenWithAccessor
|
|
92
|
+
getItemState={() => testClient.getItemState()}
|
|
93
|
+
>
|
|
94
|
+
{(getItem) => {
|
|
95
|
+
renderSpy();
|
|
96
|
+
const item = getItem();
|
|
97
|
+
return <div>{item.value}</div>;
|
|
98
|
+
}}
|
|
99
|
+
</RenderChildrenWithAccessor>,
|
|
100
|
+
{ wrapper },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// first mount accesses the item; useSyncExternalStore's post-commit
|
|
104
|
+
// tearing check should see a stable snapshot and not force a re-render
|
|
105
|
+
expect(renderSpy).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("does not re-render when item is never accessed", () => {
|
|
109
|
+
const testClient = createTestAuiClient();
|
|
110
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
111
|
+
<AuiProvider value={testClient.client as never}>{children}</AuiProvider>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const renderSpy = vi.fn();
|
|
115
|
+
|
|
116
|
+
render(
|
|
117
|
+
<RenderChildrenWithAccessor
|
|
118
|
+
getItemState={() => testClient.getItemState()}
|
|
119
|
+
>
|
|
120
|
+
{() => {
|
|
121
|
+
renderSpy();
|
|
122
|
+
return <div>static</div>;
|
|
123
|
+
}}
|
|
124
|
+
</RenderChildrenWithAccessor>,
|
|
125
|
+
{ wrapper },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const initialRenderCount = renderSpy.mock.calls.length;
|
|
129
|
+
|
|
130
|
+
act(() => {
|
|
131
|
+
testClient.update({ value: 99 });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(renderSpy.mock.calls.length).toBe(initialRenderCount);
|
|
135
|
+
});
|
|
136
|
+
});
|