recoil
Version:
Recoil - A state management library for React
271 lines (262 loc) • 9.39 kB
Flow
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+recoil
* @flow strict-local
* @format
*/
;
import type { RecoilState } from '../../core/Recoil_RecoilValue';
const {
getRecoilTestFn
} = require('../../__test_utils__/Recoil_TestingUtils');
let React, act, atom, componentThatReadsAndWritesAtom, gkx, useRecoilValue, useRecoilValueLoadable, useRetain, useRecoilCallback, useState, selector, renderElements, retentionZone;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({
useState
} = require('react'));
({
act
} = require('ReactTestUtils'));
({
retentionZone
} = require('../../core/Recoil_RetentionZone'));
({
useRecoilValue,
useRecoilValueLoadable
} = require('../../hooks/Recoil_Hooks'));
useRecoilCallback = require('../../hooks/Recoil_useRecoilCallback');
useRetain = require('../../hooks/Recoil_useRetain');
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
componentThatReadsAndWritesAtom,
renderElements
} = require('../../__test_utils__/Recoil_TestingUtils'));
gkx = require('../../util/Recoil_gkx');
const initialGKValue = gkx('recoil_memory_managament_2020');
gkx.setPass('recoil_memory_managament_2020');
return () => {
initialGKValue || gkx.setFail('recoil_memory_managament_2020');
};
});
let nextKey = 0;
declare function atomRetainedBy(retainedBy: any): any;
declare function switchComponent(defaultVisible: any): any; // Mounts a component that reads the given atom, sets its value, then unmounts it
// and re-mounts it again. Checks whether the value of the atom that was written
// is still observed. If otherChildren is provided, it will be mounted throughout this,
// then at the end it will be unmounted and the atom expected to be released.
declare function testWhetherAtomIsRetained(shouldBeRetained: boolean, atom: RecoilState<number>, otherChildren: any): void;
describe('Default retention', () => {
testRecoil('By default, atoms are retained for the lifetime of the root', () => {
testWhetherAtomIsRetained(true, atomRetainedBy(undefined));
});
});
describe('Component-level retention', () => {
testRecoil('With retainedBy: components, atoms are released when not in use', () => {
testWhetherAtomIsRetained(false, atomRetainedBy('components'));
});
testRecoil('An atom is retained by a component being subscribed to it', () => {
const anAtom = atomRetainedBy('components');
declare function Subscribes(): any;
testWhetherAtomIsRetained(true, anAtom, <Subscribes />);
});
testRecoil('An atom is retained by a component retaining it explicitly', () => {
const anAtom = atomRetainedBy('components');
declare function Retains(): any;
testWhetherAtomIsRetained(true, anAtom, <Retains />);
});
});
describe('RetentionZone retention', () => {
testRecoil('An atom can be retained via a retention zone', () => {
const zone = retentionZone();
const anAtom = atomRetainedBy(zone);
declare function RetainsZone(): any;
testWhetherAtomIsRetained(true, anAtom, <RetainsZone />);
});
});
describe('Retention of and via selectors', () => {
testRecoil('An atom is retained when a depending selector is retained', () => {
const anAtom = atomRetainedBy('components');
const aSelector = selector({
key: '...',
retainedBy_UNSTABLE: 'components',
get: ({
get
}) => {
return get(anAtom);
}
});
declare function SubscribesToSelector(): any;
testWhetherAtomIsRetained(true, anAtom, <SubscribesToSelector />);
});
declare var flushPromises: () => any;
testRecoil('An async selector is not released when its only subscribed component suspends', async () => {
let resolve;
let evalCount = 0;
const anAtom = atomRetainedBy('components');
const aSelector = selector({
key: '......',
retainedBy_UNSTABLE: 'components',
get: ({
get
}) => {
evalCount++;
get(anAtom);
return new Promise(r => {
resolve = r;
});
}
});
declare function SubscribesToSelector(): any;
const c = renderElements(<SubscribesToSelector />);
expect(c.textContent).toEqual('loading');
expect(evalCount).toBe(1);
act(() => resolve(123)); // We need to let the selector promise resolve but NOT flush timeouts because
// we do release after suspending after a timeout and we don't want that
// to happen because we're testing what happens when it doesn't.
await flushPromises();
await flushPromises();
expect(c.textContent).toEqual('123');
expect(evalCount).toBe(1); // Still in cache, hence wasn't released.
});
testRecoil('An async selector ignores promises that settle after it is released', async () => {
let resolve;
let evalCount = 0;
const anAtom = atomRetainedBy('components');
const aSelector = selector({
key: 'retention/asyncSettlesAfterRelease',
retainedBy_UNSTABLE: 'components',
get: ({
get
}) => {
evalCount++;
get(anAtom);
return new Promise(r => {
resolve = r;
});
}
});
declare function SubscribesToSelector(): any;
const [Switch, setMounted] = switchComponent(true);
const c = renderElements(<Switch>
<SubscribesToSelector />
</Switch>);
expect(c.textContent).toEqual('loading');
expect(evalCount).toBe(1);
act(() => setMounted(false)); // release selector while promise is in flight
act(() => resolve(123));
await flushPromises();
act(() => setMounted(true));
expect(evalCount).toBe(2); // selector must be re-evaluated because the resolved value is not in cache
expect(c.textContent).toEqual('loading');
act(() => resolve(123));
await flushPromises();
expect(c.textContent).toEqual('123');
});
testRecoil('Selector changing deps releases old deps, retains new ones', () => {
const switchAtom = atom({
key: 'switch',
default: false
});
const depA = atomRetainedBy('components');
const depB = atomRetainedBy('components');
const theSelector = selector({
key: 'sel',
get: ({
get
}) => {
if (get(switchAtom)) {
return get(depB);
} else {
return get(depA);
}
},
retainedBy_UNSTABLE: 'components'
});
let setup;
declare function Setup(): any;
declare function ReadsSelector(): any;
let depAValue;
declare function ReadsDepA(): any;
let depBValue;
declare function ReadsDepB(): any;
const [MountSwitch, setAtomsMountedDirectly] = switchComponent(true);
declare function unmountAndRemount(): any;
const [ReadsSwitch, setDepSwitch] = componentThatReadsAndWritesAtom(switchAtom);
renderElements(<>
<ReadsSelector />
<ReadsSwitch />
<MountSwitch>
<ReadsDepA />
<ReadsDepB />
</MountSwitch>
<Setup />
</>);
act(() => {
setup();
});
unmountAndRemount();
expect(depAValue).toBe(123);
expect(depBValue).toBe(0);
act(() => {
setDepSwitch(true);
});
unmountAndRemount();
expect(depAValue).toBe(0);
act(() => {
setup();
});
unmountAndRemount();
expect(depBValue).toBe(456);
});
});
describe('Retention during a transaction', () => {
testRecoil('Atoms are not released if unmounted and mounted within the same transaction', () => {
const anAtom = atomRetainedBy('components');
const [ReaderA, setAtom] = componentThatReadsAndWritesAtom(anAtom);
const [ReaderB] = componentThatReadsAndWritesAtom(anAtom);
const [SwitchA, setSwitchA] = switchComponent(true);
const [SwitchB, setSwitchB] = switchComponent(false);
const container = renderElements(<>
<SwitchA>
<ReaderA />
</SwitchA>
<SwitchB>
<ReaderB />
</SwitchB>
</>);
act(() => setAtom(123));
act(() => {
setSwitchA(false);
setSwitchB(true);
});
expect(container.textContent).toEqual('123');
});
testRecoil('An atom is released when two zones retaining it are released at the same time', () => {
const zoneA = retentionZone();
const zoneB = retentionZone();
const anAtom = atomRetainedBy([zoneA, zoneB]);
declare function RetainsZone(arg0: any): any; // It's the no-longer-retained-when-unmounting-otherChildren part that is
// important for this test.
testWhetherAtomIsRetained(true, anAtom, <>
<RetainsZone zone={zoneA} />
<RetainsZone zone={zoneB} />
</>);
});
testRecoil('An atom is released when both direct-retainer and zone-retainer are released at the same time', () => {
const zone = retentionZone();
const anAtom = atomRetainedBy(zone);
declare function RetainsZone(): any;
declare function RetainsAtom(): any; // It's the no-longer-retained-when-unmounting-otherChildren part that is
// important for this test.
testWhetherAtomIsRetained(true, anAtom, <>
<RetainsZone />
<RetainsAtom />
</>);
});
});