From 0fd7053dcb56edeaecab436c4f7376942d30c7b8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 03:40:22 +0000 Subject: [PATCH 1/9] feat: add StateScopeProvider for isolated nested state - Introduced `StateScopeContext` and `StateScopeProvider` to manage isolated state scopes. - `StateScopeProvider` uses `React.useId` to generate a unique scope ID and renders an internal `AutoRootCtx` manager. - Updated `useDataContext` to automatically namespace context names with the current scope ID. - Updated `createRootCtx` to respect scope when checking for duplicate root mounts. - Added regression test `tests/createAutoCtx.nested.test.tsx` verifying isolation between nested providers. Co-authored-by: quynhtrang0309 <247292619+quynhtrang0309@users.noreply.github.com> --- src/state-utils/createAutoCtx.tsx | 12 ++++- src/state-utils/createRootCtx.tsx | 32 +++++++------ src/state-utils/ctx.ts | 12 +++-- tests/createAutoCtx.nested.test.tsx | 71 +++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 tests/createAutoCtx.nested.test.tsx diff --git a/src/state-utils/createAutoCtx.tsx b/src/state-utils/createAutoCtx.tsx index 7aa3cba..725e4c3 100644 --- a/src/state-utils/createAutoCtx.tsx +++ b/src/state-utils/createAutoCtx.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState, Fragment, useCallback, useMemo, Activity } from "react" -import { useDataContext, useDataSourceMultiple, useDataSubscribe, type Context } from "./ctx" +import { useEffect, useState, Fragment, useCallback, useMemo, Activity, useId } from "react" +import { useDataContext, useDataSourceMultiple, useDataSubscribe, StateScopeContext, type Context } from "./ctx" import { createRootCtx } from "./createRootCtx" import { paramsToId, type ParamsToIdRecord } from "./paramsToId" import { useQuickSubscribe } from "./useQuickSubscribe" @@ -183,3 +183,11 @@ export const createStore = { return createAutoCtx(createRootCtx(name, useFn), timeToClean, AttatchedComponent) } + +export const StateScopeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const scopeId = useId() + return + + {children} + +} diff --git a/src/state-utils/createRootCtx.tsx b/src/state-utils/createRootCtx.tsx index fed42ff..15e5dfa 100644 --- a/src/state-utils/createRootCtx.tsx +++ b/src/state-utils/createRootCtx.tsx @@ -1,5 +1,5 @@ -import { useEffect, useMemo } from "react" -import { useDataContext, useDataSourceMultiple, type Context } from "./ctx" +import { useContext, useEffect, useMemo } from "react" +import { useDataContext, useDataSourceMultiple, StateScopeContext, type Context } from "./ctx" import { paramsToId, type ParamsToIdRecord } from "./paramsToId" import { DependencyTracker } from "./utils" // import { debugObjTime } from "./debugObjTime" @@ -48,9 +48,11 @@ export const createRootCtx = { const ctxName = getCtxName(e) + const scopeId = useContext(StateScopeContext) + const scopedCtxName = scopeId ? `${scopeId}/${ctxName}` : ctxName const ctx = useDataContext(ctxName) - DependencyTracker.enter(ctxName); + DependencyTracker.enter(scopedCtxName); let state; try { state = useFn(e, { ...ctx.data }) @@ -66,13 +68,13 @@ export const createRootCtx = { - if (ctxMountedCheck.has(ctxName)) { - const err = new Error("RootContext " + ctxName + " are mounted more than once") + if (ctxMountedCheck.has(scopedCtxName)) { + const err = new Error("RootContext " + scopedCtxName + " are mounted more than once") err.stack = stack; throw err } - ctxMountedCheck.add(ctxName) - return () => { ctxMountedCheck.delete(ctxName) }; + ctxMountedCheck.add(scopedCtxName) + return () => { ctxMountedCheck.delete(scopedCtxName) }; }) return state; @@ -100,16 +102,18 @@ export const createRootCtx = => { const ctxName = getCtxName(e) + const scopeId = useContext(StateScopeContext) + const scopedCtxName = scopeId ? `${scopeId}/${ctxName}` : ctxName const stack = useMemo(() => new Error().stack, []) useEffect(() => { - if (!ctxMountedCheck.has(ctxName)) { - const err = new Error("RootContext [" + ctxName + "] is not mounted") + if (!ctxMountedCheck.has(scopedCtxName)) { + const err = new Error("RootContext [" + scopedCtxName + "] is not mounted") err.stack = stack; throw err } - }, [ctxName]) + }, [scopedCtxName]) return useDataContext(ctxName) }, @@ -119,17 +123,19 @@ export const createRootCtx = => { const ctxName = getCtxName(e) + const scopeId = useContext(StateScopeContext) + const scopedCtxName = scopeId ? `${scopeId}/${ctxName}` : ctxName const stack = useMemo(() => new Error().stack, []) useEffect(() => { - if (!ctxMountedCheck.has(ctxName)) { - const err = new Error("RootContext [" + ctxName + "] is not mounted") + if (!ctxMountedCheck.has(scopedCtxName)) { + const err = new Error("RootContext [" + scopedCtxName + "] is not mounted") err.stack = stack; let timeout = setTimeout(() => console.error(err), 1000) return () => clearTimeout(timeout) } - }, [ctxMountedCheck.has(ctxName)]) + }, [ctxMountedCheck.has(scopedCtxName)]) return useDataContext(ctxName) } diff --git a/src/state-utils/ctx.ts b/src/state-utils/ctx.ts index 073847b..7169df4 100644 --- a/src/state-utils/ctx.ts +++ b/src/state-utils/ctx.ts @@ -1,9 +1,11 @@ import { debounce, memoize, DependencyTracker } from "./utils"; -import { useEffect, useMemo, useState } from "react" +import { createContext, useContext, useEffect, useMemo, useState } from "react" import { useArrayChangeId } from "./useArrayChangeId" +export const StateScopeContext = createContext(null) + const CHANGE_EVENT = "@--change-event" class DataEvent extends Event { @@ -119,8 +121,10 @@ export type getContext = (e: string) => Context * @returns The Context instance. */ export const useDataContext = (name: string = "noname") => { - DependencyTracker.addDependency(name); - const ctx = useMemo(() => getContext(name), [name]) + const scopeId = useContext(StateScopeContext) + const namespacedName = scopeId ? `${scopeId}/${name}` : name + DependencyTracker.addDependency(namespacedName); + const ctx = useMemo(() => getContext(namespacedName), [namespacedName]) useEffect(() => { ctx.useCounter += 1; return () => { @@ -128,7 +132,7 @@ export const useDataContext = (name: string = "noname") => { if (ctx.useCounter <= 0) { setTimeout(() => { if (ctx.useCounter <= 0) { - getContext.cache.delete(JSON.stringify([name])) + getContext.cache.delete(JSON.stringify([namespacedName])) } }, 100) } diff --git a/tests/createAutoCtx.nested.test.tsx b/tests/createAutoCtx.nested.test.tsx new file mode 100644 index 0000000..746e919 --- /dev/null +++ b/tests/createAutoCtx.nested.test.tsx @@ -0,0 +1,71 @@ +import { describe, it, expect } from 'vitest' +import { render, screen, waitFor, fireEvent } from '@testing-library/react' +import React, { useId } from 'react' +import { createAutoCtx, AutoRootCtx, StateScopeProvider } from '../src/state-utils/createAutoCtx' +import { createRootCtx } from '../src/state-utils/createRootCtx' +import { useDataSubscribe } from '../src/state-utils/ctx' + +describe('AutoRootCtx Isolation', () => { + it('should isolate state between nested scopes', async () => { + // Create a simple counter store + const useCounter = () => { + const [count, setCount] = React.useState(0) + return { + count, + increment: () => setCount(c => c + 1) + } + } + + const rootCtx = createRootCtx('isolation-test', useCounter) + const autoCtx = createAutoCtx(rootCtx) + + function Consumer({ label }: { label: string }) { + const ctx = autoCtx.useCtxState({}) + const count = useDataSubscribe(ctx, 'count') + const increment = useDataSubscribe(ctx, 'increment') + + return ( +
+ {count} + +
+ ) + } + + render( + <> + + + + + + + + ) + + // Initial state: both should be 0 + await waitFor(() => { + expect(screen.getByTestId('count-outer').textContent).toBe('0') + expect(screen.getByTestId('count-inner').textContent).toBe('0') + }) + + // Increment outer + fireEvent.click(screen.getByTestId('btn-outer')) + + await waitFor(() => { + expect(screen.getByTestId('count-outer').textContent).toBe('1') + // Inner should remain 0 if isolated + expect(screen.getByTestId('count-inner').textContent).toBe('0') + }) + + // Increment inner + fireEvent.click(screen.getByTestId('btn-inner')) + + await waitFor(() => { + expect(screen.getByTestId('count-outer').textContent).toBe('1') + expect(screen.getByTestId('count-inner').textContent).toBe('1') + }) + }) +}) From 971759dc08b8e4085d85a6c4c1018e30594d0f54 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 03:54:24 +0000 Subject: [PATCH 2/9] feat: add StateScopeProvider for isolated nested state - Introduced `StateScopeContext` and `StateScopeProvider` to manage isolated state scopes. - `StateScopeProvider` uses `React.useId` to generate a unique scope ID and renders an internal `AutoRootCtx` manager. - Updated `useDataContext` to automatically namespace context names with the current scope ID. - Updated `createRootCtx` to respect scope when checking for duplicate root mounts. - Added regression test `tests/createAutoCtx.nested.test.tsx` verifying isolation between nested providers. Co-authored-by: quynhtrang0309 <247292619+quynhtrang0309@users.noreply.github.com> From d52f817318c094a4cb18d34b55c0066d72f24783 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 06:08:00 +0000 Subject: [PATCH 3/9] feat: add StateScopeProvider for isolated nested state and docs - Introduced `StateScopeContext` and `StateScopeProvider` to manage isolated state scopes. - `StateScopeProvider` uses `React.useId` to generate a unique scope ID and renders an internal `AutoRootCtx` manager. - Updated `useDataContext` to automatically namespace context names with the current scope ID. - Updated `createRootCtx` to respect scope when checking for duplicate root mounts. - Added regression test `tests/createAutoCtx.nested.test.tsx` verifying isolation between nested providers. - Updated README.md and API_DOCUMENTATION.md with instructions for Isolated State. Co-authored-by: quynhtrang0309 <247292619+quynhtrang0309@users.noreply.github.com> --- API_DOCUMENTATION.md | 21 +++++++++++++++++++++ README.md | 24 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 8f150e4..aa9a8d8 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -105,6 +105,27 @@ import 'react-state-custom/dist/react-state-custom.css'; --- +## 🎭 State Isolation + +### `StateScopeProvider` + +A component that creates an isolated state scope for its children. All stores accessed within this provider will be independent of the global scope or parent scopes. + +```typescript +function StateScopeProvider(props: { + children: React.ReactNode; +}): JSX.Element +``` + +#### Example +```tsx + + + +``` + +--- + ## ⚙️ Advanced API (Primitives) These APIs are used internally by `createStore`. You generally don't need them unless you're building custom abstractions. diff --git a/README.md b/README.md index c2325eb..343dec5 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,30 @@ export default function App() { } ``` +### 🎭 Isolated State + +Need to run multiple independent instances of your application or isolate features? Use `StateScopeProvider`. + +```tsx +import { AutoRootCtx, StateScopeProvider } from 'react-state-custom' + +function App() { + return ( + <> + {/* Global Scope */} + + + + {/* Isolated Scope - Stores here are independent of Global Scope */} + + + + ) +} +``` + +Stores used inside `StateScopeProvider` will be completely isolated from the parent or global scope, even if they share the same store definition. + --- ## 🆚 Comparison From 914965947d7b7db1c0673e4bbce55f14b1741840 Mon Sep 17 00:00:00 2001 From: Vo Thanh Dat Date: Sat, 21 Feb 2026 18:23:31 +0700 Subject: [PATCH 4/9] Update src/state-utils/createAutoCtx.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/state-utils/createAutoCtx.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/state-utils/createAutoCtx.tsx b/src/state-utils/createAutoCtx.tsx index 725e4c3..86cdc88 100644 --- a/src/state-utils/createAutoCtx.tsx +++ b/src/state-utils/createAutoCtx.tsx @@ -184,10 +184,14 @@ export const createStore = = ({ children }) => { +export const StateScopeProvider: React.FC<{ + children: React.ReactNode + Wrapper?: React.FC + debugging?: boolean +}> = ({ children, Wrapper, debugging }) => { const scopeId = useId() return - + {children} } From 22d8ea874ee05d3c79542fb0deb47e6e45e11cb8 Mon Sep 17 00:00:00 2001 From: Vo Thanh Dat Date: Sat, 21 Feb 2026 18:24:22 +0700 Subject: [PATCH 5/9] Update API_DOCUMENTATION.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- API_DOCUMENTATION.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index aa9a8d8..8f150e4 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -105,27 +105,6 @@ import 'react-state-custom/dist/react-state-custom.css'; --- -## 🎭 State Isolation - -### `StateScopeProvider` - -A component that creates an isolated state scope for its children. All stores accessed within this provider will be independent of the global scope or parent scopes. - -```typescript -function StateScopeProvider(props: { - children: React.ReactNode; -}): JSX.Element -``` - -#### Example -```tsx - - - -``` - ---- - ## ⚙️ Advanced API (Primitives) These APIs are used internally by `createStore`. You generally don't need them unless you're building custom abstractions. From 8070f43ef144d05c2d4425755d379189862e8292 Mon Sep 17 00:00:00 2001 From: Vo Thanh Dat Date: Sat, 21 Feb 2026 18:25:22 +0700 Subject: [PATCH 6/9] Update tests/createAutoCtx.nested.test.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/createAutoCtx.nested.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/createAutoCtx.nested.test.tsx b/tests/createAutoCtx.nested.test.tsx index 746e919..95fbef9 100644 --- a/tests/createAutoCtx.nested.test.tsx +++ b/tests/createAutoCtx.nested.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest' import { render, screen, waitFor, fireEvent } from '@testing-library/react' -import React, { useId } from 'react' +import React from 'react' import { createAutoCtx, AutoRootCtx, StateScopeProvider } from '../src/state-utils/createAutoCtx' import { createRootCtx } from '../src/state-utils/createRootCtx' import { useDataSubscribe } from '../src/state-utils/ctx' From a2088ba2ef212be74880990636b6d6651dc3885b Mon Sep 17 00:00:00 2001 From: Vo Thanh Dat Date: Sat, 21 Feb 2026 18:25:32 +0700 Subject: [PATCH 7/9] Update src/state-utils/createAutoCtx.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/state-utils/createAutoCtx.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state-utils/createAutoCtx.tsx b/src/state-utils/createAutoCtx.tsx index 86cdc88..20ef6e6 100644 --- a/src/state-utils/createAutoCtx.tsx +++ b/src/state-utils/createAutoCtx.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, Fragment, useCallback, useMemo, Activity, useId } from "react" +import { useEffect, useState, Fragment, useCallback, useMemo, useId } from "react" import { useDataContext, useDataSourceMultiple, useDataSubscribe, StateScopeContext, type Context } from "./ctx" import { createRootCtx } from "./createRootCtx" import { paramsToId, type ParamsToIdRecord } from "./paramsToId" From fc8b06eba365f62301faa77cc09742e0a49c9812 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:30:31 +0000 Subject: [PATCH 8/9] Initial plan From f5b9c1ab38b2a8f8830c4ace0779314a505ce728 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:31:11 +0000 Subject: [PATCH 9/9] re-export StateScopeProvider from package entrypoint Co-authored-by: vothanhdat <8221386+vothanhdat@users.noreply.github.com> --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index dcd2416..88aa5c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ export { } from "./state-utils/ctx" export { createRootCtx } from "./state-utils/createRootCtx" -export { AutoRootCtx, createAutoCtx, createStore } from "./state-utils/createAutoCtx" +export { AutoRootCtx, createAutoCtx, createStore, StateScopeProvider } from "./state-utils/createAutoCtx" export { useArrayChangeId } from "./state-utils/useArrayChangeId" export { paramsToId, type ParamsToIdRecord, type ParamsToIdInput } from "./state-utils/paramsToId"