@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.
@@ -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,YAkB1C,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"}
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
- // if the consumer never accesses the item, do not trigger rerenders
8
- const cacheRef = useRef(undefined);
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 (cacheRef.current === undefined) {
11
- cacheRef.current = getItemState(aui);
12
- }
13
- return cacheRef.current;
15
+ if (!accessedRef.current)
16
+ return currentValue;
17
+ return getItemState(aui);
14
18
  });
15
19
  return () => {
16
- cacheRef.current = undefined; // clear the cache (rerender on next state change)
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,oEAAoE;IACpE,MAAM,QAAQ,GAAG,MAAM,CAAgB,SAAS,CAAC,CAAC;IAClD,WAAW,CAAC,GAAG,EAAE;QACf,IAAI,QAAQ,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YACnC,QAAQ,CAAC,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QACvC,CAAC;QACD,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,EAAE;QACV,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC,kDAAkD;QAEhF,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"}
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.9",
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.10",
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.0",
46
+ "jsdom": "^29.1.1",
47
47
  "react": "^19.2.5",
48
48
  "vitest": "^4.1.5",
49
- "@assistant-ui/tap": "0.5.10",
50
- "@assistant-ui/x-buildutils": "0.0.6"
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
- // if the consumer never accesses the item, do not trigger rerenders
14
- const cacheRef = useRef<T | undefined>(undefined);
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 (cacheRef.current === undefined) {
17
- cacheRef.current = getItemState(aui);
18
- }
19
- return cacheRef.current;
21
+ if (!accessedRef.current) return currentValue;
22
+ return getItemState(aui);
20
23
  });
21
24
 
22
25
  return () => {
23
- cacheRef.current = undefined; // clear the cache (rerender on next state change)
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
+ });