UNPKG

recoil

Version:

Recoil - A state management library for React

1,064 lines (1,001 loc) 39 kB
/** * 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 * @format */ /* eslint-disable fb-www/react-no-useless-fragment */ 'use strict'; import type { RecoilState, RecoilValue, RecoilValueReadOnly } from '../../core/Recoil_RecoilValue'; import type { PersistenceSettings } from '../../recoil_values/Recoil_atom'; const { getRecoilTestFn } = require('../../testing/Recoil_TestingUtils'); let React, useEffect, useState, act, Queue, batchUpdates, atom, errorSelector, selector, ReadsAtom, asyncSelector, errorThrowingAsyncSelector, flushPromisesAndTimers, renderElements, renderElementsWithSuspenseCount, recoilComponentGetRecoilValueCount_FOR_TESTING, useRecoilState, useRecoilStateLoadable, useRecoilValue, useRecoilValueLoadable, useSetRecoilState, useSetUnvalidatedAtomValues, useTransactionObservation_DEPRECATED, invariant; let { mutableSourceExists } = require('../../util/Recoil_mutableSource'); const testRecoil = getRecoilTestFn(() => { React = require('React'); ({ useEffect, useState } = require('React')); ({ act } = require('ReactTestUtils')); Queue = require('../../adt/Recoil_Queue'); ({ batchUpdates } = require('../../core/Recoil_Batching')); atom = require('../../recoil_values/Recoil_atom'); errorSelector = require('../../recoil_values/Recoil_errorSelector'); selector = require('../../recoil_values/Recoil_selector'); ({ ReadsAtom, asyncSelector, errorThrowingAsyncSelector, flushPromisesAndTimers, renderElements, renderElementsWithSuspenseCount } = require('../../testing/Recoil_TestingUtils')); ({ mutableSourceExists } = require('../../util/Recoil_mutableSource')); ({ recoilComponentGetRecoilValueCount_FOR_TESTING, useRecoilState, useRecoilStateLoadable, useRecoilValue, useRecoilValueLoadable, useSetRecoilState, useSetUnvalidatedAtomValues, useTransactionObservation_DEPRECATED } = require('../Recoil_Hooks')); invariant = require('../../util/Recoil_invariant'); }); // When not using mutable source there's usually an extra call/render. const BASE_CALLS = mutableSourceExists() ? 0 : 1; let fbOnlyTest = test.skip; // @fb-only: fbOnlyTest = test; let nextID = 0; declare function counterAtom(persistence?: PersistenceSettings<number>): any; declare function booleanAtom(persistence?: PersistenceSettings<boolean>): any; declare function plusOneSelector(dep: RecoilValue<number>): any; declare function plusOneAsyncSelector(dep: RecoilValue<number>): [RecoilValueReadOnly<number>, (number) => void]; declare function additionSelector(depA: RecoilValue<number>, depB: RecoilValue<number>): any; declare function asyncSelectorThatPushesPromisesOntoArray(dep: RecoilValue<any>): any; declare function componentThatReadsAndWritesAtom<T>(atom: RecoilState<T>): [React.AbstractComponent<{...}>, (((T) => T) | T) => void]; declare function componentThatWritesAtom<T>(atom: RecoilState<T>): [any, (((T) => T) | T) => void]; declare function componentThatReadsTwoAtoms(one: any, two: any): any; declare function componentThatReadsAtomWithCommitCount(atom: any): any; declare function componentThatToggles(a: any, b: any): any; declare function ObservesTransactions(arg0: any): any; declare function advanceTimersBy(ms: any): any; testRecoil('Component throws error when passing invalid node', async () => { declare function Component(): any; const container = renderElements(<Component />); expect(container.textContent).toEqual('CAUGHT'); }); testRecoil('Components are re-rendered when atoms change', async () => { const anAtom = counterAtom(); const [Component, updateValue] = componentThatReadsAndWritesAtom(anAtom); const container = renderElements(<Component />); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }); testRecoil('Selectors are updated when upstream atoms change', () => { const anAtom = counterAtom(); const [aSelector, _] = plusOneSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={aSelector} /> </>); expect(container.textContent).toEqual('1'); act(() => updateValue(1)); expect(container.textContent).toEqual('2'); }); testRecoil('Selectors can depend on other selectors', () => { const anAtom = counterAtom(); const [selectorA, _] = plusOneSelector(anAtom); const [selectorB, __] = plusOneSelector(selectorA); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={selectorB} /> </>); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('3'); }); testRecoil('Selectors can depend on async selectors', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [selectorA, _] = plusOneAsyncSelector(anAtom); const [selectorB, __] = plusOneSelector(selectorA); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={selectorB} /> </>); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('3'); }); testRecoil('Async selectors can depend on async selectors', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [selectorA, _] = plusOneAsyncSelector(anAtom); const [selectorB, __] = plusOneAsyncSelector(selectorA); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={selectorB} /> </>); if (mutableSourceExists()) { await flushPromisesAndTimers(); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); expect(container.textContent).toEqual('3'); } else { // we need to test the useRecoilValueLoadable_LEGACY method expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('2'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('3'); } }); testRecoil('Dep of upstream selector can change while pending', async isSelectorGkPassing => { if (isSelectorGkPassing) { const anAtom = counterAtom(); const [upstreamSel, upstreamResolvers] = asyncSelectorThatPushesPromisesOntoArray(anAtom); const [downstreamSel, downstreamResolvers] = asyncSelectorThatPushesPromisesOntoArray(upstreamSel); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={downstreamSel} /> </>); // Initially, upstream has returned a promise so there is one upstream resolver. // Downstream is waiting on upstream so it hasn't returned anything yet. expect(container.textContent).toEqual('loading'); expect(upstreamResolvers.length).toEqual(1); expect(downstreamResolvers.length).toEqual(0); // Resolve upstream; downstream should now have returned a new promise: upstreamResolvers[0][0](123); await flushPromisesAndTimers(); expect(downstreamResolvers.length).toEqual(1); // Update atom to a new value while downstream is pending: act(() => updateValue(1)); await flushPromisesAndTimers(); // Upstream returns a new promise for the new atom value. // Downstream is once again waiting on upstream so it hasn't returned a new // promise for the new value. expect(upstreamResolvers.length).toEqual(2); expect(downstreamResolvers.length).toEqual(1); // Resolve the new upstream promise: upstreamResolvers[1][0](123); await flushPromisesAndTimers(); // Downstream can now return its new promise: expect(downstreamResolvers.length).toEqual(2); // If we resolve downstream's new promise we should see the result: downstreamResolvers[1][0](123); await flushPromisesAndTimers(); expect(container.textContent).toEqual('123'); } }); testRecoil('Errors are propogated through selectors', () => { const errorThrower = errorSelector('ERROR'); const [downstreamSelector] = plusOneSelector(errorThrower); const container = renderElements(<> <ReadsAtom atom={downstreamSelector} /> </>); expect(container.textContent).toEqual('error'); }); testRecoil('Rejected promises are propogated through selectors (immediate rejection)', async () => { const anAtom = counterAtom(); const errorThrower = errorThrowingAsyncSelector('ERROR', anAtom); const [downstreamSelector] = plusOneAsyncSelector(errorThrower); const container = renderElements(<> <ReadsAtom atom={downstreamSelector} /> </>); expect(container.textContent).toEqual('loading'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('error'); }); testRecoil('Rejected promises are propogated through selectors (later rejection)', async () => { const anAtom = counterAtom(); const [errorThrower, _resolve, reject] = asyncSelector(anAtom); const [downstreamSelector] = plusOneAsyncSelector(errorThrower); const container = renderElements(<> <ReadsAtom atom={downstreamSelector} /> </>); expect(container.textContent).toEqual('loading'); act(() => reject(new Error())); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('error'); }); testRecoil('Component subscribed to atom is rendered just once', () => { const anAtom = counterAtom(); const [Component, updateValue] = componentThatReadsAndWritesAtom(anAtom); renderElements(<> <Component /> </>); expect(Component).toHaveBeenCalledTimes(BASE_CALLS + 1); act(() => updateValue(1)); expect(Component).toHaveBeenCalledTimes(BASE_CALLS + 2); }); testRecoil('Write-only components are not subscribed', () => { const anAtom = counterAtom(); const [Component, updateValue] = componentThatWritesAtom(anAtom); renderElements(<> <Component /> </>); expect(Component).toHaveBeenCalledTimes(1); act(() => updateValue(1)); expect(Component).toHaveBeenCalledTimes(1); }); testRecoil('Component that depends on atom in multiple ways is rendered just once', () => { const anAtom = counterAtom(); const [aSelector, _] = plusOneSelector(anAtom); const [WriteComp, updateValue] = componentThatWritesAtom(anAtom); const ReadComp = componentThatReadsTwoAtoms(anAtom, aSelector); renderElements(<> <WriteComp /> <ReadComp /> </>); expect(ReadComp).toHaveBeenCalledTimes(BASE_CALLS + 1); act(() => updateValue(1)); expect(ReadComp).toHaveBeenCalledTimes(BASE_CALLS + 2); }); testRecoil('Selector functions are evaluated just once', () => { const anAtom = counterAtom(); const [aSelector, selectorFn] = plusOneSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); renderElements(<> <Component /> <ReadsAtom atom={aSelector} /> </>); expect(selectorFn).toHaveBeenCalledTimes(1); act(() => updateValue(1)); expect(selectorFn).toHaveBeenCalledTimes(2); }); testRecoil('Selector functions are evaluated just once even if multiple upstreams change', () => { const atomA = counterAtom(); const atomB = counterAtom(); const [aSelector, selectorFn] = additionSelector(atomA, atomB); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); renderElements(<> <ComponentA /> <ComponentB /> <ReadsAtom atom={aSelector} /> </>); expect(selectorFn).toHaveBeenCalledTimes(1); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); expect(selectorFn).toHaveBeenCalledTimes(2); }); testRecoil('Component that depends on multiple atoms via selector is rendered just once', () => { const atomA = counterAtom(); const atomB = counterAtom(); const [aSelector, _] = additionSelector(atomA, atomB); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(aSelector); renderElements(<> <ComponentA /> <ComponentB /> <ReadComp /> </>); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); }); testRecoil('Component that depends on multiple atoms directly is rendered just once', () => { const atomA = counterAtom(); const atomB = counterAtom(); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const ReadComp = componentThatReadsTwoAtoms(atomA, atomB); renderElements(<> <ComponentA /> <ComponentB /> <ReadComp /> </>); expect(ReadComp).toHaveBeenCalledTimes(BASE_CALLS + 1); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); expect(ReadComp).toHaveBeenCalledTimes(BASE_CALLS + 2); }); testRecoil('Component is rendered just once when atom is changed twice', () => { const atomA = counterAtom(); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(atomA); renderElements(<> <ComponentA /> <ReadComp /> </>); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); act(() => { batchUpdates(() => { updateValueA(1); updateValueA(2); }); }); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); }); testRecoil('Component does not re-read atom when rendered due to another atom changing, parent re-render, or other state change', () => { const atomA = counterAtom(); const atomB = counterAtom(); let _, setLocal; let _a, setA; let _b, _setB; declare function Component(): any; let __, setParentLocal; declare function Parent(): any; renderElements(<Parent />); if (mutableSourceExists()) { const initialCalls = recoilComponentGetRecoilValueCount_FOR_TESTING.current; expect(initialCalls).toBeGreaterThan(0); // No re-read when setting local state on the component: act(() => { setLocal(1); }); expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe(initialCalls); // No re-read when setting local state on its parent causing it to re-render: act(() => { setParentLocal(1); }); expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe(initialCalls); // Setting an atom causes a re-read for that atom only, not others: act(() => { setA(1); }); expect(recoilComponentGetRecoilValueCount_FOR_TESTING.current).toBe(initialCalls + 1); } }); testRecoil('Can subscribe to and also change an atom in the same batch', () => { const anAtom = counterAtom(); let setVisible; declare function Switch(arg0: any): any; const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <Switch> <ReadsAtom atom={anAtom} /> </Switch> </>); expect(container.textContent).toEqual(''); act(() => { batchUpdates(() => { setVisible(true); updateValue(1337); }); }); expect(container.textContent).toEqual('1337'); }); testRecoil('Atom values are retained when atom has no subscribers', () => { const anAtom = counterAtom(); let setVisible; declare function Switch(arg0: any): any; const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <Switch> <ReadsAtom atom={anAtom} /> </Switch> </>); act(() => updateValue(1337)); expect(container.textContent).toEqual('1337'); act(() => setVisible(false)); expect(container.textContent).toEqual(''); act(() => setVisible(true)); expect(container.textContent).toEqual('1337'); }); testRecoil('Components unsubscribe from atoms when rendered without using them', () => { const atomA = counterAtom(); const atomB = counterAtom(); const [WriteA, updateValueA] = componentThatWritesAtom(atomA); const [WriteB, updateValueB] = componentThatWritesAtom(atomB); const Component = (jest.fn(function Read({ atom }) { const [value] = useRecoilState(atom); return value; }): any); let toggleSwitch; declare var Switch: () => any; const container = renderElements(<> <Switch /> <WriteA /> <WriteB /> </>); let baseCalls = BASE_CALLS; expect(container.textContent).toEqual('0'); expect(Component).toHaveBeenCalledTimes(baseCalls + 1); act(() => updateValueA(1)); expect(container.textContent).toEqual('1'); expect(Component).toHaveBeenCalledTimes(baseCalls + 2); if (!mutableSourceExists()) { baseCalls += 1; } act(() => toggleSwitch()); expect(container.textContent).toEqual('0'); expect(Component).toHaveBeenCalledTimes(baseCalls + 3); // Now update the atom that it used to be subscribed to but should be no longer: act(() => updateValueA(2)); expect(container.textContent).toEqual('0'); expect(Component).toHaveBeenCalledTimes(baseCalls + 3); // Important part: same as before // It is subscribed to the atom that it switched to: act(() => updateValueB(3)); expect(container.textContent).toEqual('3'); expect(Component).toHaveBeenCalledTimes(baseCalls + 4); }); testRecoil('Selectors unsubscribe from upstream when they have no subscribers', () => { const atomA = counterAtom(); const atomB = counterAtom(); const [WriteA, updateValueA] = componentThatWritesAtom(atomA); // Do two layers of selectors to test that the unsubscribing is recursive: const selectorMapFn1 = jest.fn(x => x); const sel1 = selector({ key: 'selUpstream', get: ({ get }) => selectorMapFn1(get(atomA)) }); const selectorMapFn2 = jest.fn(x => x); const sel2 = selector({ key: 'selDownstream', get: ({ get }) => selectorMapFn2(get(sel1)) }); let toggleSwitch; declare var Switch: () => any; const container = renderElements(<> <Switch /> <WriteA /> </>); expect(container.textContent).toEqual('0'); expect(selectorMapFn1).toHaveBeenCalledTimes(1); expect(selectorMapFn2).toHaveBeenCalledTimes(1); act(() => updateValueA(1)); expect(container.textContent).toEqual('1'); expect(selectorMapFn1).toHaveBeenCalledTimes(2); expect(selectorMapFn2).toHaveBeenCalledTimes(2); act(() => toggleSwitch()); expect(container.textContent).toEqual('0'); expect(selectorMapFn1).toHaveBeenCalledTimes(2); expect(selectorMapFn2).toHaveBeenCalledTimes(2); act(() => updateValueA(2)); expect(container.textContent).toEqual('0'); expect(selectorMapFn1).toHaveBeenCalledTimes(2); expect(selectorMapFn2).toHaveBeenCalledTimes(2); }); testRecoil('Unsubscribes happen in case of unmounting of a suspended component', () => { const anAtom = counterAtom(); const [aSelector, _selFn] = plusOneSelector(anAtom); const [_asyncSel, _adjustTimeout] = plusOneAsyncSelector(aSelector); // FIXME to implement }); testRecoil('Selectors stay up to date if deps are changed while they have no subscribers', () => { const anAtom = counterAtom(); const [aSelector, _] = plusOneSelector(anAtom); let setVisible; declare function Switch(arg0: any): any; const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <Switch> <ReadsAtom atom={aSelector} /> </Switch> </>); act(() => updateValue(1)); expect(container.textContent).toEqual('2'); act(() => setVisible(false)); expect(container.textContent).toEqual(''); act(() => updateValue(2)); expect(container.textContent).toEqual(''); act(() => setVisible(true)); expect(container.textContent).toEqual('3'); }); testRecoil('Selectors can be invertible', () => { const anAtom = counterAtom(); const aSelector = selector({ key: 'invertible1', get: ({ get }) => get(anAtom), set: ({ set }, newValue) => set(anAtom, newValue) }); const [Component, updateValue] = componentThatWritesAtom(aSelector); const container = renderElements(<> <Component /> <ReadsAtom atom={anAtom} /> </>); expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('1'); }); testRecoil('Selector dependencies can change over time', () => { const atomA = counterAtom(); const atomB = counterAtom(); const aSelector = selector({ key: 'depsChange', get: ({ get }) => { const a = get(atomA); if (a === 1337) { const b = get(atomB); return b; } else { return a; } } }); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const container = renderElements(<> <ComponentA /> <ComponentB /> <ReadsAtom atom={aSelector} /> </>); expect(container.textContent).toEqual('0'); act(() => updateValueA(1337)); expect(container.textContent).toEqual('0'); act(() => updateValueB(1)); expect(container.textContent).toEqual('1'); act(() => updateValueA(2)); expect(container.textContent).toEqual('2'); }); testRecoil('Selectors can gain and lose depnedencies', () => { const switchAtom = booleanAtom(); const inputAtom = counterAtom(); // Depends on inputAtom only when switchAtom is true: const aSelector = selector<number>({ key: 'gainsDeps', get: ({ get }) => { if (get(switchAtom)) { return get(inputAtom); } else { return Infinity; } } }); const [ComponentA, setSwitch] = componentThatWritesAtom(switchAtom); const [ComponentB, setInput] = componentThatWritesAtom(inputAtom); const [ComponentC, commit] = componentThatReadsAtomWithCommitCount(aSelector); const container = renderElements(<> <ComponentA /> <ComponentB /> <ComponentC /> </>); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Input is not a dep yet, so this has no effect: act(() => setInput(1)); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Flip switch: act(() => setSwitch(true)); expect(container.textContent).toEqual('1'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); // Now changing input causes a re-render: act(() => setInput(2)); expect(container.textContent).toEqual('2'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 3); // Now that we've added the dep, we can remove it... act(() => setSwitch(false)); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 4); // ... and again changing input will not cause a re-render: act(() => setInput(3)); expect(container.textContent).toEqual('Infinity'); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 4); }); testRecoil('Selector depedencies are updated transactionally', () => { const atomA = counterAtom(); const atomB = counterAtom(); const atomC = counterAtom(); const [observedSelector, selectorFn] = plusOneSelector(atomC); const aSelector = selector({ key: 'transactionally', get: ({ get }) => { const a = get(atomA); const b = get(atomB); return a !== 0 && b === 0 ? get(observedSelector) // We want to test this never happens : null; } }); const [ComponentA, updateValueA] = componentThatWritesAtom(atomA); const [ComponentB, updateValueB] = componentThatWritesAtom(atomB); const [ComponentC, updateValueC] = componentThatWritesAtom(atomC); renderElements(<> <ComponentA /> <ComponentB /> <ComponentC /> <ReadsAtom atom={aSelector} /> </>); act(() => { batchUpdates(() => { updateValueA(1); updateValueB(1); }); }); // observedSelector wasn't evaluated: expect(selectorFn).toHaveBeenCalledTimes(0); // nor were any subscriptions created for it: act(() => { updateValueC(1); }); expect(selectorFn).toHaveBeenCalledTimes(0); }); testRecoil('Can set an atom during rendering', () => { const anAtom = counterAtom(); declare function SetsDuringRendering(): any; const container = renderElements(<> <SetsDuringRendering /> <ReadsAtom atom={anAtom} /> </>); expect(container.textContent).toEqual('1'); }); testRecoil('Does not re-create "setter" function after setting a value', () => { const anAtom = counterAtom(); const anotherAtom = counterAtom(); let useRecoilStateCounter = 0; let useRecoilStateErrorStatesCounter = 0; let useTwoAtomsCounter = 0; declare function Component1(): any; declare function Component2(): any; declare function Component3(): any; renderElements(<> <Component1 /> <Component2 /> <Component3 /> </>); expect(useRecoilStateCounter).toBe(1); expect(useRecoilStateErrorStatesCounter).toBe(1); expect(useTwoAtomsCounter).toBe(2); }); testRecoil('Can set atom during post-atom-setting effect (NOT during initial render)', async () => { const anAtom = counterAtom(); let done = false; declare function SetsDuringEffect(): any; const [Comp] = componentThatWritesAtom(anAtom); const container = renderElements(<> <SetsDuringEffect /> <ReadsAtom atom={anAtom} /> <Comp /> </>); act(() => undefined); await flushPromisesAndTimers(); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('1'); }); testRecoil('Can set atom during post-atom-setting effect regardless of effect order', async () => { declare function testWithOrder(order: any): any; testWithOrder(['SetsDuringEffect', 'Batcher']); testWithOrder(['Batcher', 'SetsDuringEffect']); }); testRecoil('Basic async selector test', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={aSelector} /> </>); // Begins in loading state, then shows initial value: expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(container.textContent).toEqual('1'); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('2'); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); expect(container.textContent).toEqual('1'); }); testRecoil('Ability to not use Suspense', () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); declare function ReadsAtomWithoutSuspense(arg0: any): any; const container = renderElements(<> <Component /> <ReadsAtomWithoutSuspense atom={aSelector} /> </>); // Begins in loading state, then shows initial value: expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('1'); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('2'); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); expect(container.textContent).toEqual('1'); }); testRecoil('Ability to not use Suspense - with value instead of loadable', () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); declare function ReadsAtomWithoutSuspense(arg0: any): any; const container = renderElements(<> <Component /> <ReadsAtomWithoutSuspense atom={aSelector} /> </>); // Begins in loading state, then shows initial value: expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('1'); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); expect(container.textContent).toEqual('loading not with suspense'); act(() => jest.runAllTimers()); expect(container.textContent).toEqual('2'); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); expect(container.textContent).toEqual('1'); }); testRecoil('Selector can alternate between synchronous and asynchronous', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const aSelector = selector({ key: 'alternatingSelector', get: ({ get }) => { const x = get(anAtom); if (x === 1337) { return new Promise(() => {}); } if (x % 2 === 0) { return x; } else { return new Promise(resolve => { setTimeout(() => resolve(x), 100); }); } } }); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={aSelector} /> </>); // Transition from sync to async: expect(container.textContent).toEqual('0'); act(() => updateValue(1)); expect(container.textContent).toEqual('loading'); advanceTimersBy(101); expect(container.textContent).toEqual('1'); // Transition from async to sync (with async being in hasValue state): act(() => updateValue(2)); expect(container.textContent).toEqual('2'); // Transition from async to sync (with async being in loading state): act(() => updateValue(1337)); expect(container.textContent).toEqual('loading'); act(() => updateValue(4)); await flushPromisesAndTimers(); expect(container.textContent).toEqual('4'); // Transition from sync to async with still unresolved promise from before: act(() => updateValue(5)); expect(container.textContent).toEqual('loading'); advanceTimersBy(101); await flushPromisesAndTimers(); expect(container.textContent).toEqual('5'); }); testRecoil('Async selectors do not re-query when re-subscribed from having no subscribers', async () => { const anAtom = counterAtom(); const [sel, resolvers] = asyncSelectorThatPushesPromisesOntoArray(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const [Toggle, toggle] = componentThatToggles(<ReadsAtom atom={sel} />, null); const container = renderElements(<> <Component /> <Toggle /> </>); expect(container.textContent).toEqual('loading'); expect(resolvers.length).toBe(1); act(() => updateValue(2)); await flushPromisesAndTimers(); expect(resolvers.length).toBe(2); resolvers[1][0]('hello'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('"hello"'); // Cause sel to have no subscribers: act(() => toggle.current()); expect(container.textContent).toEqual(''); // Once it's used again, it should not issue another request: act(() => toggle.current()); expect(resolvers.length).toBe(2); expect(container.textContent).toEqual('"hello"'); }); testRecoil('Selector subscriptions are correct when a selector is unsubscribed the second time', async () => { // This regression test would fail by an exception being thrown because subscription refcounts // would would fall below zero. const anAtom = counterAtom(); const [sel, _] = plusOneSelector(anAtom); const [Toggle, toggle] = componentThatToggles(<ReadsAtom atom={sel} />, null); const container = renderElements(<> <Toggle /> </>); expect(container.textContent).toEqual('1'); act(() => toggle.current()); expect(container.textContent).toEqual(''); act(() => toggle.current()); expect(container.textContent).toEqual('1'); act(() => toggle.current()); expect(container.textContent).toEqual(''); }); testRecoil('Can move out of suspense by changing deps', async () => { const anAtom = counterAtom(); const [aSelector, resolvers] = asyncSelectorThatPushesPromisesOntoArray(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const container = renderElements(<> <Component /> <ReadsAtom atom={aSelector} /> </>); // While still waiting for first request, let a second faster request happen: expect(container.textContent).toEqual('loading'); expect(resolvers.length).toEqual(1); act(() => updateValue(1)); await flushPromisesAndTimers(); expect(resolvers.length).toEqual(2); expect(container.textContent).toEqual('loading'); // When the faster second request resolves, we should see its result: resolvers[1][0]('hello'); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('"hello"'); }); testRecoil('Can use an already-resolved promise', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [Component, updateValue] = componentThatWritesAtom(anAtom); const sel = selector({ key: `selector${nextID++}`, get: ({ get }) => { const x = get(anAtom); return Promise.resolve(x + 1); } }); const container = renderElements(<> <Component /> <ReadsAtom atom={sel} /> </>); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('1'); act(() => updateValue(1)); await flushPromisesAndTimers(); await flushPromisesAndTimers(); expect(container.textContent).toEqual('2'); }); testRecoil('Resolution of suspense causes render just once', async () => { jest.useFakeTimers(); const anAtom = counterAtom(); const [aSelector, _] = plusOneAsyncSelector(anAtom); const [Component, updateValue] = componentThatWritesAtom(anAtom); const [ReadComp, commit] = componentThatReadsAtomWithCommitCount(aSelector); const [__, suspense] = renderElementsWithSuspenseCount(<> <Component /> <ReadComp /> </>); // Begins in loading state, then shows initial value: act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(suspense).toHaveBeenCalledTimes(1); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 1); // Changing dependency makes it go back to loading, then to show new value: act(() => updateValue(1)); act(() => jest.runAllTimers()); await flushPromisesAndTimers(); expect(suspense).toHaveBeenCalledTimes(2); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 2); // Returning to a seen value does not cause the loading state: act(() => updateValue(0)); await flushPromisesAndTimers(); expect(suspense).toHaveBeenCalledTimes(2); expect(commit).toHaveBeenCalledTimes(BASE_CALLS + 3); }); testRecoil('useTransactionObservation_DEPRECATED: Transaction dirty atoms are set', async () => { const anAtom = counterAtom({ type: 'url', validator: x => (x: any) }); const [aSelector, _] = plusOneSelector(anAtom); const [anAsyncSelector, __] = plusOneAsyncSelector(aSelector); const [Component, updateValue] = componentThatWritesAtom(anAtom); const modifiedAtomsList = []; renderElements(<> <Component /> <ReadsAtom atom={aSelector} /> <React.Suspense fallback="loading"> <ReadsAtom atom={anAsyncSelector} /> </React.Suspense> <ObservesTransactions fn={({ modifiedAtoms }) => { modifiedAtomsList.push(modifiedAtoms); }} /> </>); await flushPromisesAndTimers(); await flushPromisesAndTimers(); act(() => updateValue(1)); await flushPromisesAndTimers(); expect(modifiedAtomsList.length).toBe(3); expect(modifiedAtomsList[1].size).toBe(1); expect(modifiedAtomsList[1].has(anAtom.key)).toBe(true); for (const modifiedAtoms of modifiedAtomsList) { expect(modifiedAtoms.has(aSelector.key)).toBe(false); expect(modifiedAtoms.has(anAsyncSelector.key)).toBe(false); } }); testRecoil('Can restore persisted values before atom def code is loaded', () => { let theAtom = null; let setUnvalidatedAtomValues; declare function SetsUnvalidatedAtomValues(): any; let setVisible; declare function Switch(arg0: any): any; declare function MyReadsAtom(arg0: any): any; const container = renderElements(<> <SetsUnvalidatedAtomValues /> <Switch> <MyReadsAtom getAtom={() => theAtom} /> </Switch> </>); act(() => { setUnvalidatedAtomValues(new Map().set('notDefinedYetAtom', 123)); }); const validator = jest.fn(() => 789); theAtom = atom({ key: 'notDefinedYetAtom', default: 456, persistence_UNSTABLE: { type: 'url', validator } }); act(() => { setVisible(true); }); expect(validator.mock.calls[0][0]).toBe(123); expect(container.textContent).toBe('789'); }); testRecoil('useTransactionObservation_DEPRECATED: Nonvalidated atoms are included in transaction observation', () => { const anAtom = counterAtom({ type: 'url', validator: x => (x: any) }); const [Component, updateValue] = componentThatWritesAtom(anAtom); let setUnvalidatedAtomValues; declare function SetsUnvalidatedAtomValues(): any; let values = new Map(); renderElements(<> <Component /> <SetsUnvalidatedAtomValues /> <ObservesTransactions fn={({ atomValues }) => { values = atomValues; }} /> </>); act(() => { setUnvalidatedAtomValues(new Map().set('someNonvalidatedAtom', 123)); }); values = new Map(); act(() => updateValue(1)); expect(values.size).toBe(2); expect(values.get('someNonvalidatedAtom')).toBe(123); });