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); } /**