diff --git a/__tests__/ScopeProvider/08_noscope.tsx b/__tests__/ScopeProvider/08_noscope.tsx
new file mode 100644
index 0000000..8ef407a
--- /dev/null
+++ b/__tests__/ScopeProvider/08_noscope.tsx
@@ -0,0 +1,65 @@
+import { render } from '@testing-library/react';
+import { atom, useAtomValue } from 'jotai';
+import { getTextContents } from '../utils';
+import { ScopeProvider } from '../../src/index';
+
+let i = 1;
+const AtomA = atom(() => i++);
+const AtomB = atom((get) => get(AtomA));
+
+const Child = ({ level }: { level?: string }) => {
+ const valueA = useAtomValue(AtomA);
+ const valueB = useAtomValue(AtomB);
+ return (
+
+ Atom A is not scoped so its value should always be 1
+
{valueA}
+ Atom B is scoped, so its will use the implicitly scoped Atom A
+
{valueB}
+
+ );
+};
+
+/*
+ AtomA
+ S0[]: AtomA0
+ S1[AtomA!]: AtomA!
+ S2[]: AtomA!
+*/
+const App = () => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const { container } = render();
+
+describe('No Scope', () => {
+ test('AtomA is not scoped so its value should always be 1', () => {
+ const selectors = [
+ '.level0 .valueA',
+ '.level0 .valueB',
+ '.level1 .valueA',
+ '.level1 .valueB',
+ '.level2 .valueA',
+ '.level2 .valueB',
+ ];
+
+ expect(getTextContents(container, selectors)).toEqual([
+ '1', // level0 valueA
+ '1', // level0 valueB
+ '1', // level1 valueA
+ '2', // level1 valueB
+ '1', // level1 valueA
+ '2', // level1 valueB
+ ]);
+ });
+});
diff --git a/src/ScopeProvider/ScopeProvider.tsx b/src/ScopeProvider/ScopeProvider.tsx
index fe85f14..869c106 100644
--- a/src/ScopeProvider/ScopeProvider.tsx
+++ b/src/ScopeProvider/ScopeProvider.tsx
@@ -12,16 +12,22 @@ import { createPatchedStore, isTopLevelScope } from './patchedStore';
const ScopeContext = createContext<{
scope: Scope | undefined;
baseStore: Store | undefined;
-}>({ scope: undefined, baseStore: undefined });
+ noScopeSet: Set;
+}>({ scope: undefined, baseStore: undefined, noScopeSet: new Set() });
export const ScopeProvider = ({
atoms,
+ noScope = [],
children,
debugName,
-}: PropsWithChildren<{ atoms: Iterable; debugName?: string }>) => {
+}: PropsWithChildren<{
+ atoms: Iterable;
+ noScope?: Iterable;
+ debugName?: string;
+}>) => {
const parentStore: Store = useStore();
- let { scope: parentScope, baseStore = parentStore } =
- useContext(ScopeContext);
+ const { noScopeSet: parentNoScopeSet, ...parent } = useContext(ScopeContext);
+ let { scope: parentScope, baseStore = parentStore } = parent;
// if this ScopeProvider is the first descendant scope under Provider then it is the top level scope
// https://github.com/jotaijs/jotai-scope/pull/33#discussion_r1604268003
if (isTopLevelScope(parentStore)) {
@@ -31,21 +37,25 @@ export const ScopeProvider = ({
// atomSet is used to detect if the atoms prop has changed.
const atomSet = new Set(atoms);
+ // noScopeSet defines atoms that should not be scoped
+ const noScopeSet = new Set([...noScope, ...parentNoScopeSet]);
function initialize() {
- const scope = createScope(atoms, parentScope, debugName);
+ const scope = createScope(atoms, noScopeSet, parentScope, debugName);
return {
patchedStore: createPatchedStore(baseStore, scope),
- scopeContext: { scope, baseStore },
+ scopeContext: { scope, baseStore, noScopeSet },
hasChanged(current: {
- baseStore: Store;
parentScope: Scope | undefined;
+ baseStore: Store;
atomSet: Set;
+ noScopeSet: Set;
}) {
return (
parentScope !== current.parentScope ||
+ current.baseStore !== baseStore ||
!isEqualSet(atomSet, current.atomSet) ||
- current.baseStore !== baseStore
+ !isEqualSet(noScopeSet, current.noScopeSet)
);
},
};
@@ -53,7 +63,7 @@ export const ScopeProvider = ({
const [state, setState] = useState(initialize);
const { hasChanged, scopeContext, patchedStore } = state;
- if (hasChanged({ parentScope, atomSet, baseStore })) {
+ if (hasChanged({ parentScope, baseStore, atomSet, noScopeSet })) {
setState(initialize);
}
return (
diff --git a/src/ScopeProvider/scope.ts b/src/ScopeProvider/scope.ts
index b953888..882d6b5 100644
--- a/src/ScopeProvider/scope.ts
+++ b/src/ScopeProvider/scope.ts
@@ -1,5 +1,5 @@
import { atom, type Atom } from 'jotai';
-import { type AnyAtom, type AnyWritableAtom } from './types';
+import type { AnyAtom, AnyWritableAtom } from './types';
export type Scope = {
/**
@@ -40,6 +40,7 @@ type GlobalScopeKey = typeof globalScopeKey;
export function createScope(
atoms: Iterable,
+ noScopeSet: Set,
parentScope: Scope | undefined,
scopeName?: string | undefined,
): Scope {
@@ -52,6 +53,7 @@ export function createScope(
getAtom,
prepareWriteAtom(anAtom, originalAtom, implicitScope) {
if (
+ !noScopeSet.has(originalAtom) &&
originalAtom.read === defaultRead &&
isWritableAtom(originalAtom) &&
isWritableAtom(anAtom) &&
@@ -154,17 +156,17 @@ export function createScope(
}
/**
- * @returns a copy of the atom for derived atoms or the original atom for primitive and writable atoms
+ * @returns a copy of the atom for derived atoms or the original atom for primitive, writable, and noScoped atoms
*/
function inheritAtom(
anAtom: Atom,
originalAtom: Atom,
implicitScope?: Scope,
) {
- if (originalAtom.read !== defaultRead) {
- return cloneAtom(originalAtom, implicitScope);
+ if (originalAtom.read === defaultRead || noScopeSet.has(originalAtom)) {
+ return anAtom;
}
- return anAtom;
+ return cloneAtom(originalAtom, implicitScope);
}
/**