Skip to content

Commit

Permalink
feat: add no-scope api to ScopeProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
David Maskasky committed May 23, 2024
1 parent 2954118 commit 1563d17
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 14 deletions.
65 changes: 65 additions & 0 deletions __tests__/ScopeProvider/08_noscope.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={level}>
Atom A is not scoped so its value should always be 1
<div className="valueA">{valueA}</div>
Atom B is scoped, so its will use the implicitly scoped Atom A
<div className="valueB">{valueB}</div>
</div>
);
};

/*
AtomA
S0[]: AtomA0
S1[AtomA!]: AtomA!
S2[]: AtomA!
*/
const App = () => {
return (
<div className="App">
<Child level="level0" />
<ScopeProvider atoms={[AtomB]} noScope={[AtomA]} debugName="level1">
<Child level="level1" />
<ScopeProvider atoms={[]} debugName="level2">
<Child level="level2" />
</ScopeProvider>
</ScopeProvider>
</div>
);
};

const { container } = render(<App />);

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
]);
});
});
28 changes: 19 additions & 9 deletions src/ScopeProvider/ScopeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,22 @@ import { createPatchedStore, isTopLevelScope } from './patchedStore';
const ScopeContext = createContext<{
scope: Scope | undefined;
baseStore: Store | undefined;
}>({ scope: undefined, baseStore: undefined });
noScopeSet: Set<AnyAtom>;
}>({ scope: undefined, baseStore: undefined, noScopeSet: new Set() });

export const ScopeProvider = ({
atoms,
noScope = [],
children,
debugName,
}: PropsWithChildren<{ atoms: Iterable<AnyAtom>; debugName?: string }>) => {
}: PropsWithChildren<{
atoms: Iterable<AnyAtom>;
noScope?: Iterable<AnyAtom>;
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)) {
Expand All @@ -31,29 +37,33 @@ 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<AnyAtom>;
noScopeSet: Set<AnyAtom>;
}) {
return (
parentScope !== current.parentScope ||
current.baseStore !== baseStore ||
!isEqualSet(atomSet, current.atomSet) ||
current.baseStore !== baseStore
!isEqualSet(noScopeSet, current.noScopeSet)
);
},
};
}

const [state, setState] = useState(initialize);
const { hasChanged, scopeContext, patchedStore } = state;
if (hasChanged({ parentScope, atomSet, baseStore })) {
if (hasChanged({ parentScope, baseStore, atomSet, noScopeSet })) {
setState(initialize);
}
return (
Expand Down
12 changes: 7 additions & 5 deletions src/ScopeProvider/scope.ts
Original file line number Diff line number Diff line change
@@ -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 = {
/**
Expand Down Expand Up @@ -40,6 +40,7 @@ type GlobalScopeKey = typeof globalScopeKey;

export function createScope(
atoms: Iterable<AnyAtom>,
noScopeSet: Set<AnyAtom>,
parentScope: Scope | undefined,
scopeName?: string | undefined,
): Scope {
Expand All @@ -52,6 +53,7 @@ export function createScope(
getAtom,
prepareWriteAtom(anAtom, originalAtom, implicitScope) {
if (
!noScopeSet.has(originalAtom) &&
originalAtom.read === defaultRead &&
isWritableAtom(originalAtom) &&
isWritableAtom(anAtom) &&
Expand Down Expand Up @@ -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<T>(
anAtom: Atom<T>,
originalAtom: Atom<T>,
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);
}

/**
Expand Down

0 comments on commit 1563d17

Please sign in to comment.