Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@assistant-ui/store

Package Overview
Dependencies
Maintainers
2
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@assistant-ui/store - npm Package Compare versions

Comparing version
0.2.9
to
0.2.10
+136
src/__tests__/RenderChildrenWithAccessor.test.tsx
// @vitest-environment jsdom
import type { ReactNode } from "react";
import { act, render } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { AuiProvider } from "../utils/react-assistant-context";
import { RenderChildrenWithAccessor } from "../RenderChildrenWithAccessor";
import { PROXIED_ASSISTANT_STATE_SYMBOL } from "../utils/proxied-assistant-state";
afterEach(() => {
vi.restoreAllMocks();
});
type Listener = () => void;
const createTestAuiClient = () => {
const listeners = new Set<Listener>();
let itemState: { value: number; isEditing: boolean } = {
value: 1,
isEditing: false,
};
const proxiedState = {
item: itemState,
};
const client = {
subscribe: (listener: Listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
on: () => () => {},
[PROXIED_ASSISTANT_STATE_SYMBOL]: proxiedState,
} as const;
return {
client,
getItemState: () => itemState,
update: (next: Partial<typeof itemState>) => {
itemState = { ...itemState, ...next };
proxiedState.item = itemState;
// biome-ignore lint/suspicious/useIterableCallbackReturn: forEach callback intentionally has no return
listeners.forEach((listener) => listener());
},
};
};
describe("RenderChildrenWithAccessor", () => {
it("re-renders when accessed state updates (regression: issue #3838)", () => {
const testClient = createTestAuiClient();
const wrapper = ({ children }: { children: ReactNode }) => (
<AuiProvider value={testClient.client as never}>{children}</AuiProvider>
);
const { container } = render(
<RenderChildrenWithAccessor
getItemState={() => testClient.getItemState()}
>
{(getItem) => {
const item = getItem();
return <div>{item.isEditing ? "editing" : "viewing"}</div>;
}}
</RenderChildrenWithAccessor>,
{ wrapper },
);
expect(container.textContent).toBe("viewing");
act(() => {
testClient.update({ isEditing: true });
});
expect(container.textContent).toBe("editing");
act(() => {
testClient.update({ isEditing: false });
});
expect(container.textContent).toBe("viewing");
});
it("does not schedule an extra render on first access (initial snapshot matches getItemState)", () => {
const testClient = createTestAuiClient();
const wrapper = ({ children }: { children: ReactNode }) => (
<AuiProvider value={testClient.client as never}>{children}</AuiProvider>
);
const renderSpy = vi.fn();
render(
<RenderChildrenWithAccessor
getItemState={() => testClient.getItemState()}
>
{(getItem) => {
renderSpy();
const item = getItem();
return <div>{item.value}</div>;
}}
</RenderChildrenWithAccessor>,
{ wrapper },
);
// first mount accesses the item; useSyncExternalStore's post-commit
// tearing check should see a stable snapshot and not force a re-render
expect(renderSpy).toHaveBeenCalledTimes(1);
});
it("does not re-render when item is never accessed", () => {
const testClient = createTestAuiClient();
const wrapper = ({ children }: { children: ReactNode }) => (
<AuiProvider value={testClient.client as never}>{children}</AuiProvider>
);
const renderSpy = vi.fn();
render(
<RenderChildrenWithAccessor
getItemState={() => testClient.getItemState()}
>
{() => {
renderSpy();
return <div>static</div>;
}}
</RenderChildrenWithAccessor>,
{ wrapper },
);
const initialRenderCount = renderSpy.mock.calls.length;
act(() => {
testClient.update({ value: 99 });
});
expect(renderSpy.mock.calls.length).toBe(initialRenderCount);
});
});
+1
-1

@@ -1,1 +0,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"}
{"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"}

@@ -7,12 +7,16 @@ "use client";

const aui = useAui();
// if the consumer never accesses the item, do not trigger rerenders
const cacheRef = useRef(undefined);
// Track access with a dedicated flag:
// useSyncExternalStore may call getSnapshot() after commit (tearing checks),
// which would re-cache the current state and mask later real updates.
// Use the current state as the pre-access snapshot so the post-commit check
// matches getItemState(aui) and doesn't schedule an unnecessary re-render.
const accessedRef = useRef(false);
const currentValue = accessedRef.current ? null : getItemState(aui);
useAuiState(() => {
if (cacheRef.current === undefined) {
cacheRef.current = getItemState(aui);
}
return cacheRef.current;
if (!accessedRef.current)
return currentValue;
return getItemState(aui);
});
return () => {
cacheRef.current = undefined; // clear the cache (rerender on next state change)
accessedRef.current = true;
return getItemState(aui);

@@ -19,0 +23,0 @@ };

@@ -1,1 +0,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"}
{"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"}
{
"name": "@assistant-ui/store",
"version": "0.2.9",
"version": "0.2.10",
"description": "Tap-based state management for @assistant-ui",

@@ -33,3 +33,3 @@ "keywords": [

"peerDependencies": {
"@assistant-ui/tap": "^0.5.10",
"@assistant-ui/tap": "^0.5.11",
"@types/react": "*",

@@ -47,7 +47,7 @@ "react": "^18 || ^19"

"@types/react-dom": "^19.2.3",
"jsdom": "^29.1.0",
"jsdom": "^29.1.1",
"react": "^19.2.5",
"vitest": "^4.1.5",
"@assistant-ui/tap": "0.5.10",
"@assistant-ui/x-buildutils": "0.0.6"
"@assistant-ui/tap": "0.5.11",
"@assistant-ui/x-buildutils": "0.0.7"
},

@@ -54,0 +54,0 @@ "publishConfig": {

@@ -13,14 +13,16 @@ "use client";

// if the consumer never accesses the item, do not trigger rerenders
const cacheRef = useRef<T | undefined>(undefined);
// Track access with a dedicated flag:
// useSyncExternalStore may call getSnapshot() after commit (tearing checks),
// which would re-cache the current state and mask later real updates.
// Use the current state as the pre-access snapshot so the post-commit check
// matches getItemState(aui) and doesn't schedule an unnecessary re-render.
const accessedRef = useRef(false);
const currentValue = accessedRef.current ? null : getItemState(aui);
useAuiState(() => {
if (cacheRef.current === undefined) {
cacheRef.current = getItemState(aui);
}
return cacheRef.current;
if (!accessedRef.current) return currentValue;
return getItemState(aui);
});
return () => {
cacheRef.current = undefined; // clear the cache (rerender on next state change)
accessedRef.current = true;
return getItemState(aui);

@@ -27,0 +29,0 @@ };