Version 1.0.2 mit node_modules Verzeichnis

This commit is contained in:
ISA
2024-10-02 07:58:24 +02:00
parent f353a06b1b
commit 62b6e55a0a
68228 changed files with 4548477 additions and 651 deletions

21
node_modules/recoil/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Meta Platforms, Inc. and affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
node_modules/recoil/README.md generated vendored Normal file
View File

@@ -0,0 +1,49 @@
# Recoil · [![NPM Version](https://img.shields.io/npm/v/recoil)](https://www.npmjs.com/package/recoil) [![Node.js CI](https://github.com/facebookexperimental/Recoil/workflows/Node.js%20CI/badge.svg)](https://github.com/facebookexperimental/Recoil/actions) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebookexperimental/Recoil/blob/main/LICENSE) [![Follow on Twitter](https://img.shields.io/twitter/follow/recoiljs?label=Follow%20Recoil&style=social)](https://twitter.com/recoiljs)
Recoil is an experimental state management framework for React.
Website: https://recoiljs.org
## Documentation
Documentation: https://recoiljs.org/docs/introduction/core-concepts
API Reference: https://recoiljs.org/docs/api-reference/core/RecoilRoot
Tutorials: https://recoiljs.org/resources
## Installation
The Recoil package lives in [npm](https://www.npmjs.com/get-npm). Please see the [installation guide](https://recoiljs.org/docs/introduction/installation)
To install the latest stable version, run the following command:
```shell
npm install recoil
```
Or if you're using [yarn](https://classic.yarnpkg.com/en/docs/install/):
```shell
yarn add recoil
```
Or if you're using [bower](https://bower.io/#install-bower):
```shell
bower install --save recoil
```
## Contributing
Development of Recoil happens in the open on GitHub, and we are grateful to the community for contributing bugfixes and improvements. Read below to learn how you can take part in improving Recoil.
- [Code of Conduct](./CODE_OF_CONDUCT.md)
- [Contributing Guide](./CONTRIBUTING.md)
### License
Recoil is [MIT licensed](./LICENSE).

View File

@@ -0,0 +1,110 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Implements (a subset of) the interface of built-in Map but supports arrays as
* keys. Two keys are equal if corresponding elements are equal according to the
* equality semantics of built-in Map. Operations are at worst O(n*b) where n is
* the array length and b is the complexity of the built-in operation.
*
* @flow
* @format
* @oncall recoil
*/
'use strict';
const LEAF = {};
const emptyMap = new Map<mixed, mixed>();
class ArrayKeyedMap<V> {
_base: Map<any, any> = new Map();
constructor(
existing?: ArrayKeyedMap<V> | Iterable<[mixed, V]>,
// $FlowFixMe[incompatible-return]
): ArrayKeyedMap<V> {
if (existing instanceof ArrayKeyedMap) {
for (const [k, v] of existing.entries()) {
this.set(k, v);
}
} else if (existing) {
for (const [k, v] of existing) {
this.set(k, v);
}
}
return this;
}
get(key: mixed): V | void {
const ks = Array.isArray(key) ? key : [key];
let map = this._base;
ks.forEach(k => {
map = map.get(k) ?? emptyMap;
});
return map === undefined ? undefined : map.get(LEAF);
}
set(key: mixed, value: V): any {
const ks = Array.isArray(key) ? key : [key];
let map: ?(any | Map<mixed, mixed> | Map<any, any>) = this._base;
let next: ?(any | Map<mixed, mixed> | Map<any, any>) = map;
ks.forEach(k => {
// $FlowFixMe[incompatible-use]
next = map.get(k);
if (!next) {
next = new Map();
// $FlowFixMe[incompatible-use]
map.set(k, next);
}
map = next;
});
// $FlowFixMe[incompatible-use]
next.set(LEAF, value);
return this;
}
delete(key: mixed): any {
const ks = Array.isArray(key) ? key : [key];
let map: ?(any | Map<mixed, mixed> | Map<any, any>) = this._base;
let next: ?(any | Map<mixed, mixed> | Map<any, any>) = map;
ks.forEach(k => {
// $FlowFixMe[incompatible-use]
next = map.get(k);
if (!next) {
next = new Map();
// $FlowFixMe[incompatible-use]
map.set(k, next);
}
map = next;
});
// $FlowFixMe[incompatible-use]
next.delete(LEAF);
// TODO We could cleanup empty maps
return this;
}
entries(): Iterator<[$ReadOnlyArray<mixed>, V]> {
const answer = [];
function recurse(level: any | Map<any, any>, prefix: Array<mixed>) {
level.forEach((v, k) => {
if (k === LEAF) {
answer.push([prefix, v]);
} else {
recurse(v, prefix.concat(k));
}
});
}
recurse(this._base, []);
return answer.values();
}
toBuiltInMap(): Map<$ReadOnlyArray<mixed>, V> {
return new Map(this.entries());
}
}
module.exports = {ArrayKeyedMap};

313
node_modules/recoil/cjs/adt/Recoil_Loadable.js.flow generated vendored Normal file
View File

@@ -0,0 +1,313 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* A type that represents a value that may or may not be loaded. It differs from
* LoadObject in that the `loading` state has a Promise that is meant to resolve
* when the value is available (but as with LoadObject, an individual Loadable
* is a value type and is not mutated when the status of a request changes).
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
const err = require('recoil-shared/util/Recoil_err');
const isPromise = require('recoil-shared/util/Recoil_isPromise');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
class BaseLoadable<T> {
getValue(): T {
throw err('BaseLoadable');
}
toPromise(): Promise<T> {
throw err('BaseLoadable');
}
valueMaybe(): T | void {
throw err('BaseLoadable');
}
valueOrThrow(): T {
// $FlowFixMe[prop-missing]
throw err(`Loadable expected value, but in "${this.state}" state`);
}
promiseMaybe(): Promise<T> | void {
throw err('BaseLoadable');
}
promiseOrThrow(): Promise<T> {
// $FlowFixMe[prop-missing]
throw err(`Loadable expected promise, but in "${this.state}" state`);
}
errorMaybe(): mixed | void {
throw err('BaseLoadable');
}
errorOrThrow(): mixed {
// $FlowFixMe[prop-missing]
throw err(`Loadable expected error, but in "${this.state}" state`);
}
is(other: Loadable<mixed>): boolean {
// $FlowFixMe[prop-missing]
return other.state === this.state && other.contents === this.contents;
}
map<S>(_map: T => Promise<S> | Loadable<S> | S): Loadable<S> {
throw err('BaseLoadable');
}
}
class ValueLoadable<T> extends BaseLoadable<T> {
state: 'hasValue' = 'hasValue';
contents: T;
constructor(value: T) {
super();
this.contents = value;
}
getValue(): T {
return this.contents;
}
toPromise(): Promise<T> {
return Promise.resolve(this.contents);
}
valueMaybe(): T {
return this.contents;
}
valueOrThrow(): T {
return this.contents;
}
promiseMaybe(): void {
return undefined;
}
errorMaybe(): void {
return undefined;
}
map<S>(map: T => Promise<S> | Loadable<S> | S): Loadable<S> {
try {
const next = map(this.contents);
return isPromise(next)
? loadableWithPromise(next)
: isLoadable(next)
? next
: loadableWithValue(next);
} catch (e) {
return isPromise(e)
? // If we "suspended", then try again.
// errors and subsequent retries will be handled in 'loading' case
// $FlowFixMe[prop-missing]
loadableWithPromise(e.next(() => this.map(map)))
: loadableWithError(e);
}
}
}
class ErrorLoadable<T> extends BaseLoadable<T> {
state: 'hasError' = 'hasError';
contents: mixed;
constructor(error: mixed) {
super();
this.contents = error;
}
getValue(): T {
throw this.contents;
}
toPromise(): Promise<T> {
return Promise.reject(this.contents);
}
valueMaybe(): void {
return undefined;
}
promiseMaybe(): void {
return undefined;
}
errorMaybe(): mixed {
return this.contents;
}
errorOrThrow(): mixed {
return this.contents;
}
map<S>(_map: T => Promise<S> | Loadable<S> | S): $ReadOnly<ErrorLoadable<S>> {
// $FlowIssue[incompatible-return]
return this;
}
}
class LoadingLoadable<T> extends BaseLoadable<T> {
state: 'loading' = 'loading';
contents: Promise<T>;
constructor(promise: Promise<T>) {
super();
this.contents = promise;
}
getValue(): T {
throw this.contents;
}
toPromise(): Promise<T> {
return this.contents;
}
valueMaybe(): void {
return undefined;
}
promiseMaybe(): Promise<T> {
return this.contents;
}
promiseOrThrow(): Promise<T> {
return this.contents;
}
errorMaybe(): void {
return undefined;
}
map<S>(
map: T => Promise<S> | Loadable<S> | S,
): $ReadOnly<LoadingLoadable<S>> {
return loadableWithPromise(
this.contents
.then(value => {
const next = map(value);
if (isLoadable(next)) {
const nextLoadable: Loadable<S> = next;
switch (nextLoadable.state) {
case 'hasValue':
return nextLoadable.contents;
case 'hasError':
throw nextLoadable.contents;
case 'loading':
return nextLoadable.contents;
}
}
// $FlowIssue[incompatible-return]
return next;
})
// $FlowFixMe[incompatible-call]
.catch(e => {
if (isPromise(e)) {
// we were "suspended," try again
return e.then(() => this.map(map).contents);
}
throw e;
}),
);
}
}
export type Loadable<+T> =
| $ReadOnly<ValueLoadable<T>>
| $ReadOnly<ErrorLoadable<T>>
| $ReadOnly<LoadingLoadable<T>>;
export type ValueLoadableType<+T> = $ReadOnly<ValueLoadable<T>>;
export type ErrorLoadableType<+T> = $ReadOnly<ErrorLoadable<T>>;
export type LoadingLoadableType<+T> = $ReadOnly<LoadingLoadable<T>>;
function loadableWithValue<+T>(value: T): $ReadOnly<ValueLoadable<T>> {
return Object.freeze(new ValueLoadable(value));
}
function loadableWithError<+T>(error: mixed): $ReadOnly<ErrorLoadable<T>> {
return Object.freeze(new ErrorLoadable(error));
}
function loadableWithPromise<+T>(
promise: Promise<T>,
): $ReadOnly<LoadingLoadable<T>> {
return Object.freeze(new LoadingLoadable(promise));
}
function loadableLoading<+T>(): $ReadOnly<LoadingLoadable<T>> {
return Object.freeze(new LoadingLoadable(new Promise(() => {})));
}
type UnwrapLoadables<Loadables> = $TupleMap<Loadables, <T>(Loadable<T>) => T>;
type LoadableAllOfTuple = <
Tuple: $ReadOnlyArray<Loadable<mixed> | Promise<mixed> | mixed>,
>(
tuple: Tuple,
) => Loadable<$TupleMap<Tuple, <V>(Loadable<V> | Promise<V> | V) => V>>;
type LoadableAllOfObj = <
Obj: $ReadOnly<{[string]: Loadable<mixed> | Promise<mixed> | mixed, ...}>,
>(
obj: Obj,
) => Loadable<$ObjMap<Obj, <V>(Loadable<V> | Promise<V> | V) => V>>;
type LoadableAll = LoadableAllOfTuple & LoadableAllOfObj;
function loadableAllArray<Inputs: $ReadOnlyArray<Loadable<mixed>>>(
inputs: Inputs,
): Loadable<UnwrapLoadables<Inputs>> {
return inputs.every(i => i.state === 'hasValue')
? loadableWithValue(inputs.map(i => i.contents))
: inputs.some(i => i.state === 'hasError')
? loadableWithError(
nullthrows(
inputs.find(i => i.state === 'hasError'),
'Invalid loadable passed to loadableAll',
).contents,
)
: loadableWithPromise(Promise.all(inputs.map(i => i.contents)));
}
function loadableAll<
Inputs:
| $ReadOnlyArray<Loadable<mixed> | Promise<mixed> | mixed>
| $ReadOnly<{[string]: Loadable<mixed> | Promise<mixed> | mixed, ...}>,
>(
inputs: Inputs,
): Loadable<$ReadOnlyArray<mixed> | $ReadOnly<{[string]: mixed, ...}>> {
const unwrapedInputs = Array.isArray(inputs)
? inputs
: Object.getOwnPropertyNames(inputs).map(key => inputs[key]);
const normalizedInputs = unwrapedInputs.map(x =>
isLoadable(x)
? x
: isPromise(x)
? loadableWithPromise(x)
: loadableWithValue(x),
);
const output = loadableAllArray(normalizedInputs);
return Array.isArray(inputs)
? // $FlowIssue[incompatible-return]
output
: // Object.getOwnPropertyNames() has consistent key ordering with ES6
// $FlowIssue[incompatible-call]
output.map(outputs =>
Object.getOwnPropertyNames(inputs).reduce(
// $FlowFixMe[invalid-computed-prop]
(out, key, idx) => ({...out, [key]: outputs[idx]}),
{},
),
);
}
function isLoadable(x: mixed): boolean %checks {
return x instanceof BaseLoadable;
}
const LoadableStaticInterface = {
of: <T>(value: Promise<T> | Loadable<T> | T): Loadable<T> =>
isPromise(value)
? loadableWithPromise(value)
: isLoadable(value)
? value
: loadableWithValue(value),
error: <T>(error: mixed): $ReadOnly<ErrorLoadable<T>> =>
loadableWithError(error),
// $FlowIssue[incompatible-return]
loading: <T>(): LoadingLoadable<T> => loadableLoading<T>(),
// $FlowIssue[unclear-type]
all: ((loadableAll: any): LoadableAll),
isLoadable,
};
module.exports = {
loadableWithValue,
loadableWithError,
loadableWithPromise,
loadableLoading,
loadableAll,
isLoadable,
RecoilLoadable: LoadableStaticInterface,
};

View File

@@ -0,0 +1,142 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {HAMTPlusMap} from 'hamt_plus';
const hamt = require('hamt_plus');
const gkx = require('recoil-shared/util/Recoil_gkx');
export interface PersistentMap<K: string, V> {
keys(): Iterable<K>;
entries(): Iterable<[K, V]>;
get(key: K): V | void;
has(key: K): boolean;
set(key: K, value: V): PersistentMap<K, V>;
delete(key: K): PersistentMap<K, V>;
clone(): PersistentMap<K, V>;
toMap(): Map<K, V>;
}
class BuiltInMap<K: string, V> implements PersistentMap<K, V> {
_map: Map<K, V>;
constructor(existing?: PersistentMap<K, V>) {
this._map = new Map(existing?.entries());
}
keys(): Iterable<K> {
return this._map.keys();
}
entries(): Iterable<[K, V]> {
return this._map.entries();
}
get(k: K): V | void {
return this._map.get(k);
}
has(k: K): boolean {
return this._map.has(k);
}
set(k: K, v: V): PersistentMap<K, V> {
this._map.set(k, v);
return this;
}
delete(k: K): PersistentMap<K, V> {
this._map.delete(k);
return this;
}
clone(): PersistentMap<K, V> {
return persistentMap(this);
}
toMap(): Map<K, V> {
return new Map(this._map);
}
}
class HashArrayMappedTrieMap<K: string, V> implements PersistentMap<K, V> {
// Because hamt.empty is not a function there is no way to introduce type
// parameters on it, so empty is typed as HAMTPlusMap<string, mixed>.
// $FlowIssue
_hamt: HAMTPlusMap<K, V> = ((hamt.empty: any).beginMutation(): HAMTPlusMap<
K,
V,
>);
constructor(existing?: PersistentMap<K, V>) {
if (existing instanceof HashArrayMappedTrieMap) {
const h = existing._hamt.endMutation();
existing._hamt = h.beginMutation();
this._hamt = h.beginMutation();
} else if (existing) {
for (const [k, v] of existing.entries()) {
this._hamt.set(k, v);
}
}
}
keys(): Iterable<K> {
return this._hamt.keys();
}
entries(): Iterable<[K, V]> {
return this._hamt.entries();
}
get(k: K): V | void {
return this._hamt.get(k);
}
has(k: K): boolean {
return this._hamt.has(k);
}
set(k: K, v: V): PersistentMap<K, V> {
this._hamt.set(k, v);
return this;
}
delete(k: K): PersistentMap<K, V> {
this._hamt.delete(k);
return this;
}
clone(): PersistentMap<K, V> {
return persistentMap(this);
}
toMap(): Map<K, V> {
return new Map(this._hamt);
}
}
function persistentMap<K: string, V>(
existing?: PersistentMap<K, V>,
): PersistentMap<K, V> {
if (gkx('recoil_hamt_2020')) {
return new HashArrayMappedTrieMap(existing);
} else {
return new BuiltInMap(existing);
}
}
module.exports = {
persistentMap,
};

20
node_modules/recoil/cjs/adt/Recoil_Queue.js.flow generated vendored Normal file
View File

@@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
function enqueueExecution(s: string, f: () => mixed) {
f();
}
module.exports = {
enqueueExecution,
};

22
node_modules/recoil/cjs/adt/Recoil_Wrapper.js.flow generated vendored Normal file
View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
class WrappedValue<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
module.exports = {
WrappedValue,
};

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {ArrayKeyedMap} = require('../Recoil_ArrayKeyedMap');
test('basic operation', () => {
const m = new ArrayKeyedMap<number>();
m.set([], 0);
m.set(['a'], 1);
m.set(['a', 'b'], 2);
expect(m.get([])).toBe(0);
expect(m.get(['a'])).toBe(1);
expect(m.get(['a', 'b'])).toBe(2);
});
test('enumeration of properties', () => {
const m = new ArrayKeyedMap<number>();
m.set([], 0);
m.set(['a'], 1);
m.set(['a', 'b'], 2);
const entries = Array.from(m.entries());
expect(entries[0][0]).toEqual([]);
expect(entries[0][1]).toBe(0);
expect(entries[1][0]).toEqual(['a']);
expect(entries[1][1]).toBe(1);
expect(entries[2][0]).toEqual(['a', 'b']);
expect(entries[2][1]).toBe(2);
});
test('copying', () => {
const m = new ArrayKeyedMap<number | $FlowFixMe>();
m.set([], 0);
m.set(['a'], 1);
m.set(['a', 'b'], 2);
const mm = new ArrayKeyedMap(m);
expect(mm.get([])).toBe(0);
expect(mm.get(['a'])).toBe(1);
expect(mm.get(['a', 'b'])).toBe(2);
expect(Array.from(m.entries())).toEqual(Array.from(mm.entries()));
});

View File

@@ -0,0 +1,281 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
RecoilLoadable,
loadableWithError,
loadableWithPromise,
loadableWithValue,
} = require('../Recoil_Loadable');
const ERROR = new Error('ERROR');
test('Value Loadable', async () => {
const loadable = loadableWithValue('VALUE');
expect(loadable.state).toBe('hasValue');
expect(loadable.contents).toBe('VALUE');
expect(loadable.getValue()).toBe('VALUE');
await expect(loadable.toPromise()).resolves.toBe('VALUE');
expect(loadable.valueMaybe()).toBe('VALUE');
expect(loadable.valueOrThrow()).toBe('VALUE');
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow();
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow();
});
test('Error Loadable', async () => {
const loadable = loadableWithError<$FlowFixMe>(ERROR);
expect(loadable.state).toBe('hasError');
expect(loadable.contents).toBe(ERROR);
expect(() => loadable.getValue()).toThrow(ERROR);
await expect(loadable.toPromise()).rejects.toBe(ERROR);
expect(loadable.valueMaybe()).toBe(undefined);
expect(() => loadable.valueOrThrow()).toThrow();
expect(loadable.errorMaybe()).toBe(ERROR);
expect(loadable.errorOrThrow()).toBe(ERROR);
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow();
});
test('Pending Value Loadable', async () => {
const promise = Promise.resolve('VALUE');
const loadable = loadableWithPromise(promise);
expect(loadable.state).toBe('loading');
expect(loadable.contents).toBe(promise);
expect(() => loadable.getValue()).toThrow();
await expect(loadable.toPromise()).resolves.toBe('VALUE');
expect(loadable.valueMaybe()).toBe(undefined);
expect(() => loadable.valueOrThrow()).toThrow();
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow();
await expect(loadable.promiseMaybe()).resolves.toBe('VALUE');
await expect(loadable.promiseOrThrow()).resolves.toBe('VALUE');
});
describe('Loadable mapping', () => {
test('Loadable mapping value', () => {
const loadable = loadableWithValue('VALUE').map(x => 'MAPPED ' + x);
expect(loadable.state).toBe('hasValue');
expect(loadable.contents).toBe('MAPPED VALUE');
});
test('Loadable mapping value to error', () => {
const loadable = loadableWithValue('VALUE').map<$FlowFixMe>(() => {
throw ERROR;
});
expect(loadable.state).toBe('hasError');
expect(loadable.contents).toBe(ERROR);
});
test('Loadable mapping value to Promise', async () => {
const loadable = loadableWithValue('VALUE').map(value =>
Promise.resolve('MAPPED ' + value),
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE');
});
test('Loadable mapping value to reject', async () => {
const loadable = loadableWithValue('VALUE').map(() =>
Promise.reject(ERROR),
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).rejects.toBe(ERROR);
});
test('Loadable mapping error', () => {
const loadable = loadableWithError<mixed>(ERROR).map(() => 'NOT_USED');
expect(loadable.state).toBe('hasError');
expect(loadable.contents).toBe(ERROR);
});
test('Loadable mapping promise value', async () => {
const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(
x => 'MAPPED ' + x,
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE');
});
test('Loadable mapping promise value to reject', async () => {
const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(() =>
Promise.reject(ERROR),
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).rejects.toBe(ERROR);
});
test('Loadable mapping promise value to error', async () => {
const loadable = loadableWithPromise(Promise.resolve('VALUE')).map<mixed>(
() => {
throw ERROR;
},
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).rejects.toBe(ERROR);
});
test('Loadable mapping promise error', async () => {
const loadable = loadableWithPromise(Promise.reject(ERROR)).map(
() => 'NOT_USED',
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).rejects.toBe(ERROR);
});
test('Loadable mapping to loadable', () => {
const loadable = loadableWithValue('VALUE').map(value =>
loadableWithValue(value),
);
expect(loadable.state).toBe('hasValue');
expect(loadable.contents).toBe('VALUE');
});
test('Loadable mapping promise to loadable value', async () => {
const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(value =>
loadableWithValue('MAPPED ' + value),
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE');
});
test('Loadable mapping promise to loadable error', async () => {
const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(() =>
// $FlowFixMe[underconstrained-implicit-instantiation]
loadableWithError(ERROR),
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).rejects.toBe(ERROR);
});
test('Loadable mapping promise to loadable promise', async () => {
const loadable = loadableWithPromise(Promise.resolve('VALUE')).map(value =>
loadableWithPromise(Promise.resolve('MAPPED ' + value)),
);
expect(loadable.state).toBe('loading');
await expect(loadable.toPromise()).resolves.toBe('MAPPED VALUE');
});
});
test('Loadable Factory Interface', async () => {
const valueLoadable = RecoilLoadable.of('VALUE');
expect(valueLoadable.state).toBe('hasValue');
expect(valueLoadable.contents).toBe('VALUE');
const valueLoadable2 = RecoilLoadable.of(RecoilLoadable.of('VALUE'));
expect(valueLoadable2.state).toBe('hasValue');
expect(valueLoadable2.contents).toBe('VALUE');
const promiseLoadable = RecoilLoadable.of(Promise.resolve('ASYNC'));
expect(promiseLoadable.state).toBe('loading');
await expect(promiseLoadable.contents).resolves.toBe('ASYNC');
const promiseLoadable2 = RecoilLoadable.of(
RecoilLoadable.of(Promise.resolve('ASYNC')),
);
expect(promiseLoadable2.state).toBe('loading');
await expect(promiseLoadable2.contents).resolves.toBe('ASYNC');
const errorLoadable = RecoilLoadable.error<mixed>('ERROR');
expect(errorLoadable.state).toBe('hasError');
expect(errorLoadable.contents).toBe('ERROR');
// $FlowFixMe[underconstrained-implicit-instantiation]
const errorLoadable2 = RecoilLoadable.of(RecoilLoadable.error('ERROR'));
expect(errorLoadable2.state).toBe('hasError');
expect(errorLoadable2.contents).toBe('ERROR');
const loadingLoadable = RecoilLoadable.loading<mixed>();
expect(loadingLoadable.state).toBe('loading');
});
describe('Loadable All', () => {
test('Array', async () => {
expect(
RecoilLoadable.all([RecoilLoadable.of('x'), RecoilLoadable.of(123)])
.contents,
).toEqual(['x', 123]);
await expect(
RecoilLoadable.all([
RecoilLoadable.of(Promise.resolve('x')),
RecoilLoadable.of(123),
]).contents,
).resolves.toEqual(['x', 123]);
expect(
RecoilLoadable.all([
RecoilLoadable.of('x'),
RecoilLoadable.of(123),
// $FlowFixMe[underconstrained-implicit-instantiation]
RecoilLoadable.error('ERROR'),
]).contents,
).toEqual('ERROR');
expect(
RecoilLoadable.all([
RecoilLoadable.of('x'),
RecoilLoadable.all([RecoilLoadable.of(1), RecoilLoadable.of(2)]),
]).contents,
).toEqual(['x', [1, 2]]);
});
test('Object', async () => {
expect(
RecoilLoadable.all({
str: RecoilLoadable.of('x'),
num: RecoilLoadable.of(123),
}).contents,
).toEqual({
str: 'x',
num: 123,
});
await expect(
RecoilLoadable.all({
str: RecoilLoadable.of(Promise.resolve('x')),
num: RecoilLoadable.of(123),
}).contents,
).resolves.toEqual({
str: 'x',
num: 123,
});
expect(
RecoilLoadable.all({
str: RecoilLoadable.of('x'),
num: RecoilLoadable.of(123),
// $FlowFixMe[underconstrained-implicit-instantiation]
err: RecoilLoadable.error('ERROR'),
}).contents,
).toEqual('ERROR');
});
test('mixed values', async () => {
expect(RecoilLoadable.all([RecoilLoadable.of('A'), 'B']).contents).toEqual([
'A',
'B',
]);
await expect(
RecoilLoadable.all([RecoilLoadable.of('A'), Promise.resolve('B')])
.contents,
).resolves.toEqual(['A', 'B']);
await expect(
RecoilLoadable.all([RecoilLoadable.of('A'), Promise.reject('B')])
.contents,
).rejects.toEqual('B');
await expect(
RecoilLoadable.all({
a: 'A',
b: RecoilLoadable.of('B'),
c: Promise.resolve('C'),
}).contents,
).resolves.toEqual({a: 'A', b: 'B', c: 'C'});
});
});

View File

@@ -0,0 +1,19 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
export interface CacheImplementation<K, V> {
get(K): ?V;
set(K, V): void;
delete(K): void;
clear(): void;
size(): number;
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
export type EqualityPolicy = 'reference' | 'value';
export type EvictionPolicy = 'lru' | 'keep-all' | 'most-recent';
export type CachePolicy =
| {eviction: 'lru', maxSize: number, equality?: EqualityPolicy}
| {eviction: 'keep-all', equality?: EqualityPolicy}
| {eviction: 'most-recent', equality?: EqualityPolicy}
| {equality: EqualityPolicy};
export type CachePolicyWithoutEviction = {equality: EqualityPolicy};

159
node_modules/recoil/cjs/caches/Recoil_LRUCache.js.flow generated vendored Normal file
View File

@@ -0,0 +1,159 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
type CacheNode<K, V> = {
key: K,
value: V,
left: ?CacheNode<K, V>,
right: ?CacheNode<K, V>,
};
type Options<K> = {
maxSize: number,
mapKey?: K => mixed,
};
class LRUCache<K = mixed, V = mixed> {
_maxSize: number;
_size: number;
_head: ?CacheNode<K, V>;
_tail: ?CacheNode<K, V>;
_map: Map<mixed, CacheNode<K, V>>;
_keyMapper: K => mixed;
constructor(options: Options<K>) {
this._maxSize = options.maxSize;
this._size = 0;
this._head = null;
this._tail = null;
this._map = new Map<mixed, CacheNode<K, V>>();
this._keyMapper = options.mapKey ?? (v => v);
}
head(): ?CacheNode<K, V> {
return this._head;
}
tail(): ?CacheNode<K, V> {
return this._tail;
}
size(): number {
return this._size;
}
maxSize(): number {
return this._maxSize;
}
has(key: K): boolean {
return this._map.has(this._keyMapper(key));
}
get(key: K): ?V {
const mappedKey = this._keyMapper(key);
const node = this._map.get(mappedKey);
if (!node) {
return undefined;
}
this.set(key, node.value);
return node.value;
}
set(key: K, val: V): void {
const mappedKey = this._keyMapper(key);
const existingNode = this._map.get(mappedKey);
if (existingNode) {
this.delete(key);
}
const head = this.head();
const node = {
key,
right: head,
left: null,
value: val,
};
if (head) {
head.left = node;
} else {
this._tail = node;
}
this._map.set(mappedKey, node);
this._head = node;
this._size++;
this._maybeDeleteLRU();
}
_maybeDeleteLRU() {
if (this.size() > this.maxSize()) {
this.deleteLru();
}
}
deleteLru(): void {
const tail = this.tail();
if (tail) {
this.delete(tail.key);
}
}
delete(key: K): void {
const mappedKey = this._keyMapper(key);
if (!this._size || !this._map.has(mappedKey)) {
return;
}
const node = nullthrows(this._map.get(mappedKey));
const right = node.right;
const left = node.left;
if (right) {
right.left = node.left;
}
if (left) {
left.right = node.right;
}
if (node === this.head()) {
this._head = right;
}
if (node === this.tail()) {
this._tail = left;
}
this._map.delete(mappedKey);
this._size--;
}
clear(): void {
this._size = 0;
this._head = null;
this._tail = null;
this._map = new Map<mixed, CacheNode<K, V>>();
}
}
module.exports = {LRUCache};

51
node_modules/recoil/cjs/caches/Recoil_MapCache.js.flow generated vendored Normal file
View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
type Options<K> = {
mapKey: K => mixed,
};
class MapCache<K, V> {
_map: Map<mixed, V>;
_keyMapper: K => mixed;
constructor(options?: Options<K>) {
this._map = new Map<mixed, V>();
this._keyMapper = options?.mapKey ?? (v => v);
}
size(): number {
return this._map.size;
}
has(key: K): boolean {
return this._map.has(this._keyMapper(key));
}
get(key: K): ?V {
return this._map.get(this._keyMapper(key));
}
set(key: K, val: V): void {
this._map.set(this._keyMapper(key), val);
}
delete(key: K): void {
this._map.delete(this._keyMapper(key));
}
clear(): void {
this._map.clear();
}
}
module.exports = {MapCache};

252
node_modules/recoil/cjs/caches/Recoil_TreeCache.js.flow generated vendored Normal file
View File

@@ -0,0 +1,252 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {
GetHandlers,
NodeCacheRoute,
NodeValueGet,
SetHandlers,
TreeCacheBranch,
TreeCacheLeaf,
TreeCacheNode,
} from './Recoil_TreeCacheImplementationType';
const {isFastRefreshEnabled} = require('../core/Recoil_ReactMode');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
export type Options<T> = {
name?: string,
mapNodeValue?: (value: mixed) => mixed,
onHit?: (node: TreeCacheLeaf<T>) => void,
onSet?: (node: TreeCacheLeaf<T>) => void,
};
class ChangedPathError extends Error {}
class TreeCache<T = mixed> {
_name: ?string;
_numLeafs: number;
// $FlowIssue[unclear-type]
_root: TreeCacheNode<any> | null;
_onHit: $NonMaybeType<Options<T>['onHit']>;
_onSet: $NonMaybeType<Options<T>['onSet']>;
_mapNodeValue: $NonMaybeType<Options<T>['mapNodeValue']>;
constructor(options?: Options<T>) {
this._name = options?.name;
this._numLeafs = 0;
this._root = null;
this._onHit = options?.onHit ?? (() => {});
this._onSet = options?.onSet ?? (() => {});
this._mapNodeValue = options?.mapNodeValue ?? (val => val);
}
size(): number {
return this._numLeafs;
}
// $FlowIssue[unclear-type]
root(): TreeCacheNode<any> | null {
return this._root;
}
get(getNodeValue: NodeValueGet, handlers?: GetHandlers<T>): ?T {
return this.getLeafNode(getNodeValue, handlers)?.value;
}
getLeafNode(
getNodeValue: NodeValueGet,
handlers?: GetHandlers<T>,
): ?TreeCacheLeaf<T> {
if (this._root == null) {
return undefined;
}
// Iterate down the tree based on the current node values until we hit a leaf
// $FlowIssue[unclear-type]
let node: ?TreeCacheNode<any> = this._root;
while (node) {
handlers?.onNodeVisit(node);
if (node.type === 'leaf') {
this._onHit(node);
return node;
}
const nodeValue = this._mapNodeValue(getNodeValue(node.nodeKey));
node = node.branches.get(nodeValue);
}
return undefined;
}
set(route: NodeCacheRoute, value: T, handlers?: SetHandlers<T>): void {
const addLeaf = () => {
// First, setup the branch nodes for the route:
// Iterate down the tree to find or add branch nodes following the route
let node: ?TreeCacheBranch<T>;
let branchKey;
for (const [nodeKey, nodeValue] of route) {
// If the previous root was a leaf, while we not have a get(), it means
// the selector has inconsistent values or implementation changed.
const root = this._root;
if (root?.type === 'leaf') {
throw this.invalidCacheError();
}
// node now refers to the next node down in the tree
const parent = node;
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
node = parent ? parent.branches.get(branchKey) : root;
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
node = node ?? {
type: 'branch',
nodeKey,
parent,
branches: new Map(),
branchKey,
};
// If we found an existing node, confirm it has a consistent value
if (node.type !== 'branch' || node.nodeKey !== nodeKey) {
throw this.invalidCacheError();
}
// Add the branch node to the tree
parent?.branches.set(branchKey, node);
handlers?.onNodeVisit?.(node);
// Prepare for next iteration and install root if it is new.
branchKey = this._mapNodeValue(nodeValue);
this._root = this._root ?? node;
}
// Second, setup the leaf node:
// If there is an existing leaf for this route confirm it is consistent
const oldLeaf: ?TreeCacheNode<T> = node
? node?.branches.get(branchKey)
: this._root;
if (
oldLeaf != null &&
(oldLeaf.type !== 'leaf' || oldLeaf.branchKey !== branchKey)
) {
throw this.invalidCacheError();
}
// Create a new or replacement leaf.
const leafNode = {
type: 'leaf',
value,
parent: node,
branchKey,
};
// Install the leaf and call handlers
node?.branches.set(branchKey, leafNode);
this._root = this._root ?? leafNode;
this._numLeafs++;
this._onSet(leafNode);
handlers?.onNodeVisit?.(leafNode);
};
try {
addLeaf();
} catch (error) {
// If the cache was stale or observed inconsistent values, such as with
// Fast Refresh, then clear it and rebuild with the new values.
if (error instanceof ChangedPathError) {
this.clear();
addLeaf();
} else {
throw error;
}
}
}
// Returns true if leaf was actually deleted from the tree
delete(leaf: TreeCacheLeaf<T>): boolean {
const root = this.root();
if (!root) {
return false;
}
if (leaf === root) {
this._root = null;
this._numLeafs = 0;
return true;
}
// Iterate up from the leaf deleteing it from it's parent's branches.
let node = leaf.parent;
let branchKey = leaf.branchKey;
while (node) {
node.branches.delete(branchKey);
// Stop iterating if we hit the root.
if (node === root) {
if (node.branches.size === 0) {
this._root = null;
this._numLeafs = 0;
} else {
this._numLeafs--;
}
return true;
}
// Stop iterating if there are other branches since we don't need to
// remove any more nodes.
if (node.branches.size > 0) {
break;
}
// Iterate up to our parent
branchKey = node?.branchKey;
node = node.parent;
}
// Confirm that the leaf we are deleting is actually attached to our tree
for (; node !== root; node = node.parent) {
if (node == null) {
return false;
}
}
this._numLeafs--;
return true;
}
clear(): void {
this._numLeafs = 0;
this._root = null;
}
invalidCacheError(): ChangedPathError {
const CHANGED_PATH_ERROR_MESSAGE = isFastRefreshEnabled()
? 'Possible Fast Refresh module reload detected. ' +
'This may also be caused by an selector returning inconsistent values. ' +
'Resetting cache.'
: 'Invalid cache values. This happens when selectors do not return ' +
'consistent values for the same input dependency values. That may also ' +
'be caused when using Fast Refresh to change a selector implementation. ' +
'Resetting cache.';
recoverableViolation(
CHANGED_PATH_ERROR_MESSAGE +
(this._name != null ? ` - ${this._name}` : ''),
'recoil',
);
throw new ChangedPathError();
}
}
module.exports = {TreeCache};

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from '../core/Recoil_Keys';
export type NodeCacheRoute = Array<[NodeKey, mixed]>;
export type TreeCacheNode<T> = TreeCacheLeaf<T> | TreeCacheBranch<T>;
export type TreeCacheLeaf<T> = {
type: 'leaf',
value: T,
branchKey?: mixed,
parent: ?TreeCacheBranch<T>,
};
export type TreeCacheBranch<T> = {
type: 'branch',
nodeKey: NodeKey,
branches: Map<mixed, TreeCacheNode<T>>,
branchKey?: mixed,
parent: ?TreeCacheBranch<T>,
};
export type NodeValueGet = (nodeKey: NodeKey) => mixed;
type NodeVisitHandler<T> = (node: TreeCacheNode<T>) => void;
export type GetHandlers<T> = {
onNodeVisit: NodeVisitHandler<T>,
};
export type SetHandlers<T> = {
onNodeVisit: NodeVisitHandler<T>,
};
/**
* This is an opinionated tree cache that conforms to the requirements needed
* by Recoil selectors.
*
* Unlike a conventional cache, the tree cache does not store key-value pairs,
* but "routes" that point to values. In the context of selectors these routes
* represent dependencies that a selector has to other atoms and selectors.
*
* In order to retrieve a value from the cache, a function is passed to the
* cache's `get()` method, and the tree cache will use that function to traverse
* itself, passing the provided function a "key" (the first part of the route tuple),
* reconstructing the route to some value (or undefined).
*
* The handlers are necessary for the selector to be able to capture the
* incremental nodes in the tree that are traversed while looking for a cache
* hit as these incremental nodes represent dependencies to the selector, which
* are used internally by the selector.
*/
export interface TreeCacheImplementation<T> {
get(NodeValueGet, handlers?: GetHandlers<T>): ?T;
set(NodeCacheRoute, T, handlers?: SetHandlers<T>): void;
delete(TreeCacheLeaf<T>): boolean;
clear(): void;
root(): ?TreeCacheNode<T>;
size(): number;
}

View File

@@ -0,0 +1,76 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {CacheImplementation} from './Recoil_CacheImplementationType';
import type {
CachePolicy,
EqualityPolicy,
EvictionPolicy,
} from './Recoil_CachePolicy';
const {LRUCache} = require('./Recoil_LRUCache');
const {MapCache} = require('./Recoil_MapCache');
const err = require('recoil-shared/util/Recoil_err');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const stableStringify = require('recoil-shared/util/Recoil_stableStringify');
const defaultPolicy: {
equality: 'reference',
eviction: 'none',
maxSize: number,
} = {
equality: 'reference',
eviction: 'none',
maxSize: Infinity,
};
function cacheFromPolicy<K, V>({
equality = defaultPolicy.equality,
eviction = defaultPolicy.eviction,
maxSize = defaultPolicy.maxSize,
}: // $FlowFixMe[incompatible-type]
CachePolicy = defaultPolicy): CacheImplementation<K, V> {
const valueMapper = getValueMapper(equality);
const cache = getCache<K, V>(eviction, maxSize, valueMapper);
return cache;
}
function getValueMapper(equality: EqualityPolicy): mixed => mixed {
switch (equality) {
case 'reference':
return val => val;
case 'value':
return val => stableStringify(val);
}
throw err(`Unrecognized equality policy ${equality}`);
}
function getCache<K, V>(
eviction: EvictionPolicy,
maxSize: ?number,
mapKey: mixed => mixed,
): CacheImplementation<K, V> {
switch (eviction) {
case 'keep-all':
return new MapCache<K, V>({mapKey});
case 'lru':
return new LRUCache<K, V>({mapKey, maxSize: nullthrows(maxSize)});
case 'most-recent':
return new LRUCache<K, V>({mapKey, maxSize: 1});
}
throw err(`Unrecognized eviction policy ${eviction}`);
}
module.exports = cacheFromPolicy;

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {
CachePolicy,
EqualityPolicy,
EvictionPolicy,
} from './Recoil_CachePolicy';
import type {TreeCacheImplementation} from './Recoil_TreeCacheImplementationType';
const {TreeCache} = require('./Recoil_TreeCache');
const treeCacheLRU = require('./Recoil_treeCacheLRU');
const err = require('recoil-shared/util/Recoil_err');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const stableStringify = require('recoil-shared/util/Recoil_stableStringify');
const defaultPolicy: {
equality: 'reference',
eviction: 'keep-all',
maxSize: number,
} = {
equality: 'reference',
eviction: 'keep-all',
maxSize: Infinity,
};
function treeCacheFromPolicy<T>(
{
equality = defaultPolicy.equality,
eviction = defaultPolicy.eviction,
maxSize = defaultPolicy.maxSize,
}: // $FlowFixMe[incompatible-type]
CachePolicy = defaultPolicy,
name?: string,
): TreeCacheImplementation<T> {
const valueMapper = getValueMapper(equality);
return getTreeCache(eviction, maxSize, valueMapper, name);
}
function getValueMapper(equality: EqualityPolicy): mixed => mixed {
switch (equality) {
case 'reference':
return val => val;
case 'value':
return val => stableStringify(val);
}
throw err(`Unrecognized equality policy ${equality}`);
}
function getTreeCache<T>(
eviction: EvictionPolicy,
maxSize: ?number,
mapNodeValue: mixed => mixed,
name?: string,
): TreeCacheImplementation<T> {
switch (eviction) {
case 'keep-all':
return new TreeCache<T>({name, mapNodeValue});
case 'lru':
return treeCacheLRU<T>({
name,
maxSize: nullthrows(maxSize),
mapNodeValue,
});
case 'most-recent':
return treeCacheLRU<T>({name, maxSize: 1, mapNodeValue});
}
throw err(`Unrecognized eviction policy ${eviction}`);
}
module.exports = treeCacheFromPolicy;

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {TreeCacheImplementation} from './Recoil_TreeCacheImplementationType';
const {LRUCache} = require('./Recoil_LRUCache');
const {TreeCache} = require('./Recoil_TreeCache');
function treeCacheLRU<T>({
name,
maxSize,
mapNodeValue = (v: mixed) => v,
}: {
name?: string,
maxSize: number,
mapNodeValue?: mixed => mixed,
}): TreeCacheImplementation<T> {
const lruCache = new LRUCache({maxSize});
const cache: TreeCache<T> = new TreeCache({
name,
mapNodeValue,
onHit: node => {
lruCache.set(node, true);
},
onSet: node => {
const lruNode = lruCache.tail();
lruCache.set(node, true);
if (lruNode && cache.size() > maxSize) {
// $FlowFixMe[incompatible-call]
cache.delete(lruNode.key);
}
},
});
return cache;
}
module.exports = treeCacheLRU;

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let LRUCache;
const testRecoil = getRecoilTestFn(() => {
({LRUCache} = require('../Recoil_LRUCache'));
});
describe('LRUCache', () => {
testRecoil('setting and getting (without hitting max size)', () => {
const cache = new LRUCache({
maxSize: 10,
});
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
expect(cache.size()).toBe(3);
expect(cache.get('a')).toBe(1);
expect(cache.get('b')).toBe(2);
expect(cache.get('c')).toBe(3);
cache.delete('a');
cache.delete('b');
expect(cache.size()).toBe(1);
});
testRecoil('setting and getting (hitting max size)', () => {
const cache = new LRUCache({
maxSize: 2,
});
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
expect(cache.size()).toBe(2);
expect(cache.get('a')).toBe(undefined);
expect(cache.get('b')).toBe(2);
expect(cache.get('c')).toBe(3);
cache.delete('a');
cache.delete('b');
expect(cache.size()).toBe(1);
cache.set('d', 4);
cache.set('e', 5);
expect(cache.size()).toBe(2);
expect(cache.get('b')).toBe(undefined);
expect(cache.get('c')).toBe(undefined);
});
testRecoil('manually deleting LRU', () => {
const cache = new LRUCache({
maxSize: 10,
});
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
expect(cache.size()).toBe(3);
expect(cache.get('a')).toBe(1);
expect(cache.get('b')).toBe(2);
expect(cache.get('c')).toBe(3);
cache.deleteLru(); // delete 'a'
expect(cache.get('a')).toBe(undefined);
expect(cache.size()).toBe(2);
cache.deleteLru(); // delete 'b'
expect(cache.get('b')).toBe(undefined);
expect(cache.size()).toBe(1);
});
testRecoil('head() and tail()', () => {
const cache = new LRUCache({
maxSize: 10,
});
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
expect(cache.size()).toBe(3);
expect(cache.tail()).toBeDefined();
expect(cache.tail()?.value).toBe(1);
expect(cache.head()?.value).toBe(3);
expect(cache.get('c')).toBe(3);
expect(cache.get('b')).toBe(2);
expect(cache.get('a')).toBe(1);
expect(cache.tail()?.value).toBe(3);
expect(cache.head()?.value).toBe(1);
cache.delete('a');
cache.delete('b');
expect(cache.tail()?.value).toBe(3);
expect(cache.head()?.value).toBe(3);
expect(cache.size()).toBe(1);
});
});

View File

@@ -0,0 +1,53 @@
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let MapCache;
const testRecoil = getRecoilTestFn(() => {
({MapCache} = require('../Recoil_MapCache'));
});
describe('MapCache', () => {
testRecoil('setting and getting', () => {
const cache = new MapCache<string, number>();
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
expect(cache.size()).toBe(3);
expect(cache.get('a')).toBe(1);
expect(cache.get('b')).toBe(2);
expect(cache.get('c')).toBe(3);
});
testRecoil('deleting', () => {
const cache = new MapCache<string, number>();
cache.set('a', 1);
cache.set('b', 2);
cache.set('c', 3);
expect(cache.size()).toBe(3);
cache.delete('a');
expect(cache.size()).toBe(2);
expect(cache.get('a')).toBe(undefined);
expect(cache.get('b')).toBe(2);
expect(cache.has('a')).toBe(false);
});
});

View File

@@ -0,0 +1,264 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from 'Recoil_Keys';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let TreeCache, loadableWithValue, nullthrows;
const testRecoil = getRecoilTestFn(() => {
({TreeCache} = require('../Recoil_TreeCache'));
nullthrows = require('recoil-shared/util/Recoil_nullthrows');
({loadableWithValue} = require('../../adt/Recoil_Loadable'));
});
describe('TreeCache', () => {
testRecoil('setting and getting values', () => {
const cache = new TreeCache();
const [route1, loadable1] = [
[
['a', 2],
['b', 3],
],
loadableWithValue('value1'),
];
const [route2, loadable2] = [
[
['a', 3],
['b', 4],
],
loadableWithValue('value2'),
];
const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')];
cache.set(route1, loadable1);
cache.set(route2, loadable2);
cache.set(route3, loadable3);
expect(
cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable1);
expect(
cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable2);
expect(
cache.get(nodeKey => route3.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable3);
expect(cache.size()).toBe(3);
});
testRecoil('deleting values', () => {
const cache = new TreeCache();
const [route1, loadable1] = [
[
['a', 2],
['b', 3],
],
loadableWithValue('value1'),
];
const [route2, loadable2] = [
[
['a', 2],
['b', 4],
['c', 5],
],
loadableWithValue('value2'),
];
const [route3, loadable3] = [[['a', 6]], loadableWithValue('value3')];
cache.set(route1, loadable1);
cache.set(route2, loadable2);
cache.set(route3, loadable3);
const leaf1 = cache.getLeafNode(
nodeKey => route1.find(([key]) => key === nodeKey)?.[1],
);
const leaf2 = cache.getLeafNode(
nodeKey => route2.find(([key]) => key === nodeKey)?.[1],
);
const leaf3 = cache.getLeafNode(
nodeKey => route3.find(([key]) => key === nodeKey)?.[1],
);
expect(leaf1).toBeDefined();
expect(leaf2).toBeDefined();
expect(leaf3).toBeDefined();
const leaf1Node = nullthrows(leaf1);
const leaf2Node = nullthrows(leaf2);
const leaf3Node = nullthrows(leaf3);
expect(cache.size()).toBe(3);
const deleted1 = cache.delete(leaf1Node);
expect(deleted1).toBe(true);
expect(cache.size()).toBe(2);
const deleted2 = cache.delete(leaf2Node);
expect(deleted2).toBe(true);
expect(cache.size()).toBe(1);
const deleted3 = cache.delete(leaf3Node);
expect(deleted3).toBe(true);
expect(cache.size()).toBe(0);
expect(cache.root()).toBeNull();
const deletedAgain = cache.delete(leaf1Node);
expect(deletedAgain).toBe(false);
});
testRecoil('onHit() handler', () => {
const [route1, loadable1] = [
[
['a', 2],
['b', 3],
],
loadableWithValue('value1'),
];
const onHit = jest.fn();
const cache = new TreeCache({
onHit,
});
const getter = (nodeKey: NodeKey) =>
route1.find(([key]) => key === nodeKey)?.[1];
cache.set(route1, loadable1);
// hit
cache.get(getter);
// miss
cache.get(() => {});
// hit
cache.get(getter);
expect(onHit).toHaveBeenCalledTimes(2);
});
testRecoil('onSet() handler', () => {
const onSet = jest.fn();
const cache = new TreeCache({
onSet,
});
const [route1, loadable1] = [
[
['a', 2],
['b', 3],
],
loadableWithValue('value1'),
];
const [route2, loadable2] = [
[
['a', 3],
['b', 4],
],
loadableWithValue('value2'),
];
const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')];
cache.set(route1, loadable1);
cache.set(route2, loadable2);
cache.set(route3, loadable3);
expect(onSet).toHaveBeenCalledTimes(3);
});
testRecoil('default key generation uses reference equality', () => {
const [route1, loadable1] = [
[
['a', [2]],
['b', [3]],
],
loadableWithValue('value1'),
];
const cache = new TreeCache();
cache.set(route1, loadable1);
const resultWithKeyCopy = cache.get(nodeKey => [
...(route1.find(([key]) => key === nodeKey)?.[1] ?? []),
]);
expect(resultWithKeyCopy).toBeUndefined();
const result = cache.get(
nodeKey => route1.find(([key]) => key === nodeKey)?.[1],
);
expect(result).toBe(loadable1);
});
testRecoil('mapNodeValue() to implement value equality keys', () => {
const cache = new TreeCache({
mapNodeValue: value => JSON.stringify(value),
});
const [route1, loadable1] = [
[
['a', [2]],
['b', [3]],
],
loadableWithValue('value1'),
];
cache.set(route1, loadable1);
const resultWithKeyCopy = cache.get(nodeKey => [
...(route1.find(([key]) => key === nodeKey)?.[1] ?? []),
]);
expect(resultWithKeyCopy).toBe(loadable1);
});
// Test ability to scale cache to large number of entries.
// Use more dependencies than the JavaScript callstack depth limit to ensure
// we are not using a recursive algorithm.
testRecoil('Scalability', () => {
const cache = new TreeCache();
const route = Array.from(Array(10000).keys()).map(i => [
String(i),
String(i),
]);
// $FlowFixMe[incompatible-call]
cache.set(route, 'VALUE');
expect(cache.get(x => x)).toBe('VALUE');
const leafNode = cache.getLeafNode(x => x);
expect(cache.delete(nullthrows(leafNode))).toBe(true);
});
});

View File

@@ -0,0 +1,203 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let cacheFromPolicy;
const testRecoil = getRecoilTestFn(() => {
cacheFromPolicy = require('../Recoil_cacheFromPolicy');
});
describe('cacheFromPolicy()', () => {
testRecoil('equality: reference, eviction: keep-all', () => {
const policy = {equality: 'reference', eviction: 'keep-all'};
const cache = cacheFromPolicy<{[string]: number}, boolean>(policy);
const obj1 = {a: 1};
const obj2 = {b: 2};
const obj3 = {c: 3};
cache.set(obj1, true);
cache.set(obj2, true);
cache.set(obj3, true);
expect(cache.size()).toBe(3);
expect(cache.get(obj1)).toBe(true);
expect(cache.get(obj2)).toBe(true);
expect(cache.get(obj3)).toBe(true);
expect(cache.get({...obj1})).toBe(undefined);
expect(cache.get({...obj2})).toBe(undefined);
expect(cache.get({...obj3})).toBe(undefined);
});
testRecoil('equality: value, eviction: keep-all', () => {
const policy = {equality: 'value', eviction: 'keep-all'};
const cache = cacheFromPolicy<{[string]: number}, boolean>(policy);
const obj1 = {a: 1};
const obj2 = {b: 2};
const obj3 = {c: 3};
cache.set(obj1, true);
cache.set(obj2, true);
cache.set(obj3, true);
expect(cache.size()).toBe(3);
expect(cache.get(obj1)).toBe(true);
expect(cache.get(obj2)).toBe(true);
expect(cache.get(obj3)).toBe(true);
expect(cache.get({...obj1})).toBe(true);
expect(cache.get({...obj2})).toBe(true);
expect(cache.get({...obj3})).toBe(true);
});
testRecoil('equality: reference, eviction: lru', () => {
const policy = {equality: 'reference', eviction: 'lru', maxSize: 2};
const cache = cacheFromPolicy<{[string]: number}, boolean>(policy);
const obj1 = {a: 1};
const obj2 = {b: 2};
const obj3 = {c: 3};
cache.set(obj1, true);
cache.set(obj2, true);
cache.set(obj3, true);
expect(cache.size()).toBe(2);
expect(cache.get(obj1)).toBe(undefined);
expect(cache.get(obj2)).toBe(true);
expect(cache.get(obj3)).toBe(true);
cache.set(obj1, true);
expect(cache.size()).toBe(2);
expect(cache.get(obj2)).toBe(undefined);
expect(cache.get(obj1)).toBe(true);
expect(cache.get(obj3)).toBe(true);
expect(cache.get({...obj1})).toBe(undefined);
expect(cache.get({...obj3})).toBe(undefined);
});
testRecoil('equality: value, eviction: lru', () => {
const policy = {equality: 'value', eviction: 'lru', maxSize: 2};
const cache = cacheFromPolicy<{[string]: number}, boolean>(policy);
const obj1 = {a: 1};
const obj2 = {b: 2};
const obj3 = {c: 3};
cache.set(obj1, true);
cache.set(obj2, true);
cache.set(obj3, true);
expect(cache.size()).toBe(2);
expect(cache.get(obj1)).toBe(undefined);
expect(cache.get(obj2)).toBe(true);
expect(cache.get(obj3)).toBe(true);
cache.set(obj1, true);
expect(cache.size()).toBe(2);
expect(cache.get(obj2)).toBe(undefined);
expect(cache.get(obj1)).toBe(true);
expect(cache.get(obj3)).toBe(true);
expect(cache.get({...obj2})).toBe(undefined);
expect(cache.get({...obj1})).toBe(true);
expect(cache.get({...obj3})).toBe(true);
});
testRecoil('equality: reference, eviction: most-recent', () => {
const policy = {equality: 'reference', eviction: 'most-recent'};
const cache = cacheFromPolicy<{[string]: number}, boolean>(policy);
const obj1 = {a: 1};
const obj2 = {b: 2};
const obj3 = {c: 3};
cache.set(obj1, true);
cache.set(obj2, true);
cache.set(obj3, true);
expect(cache.size()).toBe(1);
expect(cache.get(obj1)).toBe(undefined);
expect(cache.get(obj2)).toBe(undefined);
expect(cache.get(obj3)).toBe(true);
cache.set(obj1, true);
expect(cache.size()).toBe(1);
expect(cache.get(obj2)).toBe(undefined);
expect(cache.get(obj3)).toBe(undefined);
expect(cache.get(obj1)).toBe(true);
expect(cache.get({...obj2})).toBe(undefined);
expect(cache.get({...obj1})).toBe(undefined);
expect(cache.get({...obj3})).toBe(undefined);
});
testRecoil('equality: value, eviction: most-recent', () => {
const policy = {equality: 'value', eviction: 'most-recent'};
const cache = cacheFromPolicy<{[string]: number}, boolean>(policy);
const obj1 = {a: 1};
const obj2 = {b: 2};
const obj3 = {c: 3};
cache.set(obj1, true);
cache.set(obj2, true);
cache.set(obj3, true);
expect(cache.size()).toBe(1);
expect(cache.get(obj1)).toBe(undefined);
expect(cache.get(obj2)).toBe(undefined);
expect(cache.get(obj3)).toBe(true);
cache.set(obj1, true);
expect(cache.size()).toBe(1);
expect(cache.get(obj2)).toBe(undefined);
expect(cache.get(obj3)).toBe(undefined);
expect(cache.get(obj1)).toBe(true);
expect(cache.get({...obj2})).toBe(undefined);
expect(cache.get({...obj3})).toBe(undefined);
expect(cache.get({...obj1})).toBe(true);
});
});

View File

@@ -0,0 +1,276 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from 'Recoil_Keys';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let treeCacheFromPolicy;
const testRecoil = getRecoilTestFn(() => {
treeCacheFromPolicy = require('../Recoil_treeCacheFromPolicy');
});
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
const valGetterFromPath = path => (nodeKey: NodeKey) =>
path.find(([k]) => k === nodeKey)?.[1];
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
const clonePath = path => JSON.parse(JSON.stringify(path));
describe('treeCacheFromPolicy()', () => {
testRecoil('equality: reference, eviction: keep-all', () => {
const policy = {equality: 'reference', eviction: 'keep-all'};
const cache = treeCacheFromPolicy<{[string]: number}>(policy);
const path1 = [
['a', [1]],
['b', [2]],
];
const obj1 = {a: 1};
const path2 = [['a', [2]]];
const obj2 = {b: 2};
const path3 = [
['a', [3]],
['c', [4]],
];
const obj3 = {c: 3};
cache.set(path1, obj1);
cache.set(path2, obj2);
cache.set(path3, obj3);
expect(cache.size()).toBe(3);
expect(cache.get(valGetterFromPath(path1))).toBe(obj1);
expect(cache.get(valGetterFromPath(path2))).toBe(obj2);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(undefined);
expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(undefined);
expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined);
});
testRecoil('equality: value, eviction: keep-all', () => {
const policy = {equality: 'value', eviction: 'keep-all'};
const cache = treeCacheFromPolicy<{[string]: number}>(policy);
const path1 = [
['a', [1]],
['b', [2]],
];
const obj1 = {a: 1};
const path2 = [['a', [2]]];
const obj2 = {b: 2};
const path3 = [
['a', [3]],
['c', [4]],
];
const obj3 = {c: 3};
cache.set(path1, obj1);
cache.set(path2, obj2);
cache.set(path3, obj3);
expect(cache.size()).toBe(3);
expect(cache.get(valGetterFromPath(path1))).toBe(obj1);
expect(cache.get(valGetterFromPath(path2))).toBe(obj2);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(obj1);
expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(obj2);
expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(obj3);
});
testRecoil('equality: reference, eviction: lru', () => {
const policy = {equality: 'reference', eviction: 'lru', maxSize: 2};
const cache = treeCacheFromPolicy<{[string]: number}>(policy);
const path1 = [
['a', [1]],
['b', [2]],
];
const obj1 = {a: 1};
const path2 = [['a', [2]]];
const obj2 = {b: 2};
const path3 = [
['a', [3]],
['c', [4]],
];
const obj3 = {c: 3};
cache.set(path1, obj1);
cache.set(path2, obj2);
cache.set(path3, obj3);
expect(cache.size()).toBe(2);
expect(cache.get(valGetterFromPath(path1))).toBe(undefined);
expect(cache.get(valGetterFromPath(path2))).toBe(obj2);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
cache.set(path1, obj1);
expect(cache.size()).toBe(2);
expect(cache.get(valGetterFromPath(path2))).toBe(undefined);
expect(cache.get(valGetterFromPath(path1))).toBe(obj1);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(undefined);
expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined);
});
testRecoil('equality: value, eviction: lru', () => {
const policy = {equality: 'value', eviction: 'lru', maxSize: 2};
const cache = treeCacheFromPolicy<{[string]: number}>(policy);
const path1 = [
['a', [1]],
['b', [2]],
];
const obj1 = {a: 1};
const path2 = [['a', [2]]];
const obj2 = {b: 2};
const path3 = [
['a', [3]],
['c', [4]],
];
const obj3 = {c: 3};
cache.set(path1, obj1);
cache.set(path2, obj2);
cache.set(path3, obj3);
expect(cache.size()).toBe(2);
expect(cache.get(valGetterFromPath(path1))).toBe(undefined);
expect(cache.get(valGetterFromPath(path2))).toBe(obj2);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
cache.set(path1, obj1);
expect(cache.size()).toBe(2);
expect(cache.get(valGetterFromPath(path2))).toBe(undefined);
expect(cache.get(valGetterFromPath(path1))).toBe(obj1);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(obj1);
expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(obj3);
});
testRecoil('equality: reference, eviction: most-recent', () => {
const policy = {equality: 'reference', eviction: 'most-recent'};
const cache = treeCacheFromPolicy<{[string]: number}>(policy);
const path1 = [
['a', [1]],
['b', [2]],
];
const obj1 = {a: 1};
const path2 = [['a', [2]]];
const obj2 = {b: 2};
const path3 = [
['a', [3]],
['c', [4]],
];
const obj3 = {c: 3};
cache.set(path1, obj1);
cache.set(path2, obj2);
cache.set(path3, obj3);
expect(cache.size()).toBe(1);
expect(cache.get(valGetterFromPath(path1))).toBe(undefined);
expect(cache.get(valGetterFromPath(path2))).toBe(undefined);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
cache.set(path1, obj1);
expect(cache.size()).toBe(1);
expect(cache.get(valGetterFromPath(path2))).toBe(undefined);
expect(cache.get(valGetterFromPath(path3))).toBe(undefined);
expect(cache.get(valGetterFromPath(path1))).toBe(obj1);
expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(undefined);
expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(undefined);
expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined);
});
testRecoil('equality: value, eviction: most-recent', () => {
const policy = {equality: 'value', eviction: 'most-recent'};
const cache = treeCacheFromPolicy<{[string]: number}>(policy);
const path1 = [
['a', [1]],
['b', [2]],
];
const obj1 = {a: 1};
const path2 = [['a', [2]]];
const obj2 = {b: 2};
const path3 = [
['a', [3]],
['c', [4]],
];
const obj3 = {c: 3};
cache.set(path1, obj1);
cache.set(path2, obj2);
cache.set(path3, obj3);
expect(cache.size()).toBe(1);
expect(cache.get(valGetterFromPath(path1))).toBe(undefined);
expect(cache.get(valGetterFromPath(path2))).toBe(undefined);
expect(cache.get(valGetterFromPath(path3))).toBe(obj3);
cache.set(path1, obj1);
expect(cache.size()).toBe(1);
expect(cache.get(valGetterFromPath(path2))).toBe(undefined);
expect(cache.get(valGetterFromPath(path3))).toBe(undefined);
expect(cache.get(valGetterFromPath(path1))).toBe(obj1);
expect(cache.get(valGetterFromPath(clonePath(path1)))).toBe(obj1);
expect(cache.get(valGetterFromPath(clonePath(path2)))).toBe(undefined);
expect(cache.get(valGetterFromPath(clonePath(path3)))).toBe(undefined);
});
});

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let treeCacheLRU, loadableWithValue;
const testRecoil = getRecoilTestFn(() => {
treeCacheLRU = require('../Recoil_treeCacheLRU');
({loadableWithValue} = require('../../adt/Recoil_Loadable'));
});
describe('treeCacheLRU()', () => {
testRecoil('getting and setting cache', () => {
const cache = treeCacheLRU<$FlowFixMe>({maxSize: 10});
const [route1, loadable1] = [
[
['a', 2],
['b', 3],
],
loadableWithValue('value1'),
];
const [route2, loadable2] = [
[
['a', 3],
['b', 4],
],
loadableWithValue('value2'),
];
const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')];
cache.set(route1, loadable1);
cache.set(route2, loadable2);
cache.set(route3, loadable3);
expect(
cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable1);
expect(
cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable2);
expect(
cache.get(nodeKey => route3.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable3);
expect(cache.size()).toBe(3);
});
testRecoil('getting and setting cache (hitting max size)', () => {
const cache = treeCacheLRU<$FlowFixMe>({maxSize: 2});
const [route1, loadable1] = [
[
['a', 2],
['b', 3],
],
loadableWithValue('value1'),
];
const [route2, loadable2] = [
[
['a', 3],
['b', 4],
],
loadableWithValue('value2'),
];
const [route3, loadable3] = [[['a', 4]], loadableWithValue('value3')];
cache.set(route1, loadable1);
cache.set(route2, loadable2);
cache.set(route3, loadable3);
expect(
cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]),
).toBe(undefined);
expect(
cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable2);
expect(
cache.get(nodeKey => route3.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable3);
expect(cache.size()).toBe(2);
cache.set(route1, loadable1);
expect(
cache.get(nodeKey => route1.find(([key]) => key === nodeKey)?.[1]),
).toBe(loadable1);
expect(
cache.get(nodeKey => route2.find(([key]) => key === nodeKey)?.[1]),
).toBe(undefined);
expect(cache.size()).toBe(2);
});
});

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Snapshot} from '../../core/Recoil_Snapshot';
const {
useGotoRecoilSnapshot,
useRecoilSnapshot,
} = require('../../hooks/Recoil_SnapshotHooks');
const React = require('react');
const {useEffect, useRef} = require('react');
type Props = $ReadOnly<{
name?: string,
persistenceLimit?: number,
initialSnapshot?: ?Snapshot,
devMode?: ?boolean,
maxDepth?: number,
maxItems?: number,
serializeFn?: (mixed, string) => mixed,
}>;
type ConnectProps = $ReadOnly<{
...Props,
goToSnapshot: Snapshot => void,
}>;
function connect(props: ConnectProps): ?{
track: (transactionId: number, snapshot: Snapshot) => void,
disconnect: () => void,
} {
if (typeof window === 'undefined') {
return null;
}
return window.__RECOIL_DEVTOOLS_EXTENSION__?.connect?.(props);
}
let CONNECTION_INDEX = 0;
/**
* @explorer-desc
* Recoil Dev Tools Connector
*/
function Connector({
name = `Recoil Connection ${CONNECTION_INDEX++}`,
persistenceLimit = 50,
maxDepth,
maxItems,
serializeFn,
devMode = true,
}: Props): React.Node {
const transactionIdRef = useRef(0);
const connectionRef = useRef<?{
disconnect: () => void,
track: (transactionId: number, snapshot: Snapshot) => void,
}>(null);
const goToSnapshot = useGotoRecoilSnapshot();
const snapshot = useRecoilSnapshot();
const release = snapshot.retain();
useEffect(() => {
if (connectionRef.current == null) {
connectionRef.current = connect({
name,
persistenceLimit,
devMode,
goToSnapshot,
maxDepth,
maxItems,
serializeFn,
});
}
return () => {
connectionRef.current?.disconnect();
connectionRef.current = null;
};
}, [
devMode,
goToSnapshot,
maxDepth,
maxItems,
name,
persistenceLimit,
serializeFn,
]);
useEffect(() => {
try {
const transactionID = transactionIdRef.current++;
connectionRef.current?.track?.(transactionID, snapshot);
} finally {
release();
}
}, [snapshot, release]);
return null;
}
module.exports = Connector;

View File

@@ -0,0 +1,123 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {MutableSnapshot, Snapshot} from '../../core/Recoil_Snapshot';
const {
useGotoRecoilSnapshot,
useRecoilSnapshot,
} = require('../../hooks/Recoil_SnapshotHooks');
const React = require('react');
const {useCallback} = require('react');
type AnchorProps = {
download?: true | string,
rel?: string,
target?: '_self' | '_blank' | '_parent' | '_top',
onClick?: (SyntheticUIEvent<HTMLAnchorElement>) => void,
style?: {[string]: string | number, ...},
children?: React.Node,
};
type SerializationProps = {
uriFromSnapshot: Snapshot => string,
};
type LinkToSnapshotProps = {
...AnchorProps,
...SerializationProps,
snapshot: Snapshot,
};
// A Link component based on the provided `uriFromSnapshot` mapping
// of a URI from a Recoil Snapshot.
//
// The Link element renders an anchor element. But instead of an href, use a
// `snapshot` property. When clicked, the Link element updates the current
// state to the snapshot without loading a new document.
//
// The href property of the anchor will set using `uriFromSnapshot`. This
// allows users to copy the link, choose to open in a new tab, &c.
//
// If an `onClick` handler is provided, it is called before the state transition
// and may call preventDefault on the event to stop the state transition.
function LinkToRecoilSnapshot({
uriFromSnapshot,
snapshot,
...anchorProps
}: LinkToSnapshotProps): React.Node {
const gotoSnapshot = useGotoRecoilSnapshot();
const {onClick, target} = anchorProps;
const onClickWrapper = useCallback(
(event: $FlowFixMe) => {
onClick?.(event);
if (
!event.defaultPrevented &&
event.button === 0 && // left-click
!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) &&
(!target || target === '_self')
) {
event.preventDefault();
gotoSnapshot(snapshot);
}
},
[target, onClick, gotoSnapshot, snapshot],
);
return (
<a
{...anchorProps}
href={uriFromSnapshot(snapshot)}
onClick={onClickWrapper}
/>
);
}
type LinkToStateChangeProps = {
...AnchorProps,
...SerializationProps,
stateChange: MutableSnapshot => void,
};
// A Link component based on the provided `uriFromSnapshot` mapping
// of a URI from a Recoil Snapshot.
//
// The Link element renders an anchor element. But instead of an href, use a
// `stateChange` property. When clicked, the Link element updates the current
// state based on the `stateChange` callback without loading a new document.
// `stateChange` is a function which takes a `MutableSnapshot` that can be used
// to read the current state and set or update any changes.
//
// The href property of the anchor will set using `uriFromSnapshot`. This
// allows users to copy the link, choose to open in a new tab, &c.
//
// If an `onClick` handler is provided, it is called before the state transition
// and may call preventDefault on the event to stop the state transition.
//
// Note that, because the link renders the href based on the current state
// snapshot, it is re-rendered whenever any state change is made. Keep the
// performance implications of this in mind.
function LinkToRecoilStateChange({
stateChange,
...linkProps
}: LinkToStateChangeProps): React.Node {
const currentSnapshot = useRecoilSnapshot();
const snapshot = currentSnapshot.map(stateChange);
return <LinkToRecoilSnapshot {...linkProps} snapshot={snapshot} />;
}
module.exports = {
LinkToRecoilSnapshot,
LinkToRecoilStateChange,
};

View File

@@ -0,0 +1,148 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {MutableSnapshot, Snapshot} from 'Recoil_Snapshot';
const {Simulate, act} = require('ReactTestUtils');
const {freshSnapshot} = require('../../../core/Recoil_Snapshot');
const atom = require('../../../recoil_values/Recoil_atom');
const {
LinkToRecoilSnapshot,
LinkToRecoilStateChange,
} = require('../Recoil_Link');
const React = require('react');
const {
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
const myAtom = atom<string>({key: 'Link Snapshot', default: 'DEFAULT'});
const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const LinkToSnapshot = ({
snapshot,
children,
}: $TEMPORARY$object<{
children: Array<$TEMPORARY$string<'LINK-'> | string>,
snapshot: Snapshot,
}>) => (
<LinkToRecoilSnapshot
snapshot={snapshot}
uriFromSnapshot={({getLoadable}) =>
`https://test.com/test?atom="${getLoadable(myAtom)
.valueOrThrow()
.toString()}`
}>
{children}
</LinkToRecoilSnapshot>
);
const LinkToStateChange = ({
stateChange,
children,
}: $TEMPORARY$object<{
children: $TEMPORARY$string<'LINK'>,
stateChange: MutableSnapshot => void,
}>) => (
<LinkToRecoilStateChange
stateChange={stateChange}
uriFromSnapshot={({getLoadable}) =>
`https://test.com/test?atom="${getLoadable(myAtom)
.valueOrThrow()
.toString()}`
}>
{children}
</LinkToRecoilStateChange>
);
test('Link - snapshot', async () => {
const snapshot = freshSnapshot().map(({set}) => set(myAtom, 'MAP'));
const c = renderElements(
<>
<ReadsAndWritesAtom />
<LinkToSnapshot snapshot={snapshot}>
LINK-{snapshot.getLoadable(myAtom).valueOrThrow().toString()}
</LinkToSnapshot>
</>,
);
expect(c.textContent).toEqual('"DEFAULT"LINK-MAP');
act(() => setAtom('SET'));
expect(c.textContent).toEqual('"SET"LINK-MAP');
// flowlint-next-line unclear-type:off
expect(((c.children[0]: any): HTMLAnchorElement).href).toEqual(
'https://test.com/test?atom=%22MAP',
);
act(() => {
Simulate.click(c.children[0], {button: 0});
});
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"MAP"LINK-MAP');
});
test('Link - stateChange', async () => {
const c = renderElements(
<>
<ReadsAndWritesAtom />
<LinkToStateChange stateChange={({set}) => set(myAtom, 'MAP')}>
LINK
</LinkToStateChange>
</>,
);
expect(c.textContent).toEqual('"DEFAULT"LINK');
act(() => setAtom('SET'));
expect(c.textContent).toEqual('"SET"LINK');
// flowlint-next-line unclear-type:off
expect(((c.children[0]: any): HTMLAnchorElement).href).toEqual(
'https://test.com/test?atom=%22MAP',
);
act(() => {
Simulate.click(c.children[0], {button: 0});
});
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"MAP"LINK');
});
test('Link - state update', async () => {
const c = renderElements(
<>
<ReadsAndWritesAtom />
<LinkToStateChange
stateChange={({set}) => set(myAtom, value => 'MAP ' + value)}>
LINK
</LinkToStateChange>
</>,
);
expect(c.textContent).toEqual('"DEFAULT"LINK');
act(() => setAtom('SET'));
expect(c.textContent).toEqual('"SET"LINK');
// flowlint-next-line unclear-type:off
expect(((c.children[0]: any): HTMLAnchorElement).href).toEqual(
'https://test.com/test?atom=%22MAP%20SET',
);
act(() => {
Simulate.click(c.children[0], {button: 0});
});
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"MAP SET"LINK');
});

View File

@@ -0,0 +1,125 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {ValueOrUpdater} from '../recoil_values/Recoil_callbackTypes';
import type {RecoilState, RecoilValue} from './Recoil_RecoilValue';
import type {NodeKey, Store, TreeState} from './Recoil_State';
const {loadableWithValue} = require('../adt/Recoil_Loadable');
const {initializeNode} = require('./Recoil_FunctionalCore');
const {DEFAULT_VALUE, getNode} = require('./Recoil_Node');
const {
copyTreeState,
getRecoilValueAsLoadable,
invalidateDownstreams,
writeLoadableToTreeState,
} = require('./Recoil_RecoilValueInterface');
const err = require('recoil-shared/util/Recoil_err');
export interface TransactionInterface {
get: <T>(RecoilValue<T>) => T;
set: <T>(RecoilState<T>, ValueOrUpdater<T>) => void;
reset: <T>(RecoilState<T>) => void;
}
function isAtom<T>(recoilValue: RecoilValue<T>): boolean {
return getNode(recoilValue.key).nodeType === 'atom';
}
class TransactionInterfaceImpl {
_store: Store;
_treeState: TreeState;
_changes: Map<NodeKey, mixed>;
constructor(store: Store, treeState: TreeState) {
this._store = store;
this._treeState = treeState;
this._changes = new Map();
}
// Allow destructing
// eslint-disable-next-line fb-www/extra-arrow-initializer
get = <T>(recoilValue: RecoilValue<T>): T => {
if (this._changes.has(recoilValue.key)) {
// $FlowIssue[incompatible-return]
return this._changes.get(recoilValue.key);
}
if (!isAtom(recoilValue)) {
throw err('Reading selectors within atomicUpdate is not supported');
}
const loadable = getRecoilValueAsLoadable(
this._store,
recoilValue,
this._treeState,
);
if (loadable.state === 'hasValue') {
return loadable.contents;
} else if (loadable.state === 'hasError') {
throw loadable.contents;
} else {
throw err(
`Expected Recoil atom ${recoilValue.key} to have a value, but it is in a loading state.`,
);
}
};
// Allow destructing
// eslint-disable-next-line fb-www/extra-arrow-initializer
set = <T>(
recoilState: RecoilState<T>,
valueOrUpdater: ValueOrUpdater<T>,
): void => {
if (!isAtom(recoilState)) {
throw err('Setting selectors within atomicUpdate is not supported');
}
if (typeof valueOrUpdater === 'function') {
const current = this.get(recoilState);
this._changes.set(recoilState.key, (valueOrUpdater: any)(current)); // flowlint-line unclear-type:off
} else {
// Initialize atom and run effects if not initialized yet
initializeNode(this._store, recoilState.key, 'set');
this._changes.set(recoilState.key, valueOrUpdater);
}
};
// Allow destructing
// eslint-disable-next-line fb-www/extra-arrow-initializer
reset = <T>(recoilState: RecoilState<T>): void => {
this.set(recoilState, DEFAULT_VALUE);
};
newTreeState_INTERNAL(): TreeState {
if (this._changes.size === 0) {
return this._treeState;
}
const newState = copyTreeState(this._treeState);
for (const [k, v] of this._changes) {
writeLoadableToTreeState(newState, k, loadableWithValue(v));
}
invalidateDownstreams(this._store, newState);
return newState;
}
}
function atomicUpdater(store: Store): ((TransactionInterface) => void) => void {
return fn => {
store.replaceState(treeState => {
const changeset = new TransactionInterfaceImpl(store, treeState);
fn(changeset);
return changeset.newTreeState_INTERNAL();
});
};
}
module.exports = {atomicUpdater};

62
node_modules/recoil/cjs/core/Recoil_Batching.js.flow generated vendored Normal file
View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
const {batchStart} = require('../core/Recoil_RecoilValueInterface');
const {
unstable_batchedUpdates,
} = require('recoil-shared/util/Recoil_ReactBatchedUpdates');
// flowlint-next-line unclear-type:off
type Callback = () => any;
type Batcher = (callback: Callback) => void;
/*
* During SSR, unstable_batchedUpdates may be undefined so this
* falls back to a basic function that executes the batch
*/
let batcher: Batcher = unstable_batchedUpdates || (batchFn => batchFn());
/**
* Sets the provided batcher function as the batcher function used by Recoil.
*
* Set the batcher to a custom batcher for your renderer,
* if you use a renderer other than React DOM or React Native.
*/
const setBatcher: Batcher => void = (newBatcher: Batcher) => {
batcher = newBatcher;
};
/**
* Returns the current batcher function.
*/
const getBatcher: () => Batcher = () => batcher;
/**
* Calls the current batcher function and passes the
* provided callback function.
*/
const batchUpdates: Callback => void = (callback: Callback) => {
batcher(() => {
let batchEnd = () => undefined;
try {
batchEnd = batchStart();
callback();
} finally {
batchEnd();
}
});
};
module.exports = {
getBatcher,
setBatcher,
batchUpdates,
};

View File

@@ -0,0 +1,263 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {DefaultValue, Trigger} from './Recoil_Node';
import type {RecoilValue} from './Recoil_RecoilValue';
import type {RetainedBy} from './Recoil_RetainedBy';
import type {AtomWrites, NodeKey, Store, TreeState} from './Recoil_State';
const {getNode, getNodeMaybe, recoilValuesForKeys} = require('./Recoil_Node');
const {RetentionZone} = require('./Recoil_RetentionZone');
const {setByAddingToSet} = require('recoil-shared/util/Recoil_CopyOnWrite');
const filterIterable = require('recoil-shared/util/Recoil_filterIterable');
const gkx = require('recoil-shared/util/Recoil_gkx');
const lazyProxy = require('recoil-shared/util/Recoil_lazyProxy');
const mapIterable = require('recoil-shared/util/Recoil_mapIterable');
// flowlint-next-line unclear-type:off
const emptySet: $ReadOnlySet<any> = Object.freeze(new Set());
class ReadOnlyRecoilValueError extends Error {}
function initializeRetentionForNode(
store: Store,
nodeKey: NodeKey,
retainedBy: RetainedBy,
): () => void {
if (!gkx('recoil_memory_managament_2020')) {
return () => undefined;
}
const {nodesRetainedByZone} = store.getState().retention;
function addToZone(zone: RetentionZone) {
let set = nodesRetainedByZone.get(zone);
if (!set) {
nodesRetainedByZone.set(zone, (set = new Set()));
}
set.add(nodeKey);
}
if (retainedBy instanceof RetentionZone) {
addToZone(retainedBy);
} else if (Array.isArray(retainedBy)) {
for (const zone of retainedBy) {
addToZone(zone);
}
}
return () => {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const {retention} = store.getState();
function deleteFromZone(zone: RetentionZone) {
const set = retention.nodesRetainedByZone.get(zone);
set?.delete(nodeKey);
if (set && set.size === 0) {
retention.nodesRetainedByZone.delete(zone);
}
}
if (retainedBy instanceof RetentionZone) {
deleteFromZone(retainedBy);
} else if (Array.isArray(retainedBy)) {
for (const zone of retainedBy) {
deleteFromZone(zone);
}
}
};
}
function initializeNodeIfNewToStore(
store: Store,
treeState: TreeState,
key: NodeKey,
trigger: Trigger,
): void {
const storeState = store.getState();
if (storeState.nodeCleanupFunctions.has(key)) {
return;
}
const node = getNode(key);
const retentionCleanup = initializeRetentionForNode(
store,
key,
node.retainedBy,
);
const nodeCleanup = node.init(store, treeState, trigger);
storeState.nodeCleanupFunctions.set(key, () => {
nodeCleanup();
retentionCleanup();
});
}
function initializeNode(store: Store, key: NodeKey, trigger: Trigger): void {
initializeNodeIfNewToStore(store, store.getState().currentTree, key, trigger);
}
function cleanUpNode(store: Store, key: NodeKey) {
const state = store.getState();
state.nodeCleanupFunctions.get(key)?.();
state.nodeCleanupFunctions.delete(key);
}
// Get the current value loadable of a node and update the state.
// Update dependencies and subscriptions for selectors.
// Update saved value validation for atoms.
function getNodeLoadable<T>(
store: Store,
state: TreeState,
key: NodeKey,
): Loadable<T> {
initializeNodeIfNewToStore(store, state, key, 'get');
return getNode(key).get(store, state);
}
// Peek at the current value loadable for a node without any evaluation or state change
function peekNodeLoadable<T>(
store: Store,
state: TreeState,
key: NodeKey,
): ?Loadable<T> {
return getNode(key).peek(store, state);
}
// Write value directly to state bypassing the Node interface as the node
// definitions may not have been loaded yet when processing the initial snapshot.
function setUnvalidatedAtomValue_DEPRECATED<T>(
state: TreeState,
key: NodeKey,
newValue: T,
): TreeState {
const node = getNodeMaybe(key);
node?.invalidate?.(state);
return {
...state,
atomValues: state.atomValues.clone().delete(key),
nonvalidatedAtoms: state.nonvalidatedAtoms.clone().set(key, newValue),
dirtyAtoms: setByAddingToSet(state.dirtyAtoms, key),
};
}
// Return the discovered dependencies and values to be written by setting
// a node value. (Multiple values may be written due to selectors getting to
// set upstreams; deps may be discovered because of reads in updater functions.)
function setNodeValue<T>(
store: Store,
state: TreeState,
key: NodeKey,
newValue: T | DefaultValue,
): AtomWrites {
const node = getNode(key);
if (node.set == null) {
throw new ReadOnlyRecoilValueError(
`Attempt to set read-only RecoilValue: ${key}`,
);
}
const set = node.set; // so flow doesn't lose the above refinement.
initializeNodeIfNewToStore(store, state, key, 'set');
return set(store, state, newValue);
}
type ComponentInfo = {
name: string,
};
export type RecoilValueInfo<T> = {
loadable: ?Loadable<T>,
isActive: boolean,
isSet: boolean,
isModified: boolean, // TODO report modified selectors
type: 'atom' | 'selector',
deps: Iterable<RecoilValue<mixed>>,
subscribers: {
nodes: Iterable<RecoilValue<mixed>>,
components: Iterable<ComponentInfo>,
},
};
function peekNodeInfo<T>(
store: Store,
state: TreeState,
key: NodeKey,
): RecoilValueInfo<T> {
const storeState = store.getState();
const graph = store.getGraph(state.version);
const type = getNode(key).nodeType;
return lazyProxy(
{
type,
},
{
// $FlowFixMe[underconstrained-implicit-instantiation]
loadable: () => peekNodeLoadable(store, state, key),
isActive: () =>
storeState.knownAtoms.has(key) || storeState.knownSelectors.has(key),
isSet: () => (type === 'selector' ? false : state.atomValues.has(key)),
isModified: () => state.dirtyAtoms.has(key),
// Report current dependencies. If the node hasn't been evaluated, then
// dependencies may be missing based on the current state.
deps: () => recoilValuesForKeys(graph.nodeDeps.get(key) ?? []),
// Reports all "current" subscribers. Evaluating other nodes or
// previous in-progress async evaluations may introduce new subscribers.
subscribers: () => ({
nodes: recoilValuesForKeys(
filterIterable(
getDownstreamNodes(store, state, new Set([key])),
nodeKey => nodeKey !== key,
),
),
components: mapIterable(
storeState.nodeToComponentSubscriptions.get(key)?.values() ?? [],
([name]) => ({name}),
),
}),
},
);
}
// Find all of the recursively dependent nodes
function getDownstreamNodes(
store: Store,
state: TreeState,
keys: $ReadOnlySet<NodeKey> | $ReadOnlyArray<NodeKey>,
): $ReadOnlySet<NodeKey> {
const visitedNodes = new Set<NodeKey>();
const visitingNodes = Array.from(keys);
const graph = store.getGraph(state.version);
for (let key = visitingNodes.pop(); key; key = visitingNodes.pop()) {
visitedNodes.add(key);
const subscribedNodes = graph.nodeToNodeSubscriptions.get(key) ?? emptySet;
for (const downstreamNode of subscribedNodes) {
if (!visitedNodes.has(downstreamNode)) {
visitingNodes.push(downstreamNode);
}
}
}
return visitedNodes;
}
module.exports = {
getNodeLoadable,
peekNodeLoadable,
setNodeValue,
initializeNode,
cleanUpNode,
setUnvalidatedAtomValue_DEPRECATED,
peekNodeInfo,
getDownstreamNodes,
};

134
node_modules/recoil/cjs/core/Recoil_Graph.js.flow generated vendored Normal file
View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {Graph} from './Recoil_GraphTypes';
import type {NodeKey, StateID} from './Recoil_Keys';
import type {Store} from './Recoil_State';
const differenceSets = require('recoil-shared/util/Recoil_differenceSets');
const mapMap = require('recoil-shared/util/Recoil_mapMap');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
function makeGraph(): Graph {
return {
nodeDeps: new Map(),
nodeToNodeSubscriptions: new Map(),
};
}
function cloneGraph(graph: Graph): Graph {
return {
nodeDeps: mapMap(graph.nodeDeps, s => new Set(s)),
nodeToNodeSubscriptions: mapMap(
graph.nodeToNodeSubscriptions,
s => new Set(s),
),
};
}
// Note that this overwrites the deps of existing nodes, rather than unioning
// the new deps with the old deps.
function mergeDepsIntoGraph(
key: NodeKey,
newDeps: $ReadOnlySet<NodeKey>,
graph: Graph,
// If olderGraph is given then we will not overwrite changes made to the given
// graph compared with olderGraph:
olderGraph?: Graph,
): void {
const {nodeDeps, nodeToNodeSubscriptions} = graph;
const oldDeps = nodeDeps.get(key);
if (oldDeps && olderGraph && oldDeps !== olderGraph.nodeDeps.get(key)) {
return;
}
// Update nodeDeps:
nodeDeps.set(key, newDeps);
// Add new deps to nodeToNodeSubscriptions:
const addedDeps =
oldDeps == null ? newDeps : differenceSets(newDeps, oldDeps);
for (const dep of addedDeps) {
if (!nodeToNodeSubscriptions.has(dep)) {
nodeToNodeSubscriptions.set(dep, new Set());
}
const existing = nullthrows(nodeToNodeSubscriptions.get(dep));
existing.add(key);
}
// Remove removed deps from nodeToNodeSubscriptions:
if (oldDeps) {
const removedDeps = differenceSets(oldDeps, newDeps);
for (const dep of removedDeps) {
if (!nodeToNodeSubscriptions.has(dep)) {
return;
}
const existing = nullthrows(nodeToNodeSubscriptions.get(dep));
existing.delete(key);
if (existing.size === 0) {
nodeToNodeSubscriptions.delete(dep);
}
}
}
}
function saveDepsToStore(
key: NodeKey,
deps: $ReadOnlySet<NodeKey>,
store: Store,
version: StateID,
): void {
const storeState = store.getState();
if (
!(
version === storeState.currentTree.version ||
version === storeState.nextTree?.version ||
version === storeState.previousTree?.version
)
) {
recoverableViolation(
'Tried to save dependencies to a discarded tree',
'recoil',
);
}
// Merge the dependencies discovered into the store's dependency map
// for the version that was read:
const graph = store.getGraph(version);
mergeDepsIntoGraph(key, deps, graph);
// If this version is not the latest version, also write these dependencies
// into later versions if they don't already have their own:
if (version === storeState.previousTree?.version) {
const currentGraph = store.getGraph(storeState.currentTree.version);
mergeDepsIntoGraph(key, deps, currentGraph, graph);
}
if (
version === storeState.previousTree?.version ||
version === storeState.currentTree.version
) {
const nextVersion = storeState.nextTree?.version;
if (nextVersion !== undefined) {
const nextGraph = store.getGraph(nextVersion);
mergeDepsIntoGraph(key, deps, nextGraph, graph);
}
}
}
module.exports = {
cloneGraph,
graph: makeGraph,
saveDepsToStore,
};

27
node_modules/recoil/cjs/core/Recoil_GraphTypes.js.flow generated vendored Normal file
View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from './Recoil_Keys';
export type Graph = $ReadOnly<{
// TODO rename these properties to be more descriptive and symetric.
// Upstream Node dependencies
// NOTE: if you ever make the sets in nodeDeps mutable you must change the
// logic in mergeDepsIntoGraph() that relies on reference equality
// of these sets in avoiding overwriting newer deps with older ones.
nodeDeps: Map<NodeKey, $ReadOnlySet<NodeKey>>,
// Downstream Node subscriptions
nodeToNodeSubscriptions: Map<NodeKey, Set<NodeKey>>,
}>;
module.exports = ({}: {...});

31
node_modules/recoil/cjs/core/Recoil_Keys.js.flow generated vendored Normal file
View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
export type NodeKey = string;
export opaque type StateID = number;
export opaque type StoreID = number;
export opaque type ComponentID = number;
let nextTreeStateVersion = 0;
const getNextTreeStateVersion: () => StateID = () => nextTreeStateVersion++;
let nextStoreID = 0;
const getNextStoreID: () => StoreID = () => nextStoreID++;
let nextComponentID = 0;
const getNextComponentID: () => ComponentID = () => nextComponentID++;
module.exports = {
getNextTreeStateVersion,
getNextStoreID,
getNextComponentID,
};

205
node_modules/recoil/cjs/core/Recoil_Node.js.flow generated vendored Normal file
View File

@@ -0,0 +1,205 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {RecoilValue} from './Recoil_RecoilValue';
import type {RetainedBy} from './Recoil_RetainedBy';
import type {AtomWrites, NodeKey, Store, TreeState} from './Recoil_State';
const {isFastRefreshEnabled} = require('./Recoil_ReactMode');
const RecoilValueClasses = require('./Recoil_RecoilValue');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const gkx = require('recoil-shared/util/Recoil_gkx');
const mapIterable = require('recoil-shared/util/Recoil_mapIterable');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const RecoilEnv = require('recoil-shared/util/Recoil_RecoilEnv');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
class DefaultValue {}
const DEFAULT_VALUE: DefaultValue = new DefaultValue();
export type PersistenceType = 'none' | 'url';
export type PersistenceInfo = $ReadOnly<{
type: PersistenceType,
backButton?: boolean,
}>;
export type Trigger = 'get' | 'set';
type NodeType = 'atom' | 'selector';
export type ReadOnlyNodeOptions<T> = $ReadOnly<{
key: NodeKey,
nodeType: NodeType,
// Returns the current value without evaluating or modifying state
peek: (Store, TreeState) => ?Loadable<T>,
// Returns the discovered deps and the loadable value of the node
get: (Store, TreeState) => Loadable<T>,
// Informs the node the first time it is used (either ever or since the node was
// last released). Returns a cleanup function for when the store ceases to be or
// the node is released again.
init: (Store, TreeState, Trigger) => () => void,
// Invalidate the cached value stored in the TreeState.
// It is used at the end of each batch for mutated state.
// This does not affect any other caches such as the selector cache.
invalidate: TreeState => void,
// Clear all internal caches for this node. Unlike "invalidate()" this clears
// the selector cache and clears for all possible dependency values.
clearCache?: (Store, TreeState) => void,
shouldRestoreFromSnapshots: boolean,
dangerouslyAllowMutability?: boolean,
persistence_UNSTABLE?: PersistenceInfo,
// True for members of families, since another node can be created later for the
// same parameter value; but false for individual atoms and selectors which have
// a singleton config passed to us only once when they're defined:
shouldDeleteConfigOnRelease?: () => boolean,
retainedBy: RetainedBy,
}>;
export type ReadWriteNodeOptions<T> = $ReadOnly<{
...ReadOnlyNodeOptions<T>,
// Returns the discovered deps and the set of key-value pairs to be written.
// (Deps may be discovered since selectors get an updater function which has
// the ability to read other atoms, which may have deps.)
set: (
store: Store,
state: TreeState,
newValue: T | DefaultValue,
) => AtomWrites,
}>;
type Node<T> = ReadOnlyNodeOptions<T> | ReadWriteNodeOptions<T>;
// flowlint-next-line unclear-type:off
const nodes: Map<string, Node<any>> = new Map();
// flowlint-next-line unclear-type:off
const recoilValues: Map<string, RecoilValue<any>> = new Map();
/* eslint-disable no-redeclare */
declare function registerNode<T>(
node: ReadWriteNodeOptions<T>,
): RecoilValueClasses.RecoilState<T>;
declare function registerNode<T>(
node: ReadOnlyNodeOptions<T>,
): RecoilValueClasses.RecoilValueReadOnly<T>;
function recoilValuesForKeys(
keys: Iterable<NodeKey>,
): Iterable<RecoilValue<mixed>> {
return mapIterable(keys, key => nullthrows(recoilValues.get(key)));
}
function checkForDuplicateAtomKey(key: string): void {
if (nodes.has(key)) {
const message = `Duplicate atom key "${key}". This is a FATAL ERROR in
production. But it is safe to ignore this warning if it occurred because of
hot module replacement.`;
if (__DEV__) {
// TODO Figure this out for open-source
if (!isFastRefreshEnabled()) {
expectationViolation(message, 'recoil');
}
} else {
// @fb-only: recoverableViolation(message, 'recoil');
console.warn(message); // @oss-only
}
}
}
function registerNode<T>(node: Node<T>): RecoilValue<T> {
if (RecoilEnv.RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED) {
checkForDuplicateAtomKey(node.key);
}
nodes.set(node.key, node);
const recoilValue: RecoilValue<T> =
node.set == null
? new RecoilValueClasses.RecoilValueReadOnly(node.key)
: new RecoilValueClasses.RecoilState(node.key);
recoilValues.set(node.key, recoilValue);
return recoilValue;
}
/* eslint-enable no-redeclare */
class NodeMissingError extends Error {}
// flowlint-next-line unclear-type:off
function getNode(key: NodeKey): Node<any> {
const node = nodes.get(key);
if (node == null) {
throw new NodeMissingError(`Missing definition for RecoilValue: "${key}""`);
}
return node;
}
// flowlint-next-line unclear-type:off
function getNodeMaybe(key: NodeKey): void | Node<any> {
return nodes.get(key);
}
const configDeletionHandlers = new Map<NodeKey, () => void>();
function deleteNodeConfigIfPossible(key: NodeKey): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const node = nodes.get(key);
if (node?.shouldDeleteConfigOnRelease?.()) {
nodes.delete(key);
getConfigDeletionHandler(key)?.();
configDeletionHandlers.delete(key);
}
}
function setConfigDeletionHandler(key: NodeKey, fn: void | (() => void)): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
if (fn === undefined) {
configDeletionHandlers.delete(key);
} else {
configDeletionHandlers.set(key, fn);
}
}
function getConfigDeletionHandler(key: NodeKey): void | (() => void) {
return configDeletionHandlers.get(key);
}
module.exports = {
nodes,
recoilValues,
registerNode,
getNode,
getNodeMaybe,
deleteNodeConfigIfPossible,
setConfigDeletionHandler,
getConfigDeletionHandler,
recoilValuesForKeys,
NodeMissingError,
DefaultValue,
DEFAULT_VALUE,
};

131
node_modules/recoil/cjs/core/Recoil_ReactMode.js.flow generated vendored Normal file
View File

@@ -0,0 +1,131 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const React = require('react');
const gkx = require('recoil-shared/util/Recoil_gkx');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
export opaque type MutableSource = {};
const createMutableSource: <StoreState, Version>(
{current: StoreState},
() => Version,
) => MutableSource =
// flowlint-next-line unclear-type:off
(React: any).createMutableSource ?? (React: any).unstable_createMutableSource;
const useMutableSource: <StoreState, T>(
MutableSource,
() => T,
(StoreState, () => void) => () => void,
) => T =
// flowlint-next-line unclear-type:off
(React: any).useMutableSource ?? (React: any).unstable_useMutableSource;
// https://github.com/reactwg/react-18/discussions/86
const useSyncExternalStore: <T>(
subscribe: (() => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T,
) => T =
// flowlint-next-line unclear-type:off
(React: any).useSyncExternalStore ??
// flowlint-next-line unclear-type:off
(React: any).unstable_useSyncExternalStore;
let ReactRendererVersionMismatchWarnOnce = false;
// Check if the current renderer supports `useSyncExternalStore()`.
// Since React goes through a proxy dispatcher and the current renderer can
// change we can't simply check if `React.useSyncExternalStore()` is defined.
function currentRendererSupportsUseSyncExternalStore(): boolean {
// $FlowFixMe[incompatible-use]
const {ReactCurrentDispatcher, ReactCurrentOwner} =
/* $FlowFixMe[prop-missing] This workaround was approved as a safer mechanism
* to detect if the current renderer supports useSyncExternalStore()
* https://fb.workplace.com/groups/reactjs/posts/9558682330846963/ */
React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
const dispatcher =
ReactCurrentDispatcher?.current ?? ReactCurrentOwner.currentDispatcher;
const isUseSyncExternalStoreSupported =
dispatcher.useSyncExternalStore != null;
if (
useSyncExternalStore &&
!isUseSyncExternalStoreSupported &&
!ReactRendererVersionMismatchWarnOnce
) {
ReactRendererVersionMismatchWarnOnce = true;
recoverableViolation(
'A React renderer without React 18+ API support is being used with React 18+.',
'recoil',
);
}
return isUseSyncExternalStoreSupported;
}
type ReactMode =
| 'TRANSITION_SUPPORT'
| 'SYNC_EXTERNAL_STORE'
| 'MUTABLE_SOURCE'
| 'LEGACY';
/**
* mode: The React API and approach to use for syncing state with React
* early: Re-renders from Recoil updates occur:
* 1) earlier
* 2) in sync with React updates in the same batch
* 3) before transaction observers instead of after.
* concurrent: Is the current mode compatible with Concurrent Mode and useTransition()
*/
function reactMode(): {mode: ReactMode, early: boolean, concurrent: boolean} {
// NOTE: This mode is currently broken with some Suspense cases
// see Recoil_selector-test.js
if (gkx('recoil_transition_support')) {
return {mode: 'TRANSITION_SUPPORT', early: true, concurrent: true};
}
if (gkx('recoil_sync_external_store') && useSyncExternalStore != null) {
return {mode: 'SYNC_EXTERNAL_STORE', early: true, concurrent: false};
}
if (
gkx('recoil_mutable_source') &&
useMutableSource != null &&
typeof window !== 'undefined' &&
!window.$disableRecoilValueMutableSource_TEMP_HACK_DO_NOT_USE
) {
return gkx('recoil_suppress_rerender_in_callback')
? {mode: 'MUTABLE_SOURCE', early: true, concurrent: true}
: {mode: 'MUTABLE_SOURCE', early: false, concurrent: false};
}
return gkx('recoil_suppress_rerender_in_callback')
? {mode: 'LEGACY', early: true, concurrent: false}
: {mode: 'LEGACY', early: false, concurrent: false};
}
// TODO Need to figure out if there is a standard/open-source equivalent to see if hot module replacement is happening:
function isFastRefreshEnabled(): boolean {
// @fb-only: const {isAcceptingUpdate} = require('__debug');
// @fb-only: return typeof isAcceptingUpdate === 'function' && isAcceptingUpdate();
return false; // @oss-only
}
module.exports = {
createMutableSource,
useMutableSource,
useSyncExternalStore,
currentRendererSupportsUseSyncExternalStore,
reactMode,
isFastRefreshEnabled,
};

591
node_modules/recoil/cjs/core/Recoil_RecoilRoot.js.flow generated vendored Normal file
View File

@@ -0,0 +1,591 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey, StateID, StoreID} from './Recoil_Keys';
import type {MutableSource} from './Recoil_ReactMode';
import type {RecoilValue} from './Recoil_RecoilValue';
import type {MutableSnapshot} from './Recoil_Snapshot';
import type {Store, StoreRef, StoreState, TreeState} from './Recoil_State';
// @fb-only: const RecoilusagelogEvent = require('RecoilusagelogEvent');
// @fb-only: const RecoilUsageLogFalcoEvent = require('RecoilUsageLogFalcoEvent');
// @fb-only: const URI = require('URI');
const Queue = require('../adt/Recoil_Queue');
const {
getNextTreeStateVersion,
makeEmptyStoreState,
} = require('../core/Recoil_State');
const {
cleanUpNode,
getDownstreamNodes,
initializeNode,
setNodeValue,
setUnvalidatedAtomValue_DEPRECATED,
} = require('./Recoil_FunctionalCore');
const {graph} = require('./Recoil_Graph');
const {cloneGraph} = require('./Recoil_Graph');
const {getNextStoreID} = require('./Recoil_Keys');
const {createMutableSource, reactMode} = require('./Recoil_ReactMode');
const {applyAtomValueWrites} = require('./Recoil_RecoilValueInterface');
const {releaseScheduledRetainablesNow} = require('./Recoil_Retention');
const {freshSnapshot} = require('./Recoil_Snapshot');
const React = require('react');
const {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} = require('react');
const err = require('recoil-shared/util/Recoil_err');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const gkx = require('recoil-shared/util/Recoil_gkx');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
const unionSets = require('recoil-shared/util/Recoil_unionSets');
const useRefInitOnce = require('recoil-shared/util/Recoil_useRefInitOnce');
type InternalProps = {
initializeState_DEPRECATED?: ({
set: <T>(RecoilValue<T>, T) => void,
setUnvalidatedAtomValues: (Map<string, mixed>) => void,
}) => void,
initializeState?: MutableSnapshot => void,
store_INTERNAL?: Store,
children: React.Node,
};
function notInAContext() {
throw err('This component must be used inside a <RecoilRoot> component.');
}
const defaultStore: Store = Object.freeze({
storeID: getNextStoreID(),
getState: notInAContext,
replaceState: notInAContext,
getGraph: notInAContext,
subscribeToTransactions: notInAContext,
addTransactionMetadata: notInAContext,
});
let stateReplacerIsBeingExecuted: boolean = false;
function startNextTreeIfNeeded(store: Store): void {
if (stateReplacerIsBeingExecuted) {
throw err(
'An atom update was triggered within the execution of a state updater function. State updater functions provided to Recoil must be pure functions.',
);
}
const storeState = store.getState();
if (storeState.nextTree === null) {
if (
gkx('recoil_memory_managament_2020') &&
gkx('recoil_release_on_cascading_update_killswitch_2021')
) {
// If this is a cascading update (that is, rendering due to one state change
// invokes a second state change), we won't have cleaned up retainables yet
// because this normally happens after notifying components. Do it before
// proceeding with the cascading update so that it remains predictable:
if (storeState.commitDepth > 0) {
releaseScheduledRetainablesNow(store);
}
}
const version = storeState.currentTree.version;
const nextVersion = getNextTreeStateVersion();
storeState.nextTree = {
...storeState.currentTree,
version: nextVersion,
stateID: nextVersion,
dirtyAtoms: new Set(),
transactionMetadata: {},
};
storeState.graphsByVersion.set(
nextVersion,
cloneGraph(nullthrows(storeState.graphsByVersion.get(version))),
);
}
}
const AppContext = React.createContext<StoreRef>({current: defaultStore});
const useStoreRef = (): StoreRef => useContext(AppContext);
// $FlowExpectedError[incompatible-call]
const MutableSourceContext = React.createContext<MutableSource>(null);
function useRecoilMutableSource(): MutableSource {
const mutableSource = useContext(MutableSourceContext);
if (mutableSource == null) {
expectationViolation(
'Attempted to use a Recoil hook outside of a <RecoilRoot>. ' +
'<RecoilRoot> must be an ancestor of any component that uses ' +
'Recoil hooks.',
);
}
return mutableSource;
}
function notifyComponents(
store: Store,
storeState: StoreState,
treeState: TreeState,
): void {
const dependentNodes = getDownstreamNodes(
store,
treeState,
treeState.dirtyAtoms,
);
for (const key of dependentNodes) {
const comps = storeState.nodeToComponentSubscriptions.get(key);
if (comps) {
for (const [_subID, [_debugName, callback]] of comps) {
callback(treeState);
}
}
}
}
function sendEndOfBatchNotifications(store: Store) {
const storeState = store.getState();
const treeState = storeState.currentTree;
// Inform transaction subscribers of the transaction:
const dirtyAtoms = treeState.dirtyAtoms;
if (dirtyAtoms.size) {
// Execute Node-specific subscribers before global subscribers
for (const [
key,
subscriptions,
] of storeState.nodeTransactionSubscriptions) {
if (dirtyAtoms.has(key)) {
for (const [_, subscription] of subscriptions) {
subscription(store);
}
}
}
for (const [_, subscription] of storeState.transactionSubscriptions) {
subscription(store);
}
if (!reactMode().early || storeState.suspendedComponentResolvers.size > 0) {
// Notifying components is needed to wake from suspense, even when using
// early rendering.
notifyComponents(store, storeState, treeState);
// Wake all suspended components so the right one(s) can try to re-render.
// We need to wake up components not just when some asynchronous selector
// resolved, but also when changing synchronous values because this may cause
// a selector to change from asynchronous to synchronous, in which case there
// would be no follow-up asynchronous resolution to wake us up.
// TODO OPTIMIZATION Only wake up related downstream components
storeState.suspendedComponentResolvers.forEach(cb => cb());
storeState.suspendedComponentResolvers.clear();
}
}
// Special behavior ONLY invoked by useInterface.
// FIXME delete queuedComponentCallbacks_DEPRECATED when deleting useInterface.
storeState.queuedComponentCallbacks_DEPRECATED.forEach(cb => cb(treeState));
storeState.queuedComponentCallbacks_DEPRECATED.splice(
0,
storeState.queuedComponentCallbacks_DEPRECATED.length,
);
}
function endBatch(store: Store) {
const storeState = store.getState();
storeState.commitDepth++;
try {
const {nextTree} = storeState;
// Ignore commits that are not because of Recoil transactions -- namely,
// because something above RecoilRoot re-rendered:
if (nextTree == null) {
return;
}
// nextTree is now committed -- note that copying and reset occurs when
// a transaction begins, in startNextTreeIfNeeded:
storeState.previousTree = storeState.currentTree;
storeState.currentTree = nextTree;
storeState.nextTree = null;
sendEndOfBatchNotifications(store);
if (storeState.previousTree != null) {
storeState.graphsByVersion.delete(storeState.previousTree.version);
} else {
recoverableViolation(
'Ended batch with no previous state, which is unexpected',
'recoil',
);
}
storeState.previousTree = null;
if (gkx('recoil_memory_managament_2020')) {
// Only release retainables if there were no writes during the end of the
// batch. This avoids releasing something we might be about to use.
if (nextTree == null) {
releaseScheduledRetainablesNow(store);
}
}
} finally {
storeState.commitDepth--;
}
}
/*
* The purpose of the Batcher is to observe when React batches end so that
* Recoil state changes can be batched. Whenever Recoil state changes, we call
* setState on the batcher. Then we wait for that change to be committed, which
* signifies the end of the batch. That's when we respond to the Recoil change.
*/
function Batcher({
setNotifyBatcherOfChange,
}: {
setNotifyBatcherOfChange: (() => void) => void,
}) {
const storeRef = useStoreRef();
const [, setState] = useState(([]: Array<$FlowFixMe>));
// $FlowFixMe[incompatible-call]
setNotifyBatcherOfChange(() => setState({}));
useEffect(() => {
// $FlowFixMe[incompatible-call]
setNotifyBatcherOfChange(() => setState({}));
// If an asynchronous selector resolves after the Batcher is unmounted,
// notifyBatcherOfChange will still be called. An error gets thrown whenever
// setState is called after a component is already unmounted, so this sets
// notifyBatcherOfChange to be a no-op.
return () => {
setNotifyBatcherOfChange(() => {});
};
}, [setNotifyBatcherOfChange]);
useEffect(() => {
// enqueueExecution runs this function immediately; it is only used to
// manipulate the order of useEffects during tests, since React seems to
// call useEffect in an unpredictable order sometimes.
Queue.enqueueExecution('Batcher', () => {
endBatch(storeRef.current);
});
});
return null;
}
if (__DEV__) {
if (typeof window !== 'undefined' && !window.$recoilDebugStates) {
window.$recoilDebugStates = [];
}
}
// When removing this deprecated function, remove stateBySettingRecoilValue
// which will no longer be needed.
function initialStoreState_DEPRECATED(
store: Store,
initializeState: ({
set: <T>(RecoilValue<T>, T) => void,
setUnvalidatedAtomValues: (Map<string, mixed>) => void,
}) => void,
): StoreState {
const initial: StoreState = makeEmptyStoreState();
initializeState({
set: <T>(atom: RecoilValue<T>, value: T) => {
const state = initial.currentTree;
const writes = setNodeValue(store, state, atom.key, value);
const writtenNodes = new Set(writes.keys());
const nonvalidatedAtoms = state.nonvalidatedAtoms.clone();
for (const n of writtenNodes) {
nonvalidatedAtoms.delete(n);
}
initial.currentTree = {
...state,
dirtyAtoms: unionSets(state.dirtyAtoms, writtenNodes),
atomValues: applyAtomValueWrites(state.atomValues, writes), // NB: PLEASE un-export applyAtomValueWrites when deleting this code
nonvalidatedAtoms,
};
},
setUnvalidatedAtomValues: atomValues => {
// FIXME replace this with a mutative loop
atomValues.forEach((v, k) => {
initial.currentTree = setUnvalidatedAtomValue_DEPRECATED(
initial.currentTree,
k,
v,
);
});
},
});
return initial;
}
// Initialize state snapshot for <RecoilRoot> for the initializeState prop.
// Atom effect initialization takes precedence over this prop.
// Any atom effects will be run before initialization, but then cleaned up,
// they are then re-run when used as part of rendering. These semantics are
// compatible with React StrictMode where effects may be re-run multiple times
// but state initialization only happens once the first time.
function initialStoreState(
initializeState: MutableSnapshot => void,
): StoreState {
// Initialize a snapshot and get its store
const snapshot = freshSnapshot(initializeState);
const storeState = snapshot.getStore_INTERNAL().getState();
// Counteract the snapshot auto-release
snapshot.retain();
// Cleanup any effects run during initialization and clear the handlers so
// they will re-initialize if used during rendering. This allows atom effect
// initialization to take precedence over initializeState and be compatible
// with StrictMode semantics.
storeState.nodeCleanupFunctions.forEach(cleanup => cleanup());
storeState.nodeCleanupFunctions.clear();
return storeState;
}
let nextID = 0;
function RecoilRoot_INTERNAL({
initializeState_DEPRECATED,
initializeState,
store_INTERNAL: storeProp, // For use with React "context bridging"
children,
}: InternalProps): React.Node {
// prettier-ignore
// @fb-only: useEffect(() => {
// @fb-only: if (gkx('recoil_usage_logging')) {
// @fb-only: try {
// @fb-only: RecoilUsageLogFalcoEvent.log(() => ({
// @fb-only: type: RecoilusagelogEvent.RECOIL_ROOT_MOUNTED,
// @fb-only: path: URI.getRequestURI().getPath(),
// @fb-only: }));
// @fb-only: } catch {
// @fb-only: recoverableViolation(
// @fb-only: 'Error when logging Recoil Usage event',
// @fb-only: 'recoil',
// @fb-only: );
// @fb-only: }
// @fb-only: }
// @fb-only: }, []);
let storeStateRef: {current: StoreState}; // eslint-disable-line prefer-const
const getGraph = (version: StateID) => {
const graphs = storeStateRef.current.graphsByVersion;
if (graphs.has(version)) {
return nullthrows(graphs.get(version));
}
const newGraph = graph();
graphs.set(version, newGraph);
return newGraph;
};
const subscribeToTransactions = (
callback: Store => void,
key: ?NodeKey,
): ({release: () => void}) => {
if (key == null) {
// Global transaction subscriptions
const {transactionSubscriptions} = storeRef.current.getState();
const id = nextID++;
transactionSubscriptions.set(id, callback);
return {
release: () => {
transactionSubscriptions.delete(id);
},
};
} else {
// Node-specific transaction subscriptions:
const {nodeTransactionSubscriptions} = storeRef.current.getState();
if (!nodeTransactionSubscriptions.has(key)) {
nodeTransactionSubscriptions.set(key, new Map());
}
const id = nextID++;
nullthrows(nodeTransactionSubscriptions.get(key)).set(id, callback);
return {
release: () => {
const subs: ?Map<number, (_0: Store) => void> =
nodeTransactionSubscriptions.get(key);
if (subs) {
subs.delete(id);
if (subs.size === 0) {
nodeTransactionSubscriptions.delete(key);
}
}
},
};
}
};
const addTransactionMetadata = (metadata: {...}) => {
startNextTreeIfNeeded(storeRef.current);
for (const k of Object.keys(metadata)) {
nullthrows(storeRef.current.getState().nextTree).transactionMetadata[k] =
metadata[k];
}
};
const replaceState = (replacer: TreeState => TreeState) => {
startNextTreeIfNeeded(storeRef.current);
// Use replacer to get the next state:
const nextTree = nullthrows(storeStateRef.current.nextTree);
let replaced;
try {
stateReplacerIsBeingExecuted = true;
replaced = replacer(nextTree);
} finally {
stateReplacerIsBeingExecuted = false;
}
if (replaced === nextTree) {
return;
}
if (__DEV__) {
if (typeof window !== 'undefined') {
window.$recoilDebugStates.push(replaced); // TODO this shouldn't happen here because it's not batched
}
}
// Save changes to nextTree and schedule a React update:
storeStateRef.current.nextTree = replaced;
if (reactMode().early) {
notifyComponents(storeRef.current, storeStateRef.current, replaced);
}
nullthrows(notifyBatcherOfChange.current)();
};
const notifyBatcherOfChange = useRef<null | (mixed => void)>(null);
const setNotifyBatcherOfChange = useCallback(
(x: mixed => void) => {
notifyBatcherOfChange.current = x;
},
[notifyBatcherOfChange],
);
const storeRef: {current: Store} = useRefInitOnce(
() =>
storeProp ?? {
storeID: getNextStoreID(),
getState: () => storeStateRef.current,
replaceState,
getGraph,
subscribeToTransactions,
addTransactionMetadata,
},
);
if (storeProp != null) {
storeRef.current = storeProp;
}
storeStateRef = useRefInitOnce(() =>
initializeState_DEPRECATED != null
? initialStoreState_DEPRECATED(
storeRef.current,
initializeState_DEPRECATED,
)
: initializeState != null
? initialStoreState(initializeState)
: makeEmptyStoreState(),
);
const mutableSource = useMemo(
() =>
createMutableSource?.(
storeStateRef,
() => storeStateRef.current.currentTree.version,
),
[storeStateRef],
);
// Cleanup when the <RecoilRoot> is unmounted
useEffect(() => {
// React is free to call effect cleanup handlers and effects at will, the
// deps array is only an optimization. For example, React strict mode
// will execute each effect twice for testing. Therefore, we need symmetry
// to re-initialize all known atoms after they were cleaned up.
const store = storeRef.current;
for (const atomKey of new Set(store.getState().knownAtoms)) {
initializeNode(store, atomKey, 'get');
}
return () => {
for (const atomKey of store.getState().knownAtoms) {
cleanUpNode(store, atomKey);
}
};
}, [storeRef]);
return (
<AppContext.Provider value={storeRef}>
<MutableSourceContext.Provider value={mutableSource}>
<Batcher setNotifyBatcherOfChange={setNotifyBatcherOfChange} />
{children}
</MutableSourceContext.Provider>
</AppContext.Provider>
);
}
type Props =
| {
initializeState_DEPRECATED?: ({
set: <T>(RecoilValue<T>, T) => void,
setUnvalidatedAtomValues: (Map<string, mixed>) => void,
}) => void,
initializeState?: MutableSnapshot => void,
store_INTERNAL?: Store,
override?: true,
children: React.Node,
}
| {
store_INTERNAL?: Store,
/**
* Defaults to true. If override is true, this RecoilRoot will create a
* new Recoil scope. If override is false and this RecoilRoot is nested
* within another RecoilRoot, this RecoilRoot will perform no function.
* Children of this RecoilRoot will access the Recoil values of the
* nearest ancestor RecoilRoot.
*/
override: false,
children: React.Node,
};
function RecoilRoot(props: Props): React.Node {
const {override, ...propsExceptOverride} = props;
const ancestorStoreRef = useStoreRef();
if (override === false && ancestorStoreRef.current !== defaultStore) {
// If ancestorStoreRef.current !== defaultStore, it means that this
// RecoilRoot is not nested within another.
return props.children;
}
return <RecoilRoot_INTERNAL {...propsExceptOverride} />;
}
function useRecoilStoreID(): StoreID {
return useStoreRef().current.storeID;
}
module.exports = {
RecoilRoot,
useStoreRef,
useRecoilMutableSource,
useRecoilStoreID,
notifyComponents_FOR_TESTING: notifyComponents,
sendEndOfBatchNotifications_FOR_TESTING: sendEndOfBatchNotifications,
};

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from './Recoil_State';
// eslint-disable-next-line no-unused-vars
class AbstractRecoilValue<+T> {
key: NodeKey;
constructor(newKey: NodeKey) {
this.key = newKey;
}
toJSON(): {key: string} {
return {key: this.key};
}
}
class RecoilState<T> extends AbstractRecoilValue<T> {}
class RecoilValueReadOnly<+T> extends AbstractRecoilValue<T> {}
export type RecoilValue<T> = RecoilValueReadOnly<T> | RecoilState<T>;
function isRecoilValue(x: mixed): boolean %checks {
return x instanceof RecoilState || x instanceof RecoilValueReadOnly;
}
module.exports = {
AbstractRecoilValue,
RecoilState,
RecoilValueReadOnly,
isRecoilValue,
};

View File

@@ -0,0 +1,378 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {ValueOrUpdater} from '../recoil_values/Recoil_callbackTypes';
import type {
AtomValues,
AtomWrites,
NodeKey,
Store,
TreeState,
} from './Recoil_State';
const {
getDownstreamNodes,
getNodeLoadable,
setNodeValue,
} = require('./Recoil_FunctionalCore');
const {getNextComponentID} = require('./Recoil_Keys');
const {getNode, getNodeMaybe} = require('./Recoil_Node');
const {DefaultValue} = require('./Recoil_Node');
const {reactMode} = require('./Recoil_ReactMode');
const {
AbstractRecoilValue,
RecoilState,
RecoilValueReadOnly,
isRecoilValue,
} = require('./Recoil_RecoilValue');
const {invalidateMemoizedSnapshot} = require('./Recoil_SnapshotCache');
const err = require('recoil-shared/util/Recoil_err');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
function getRecoilValueAsLoadable<T>(
store: Store,
{key}: AbstractRecoilValue<T>,
treeState: TreeState = store.getState().currentTree,
): Loadable<T> {
// Reading from an older tree can cause bugs because the dependencies that we
// discover during the read are lost.
const storeState = store.getState();
if (
!(
treeState.version === storeState.currentTree.version ||
treeState.version === storeState.nextTree?.version ||
treeState.version === storeState.previousTree?.version
)
) {
recoverableViolation('Tried to read from a discarded tree', 'recoil');
}
const loadable = getNodeLoadable<T>(store, treeState, key);
if (loadable.state === 'loading') {
loadable.contents.catch(() => {
/**
* HACK: intercept thrown error here to prevent an uncaught promise exception. Ideally this would happen closer to selector
* execution (perhaps introducing a new ERROR class to be resolved by async selectors that are in an error state)
*/
return;
});
}
return loadable;
}
function applyAtomValueWrites(
atomValues: AtomValues,
writes: AtomWrites,
): AtomValues {
const result = atomValues.clone();
writes.forEach((v, k) => {
if (v.state === 'hasValue' && v.contents instanceof DefaultValue) {
result.delete(k);
} else {
result.set(k, v);
}
});
return result;
}
function valueFromValueOrUpdater<T>(
store: Store,
state: TreeState,
{key}: AbstractRecoilValue<T>,
valueOrUpdater: ValueOrUpdater<T>,
): T | DefaultValue {
if (typeof valueOrUpdater === 'function') {
// Updater form: pass in the current value. Throw if the current value
// is unavailable (namely when updating an async selector that's
// pending or errored):
const current = getNodeLoadable<$FlowFixMe>(store, state, key);
if (current.state === 'loading') {
const msg = `Tried to set atom or selector "${key}" using an updater function while the current state is pending, this is not currently supported.`;
recoverableViolation(msg, 'recoil');
throw err(msg);
} else if (current.state === 'hasError') {
throw current.contents;
}
// T itself may be a function, so our refinement is not sufficient:
return (valueOrUpdater: any)(current.contents); // flowlint-line unclear-type:off
} else {
return valueOrUpdater;
}
}
type Action<T> =
| {
type: 'set',
recoilValue: AbstractRecoilValue<T>,
valueOrUpdater: T | DefaultValue | (T => T | DefaultValue),
}
| {
type: 'setLoadable',
recoilValue: AbstractRecoilValue<T>,
loadable: Loadable<T>,
}
| {
type: 'setUnvalidated',
recoilValue: AbstractRecoilValue<T>,
unvalidatedValue: mixed,
}
| {type: 'markModified', recoilValue: AbstractRecoilValue<T>};
function applyAction(store: Store, state: TreeState, action: Action<mixed>) {
if (action.type === 'set') {
const {recoilValue, valueOrUpdater} = action;
const newValue = valueFromValueOrUpdater(
store,
state,
recoilValue,
valueOrUpdater,
);
const writes = setNodeValue(store, state, recoilValue.key, newValue);
for (const [key, loadable] of writes.entries()) {
writeLoadableToTreeState(state, key, loadable);
}
} else if (action.type === 'setLoadable') {
const {
recoilValue: {key},
loadable,
} = action;
writeLoadableToTreeState(state, key, loadable);
} else if (action.type === 'markModified') {
const {
recoilValue: {key},
} = action;
state.dirtyAtoms.add(key);
} else if (action.type === 'setUnvalidated') {
// Write value directly to state bypassing the Node interface as the node
// definitions may not have been loaded yet when processing the initial snapshot.
const {
recoilValue: {key},
unvalidatedValue,
} = action;
const node = getNodeMaybe(key);
node?.invalidate?.(state);
state.atomValues.delete(key);
state.nonvalidatedAtoms.set(key, unvalidatedValue);
state.dirtyAtoms.add(key);
} else {
recoverableViolation(`Unknown action ${action.type}`, 'recoil');
}
}
function writeLoadableToTreeState(
state: TreeState,
key: NodeKey,
loadable: Loadable<mixed>,
): void {
if (
loadable.state === 'hasValue' &&
loadable.contents instanceof DefaultValue
) {
state.atomValues.delete(key);
} else {
state.atomValues.set(key, loadable);
}
state.dirtyAtoms.add(key);
state.nonvalidatedAtoms.delete(key);
}
function applyActionsToStore(store: Store, actions: Array<Action<mixed>>) {
store.replaceState(state => {
const newState = copyTreeState(state);
for (const action of actions) {
applyAction(store, newState, action);
}
invalidateDownstreams(store, newState);
invalidateMemoizedSnapshot();
return newState;
});
}
function queueOrPerformStateUpdate(store: Store, action: Action<mixed>): void {
if (batchStack.length) {
const actionsByStore = batchStack[batchStack.length - 1];
let actions = actionsByStore.get(store);
if (!actions) {
actionsByStore.set(store, (actions = []));
}
actions.push(action);
} else {
applyActionsToStore(store, [action]);
}
}
const batchStack: Array<Map<Store, Array<Action<mixed>>>> = [];
function batchStart(): () => void {
const actionsByStore = new Map<Store, Array<Action<mixed>>>();
batchStack.push(actionsByStore);
return () => {
for (const [store, actions] of actionsByStore) {
applyActionsToStore(store, actions);
}
const popped = batchStack.pop();
if (popped !== actionsByStore) {
recoverableViolation('Incorrect order of batch popping', 'recoil');
}
};
}
function copyTreeState(state: TreeState): TreeState {
return {
...state,
atomValues: state.atomValues.clone(),
nonvalidatedAtoms: state.nonvalidatedAtoms.clone(),
dirtyAtoms: new Set(state.dirtyAtoms),
};
}
function invalidateDownstreams(store: Store, state: TreeState): void {
// Inform any nodes that were changed or downstream of changes so that they
// can clear out any caches as needed due to the update:
const downstreams = getDownstreamNodes(store, state, state.dirtyAtoms);
for (const key of downstreams) {
getNodeMaybe(key)?.invalidate?.(state);
}
}
function setRecoilValue<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
valueOrUpdater: T | DefaultValue | (T => T | DefaultValue),
): void {
queueOrPerformStateUpdate(store, {
type: 'set',
recoilValue,
valueOrUpdater,
});
}
function setRecoilValueLoadable<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
loadable: DefaultValue | Loadable<T>,
): void {
if (loadable instanceof DefaultValue) {
return setRecoilValue(store, recoilValue, loadable);
}
queueOrPerformStateUpdate(store, {
type: 'setLoadable',
recoilValue,
loadable: (loadable: Loadable<T>),
});
}
function markRecoilValueModified<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
): void {
queueOrPerformStateUpdate(store, {
type: 'markModified',
recoilValue,
});
}
function setUnvalidatedRecoilValue<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
unvalidatedValue: T,
): void {
queueOrPerformStateUpdate(store, {
type: 'setUnvalidated',
recoilValue,
unvalidatedValue,
});
}
export type ComponentSubscription = {release: () => void};
function subscribeToRecoilValue<T>(
store: Store,
{key}: AbstractRecoilValue<T>,
callback: TreeState => void,
componentDebugName: ?string = null,
): ComponentSubscription {
const subID = getNextComponentID();
const storeState = store.getState();
if (!storeState.nodeToComponentSubscriptions.has(key)) {
storeState.nodeToComponentSubscriptions.set(key, new Map());
}
nullthrows(storeState.nodeToComponentSubscriptions.get(key)).set(subID, [
componentDebugName ?? '<not captured>',
callback,
]);
// Handle the case that, during the same tick that we are subscribing, an atom
// has been updated by some effect handler. Otherwise we will miss the update.
const mode = reactMode();
if (
mode.early &&
(mode.mode === 'LEGACY' || mode.mode === 'MUTABLE_SOURCE')
) {
const nextTree = store.getState().nextTree;
if (nextTree && nextTree.dirtyAtoms.has(key)) {
callback(nextTree);
}
}
return {
release: () => {
const releaseStoreState = store.getState();
const subs = releaseStoreState.nodeToComponentSubscriptions.get(key);
if (subs === undefined || !subs.has(subID)) {
recoverableViolation(
`Subscription missing at release time for atom ${key}. This is a bug in Recoil.`,
'recoil',
);
return;
}
subs.delete(subID);
if (subs.size === 0) {
releaseStoreState.nodeToComponentSubscriptions.delete(key);
}
},
};
}
function refreshRecoilValue<T>(
store: Store,
recoilValue: AbstractRecoilValue<T>,
): void {
const {currentTree} = store.getState();
const node = getNode(recoilValue.key);
node.clearCache?.(store, currentTree);
}
module.exports = {
RecoilValueReadOnly,
AbstractRecoilValue,
RecoilState,
getRecoilValueAsLoadable,
setRecoilValue,
setRecoilValueLoadable,
markRecoilValueModified,
setUnvalidatedRecoilValue,
subscribeToRecoilValue,
isRecoilValue,
applyAtomValueWrites, // TODO Remove export when deprecating initialStoreState_DEPRECATED in RecoilRoot
batchStart,
writeLoadableToTreeState,
invalidateDownstreams,
copyTreeState,
refreshRecoilValue,
};

24
node_modules/recoil/cjs/core/Recoil_RetainedBy.js.flow generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {RetentionZone} from './Recoil_RetentionZone';
// This is a separate module to prevent an import cycle.
// Options for how an atom can be retained:
export type RetainedBy =
| 'components' // only retained directly by components
| 'recoilRoot' // lives for the lifetime of the root
| RetentionZone // retained whenever this zone or these zones are retained
| Array<RetentionZone>;
module.exports = undefined;

307
node_modules/recoil/cjs/core/Recoil_Retention.js.flow generated vendored Normal file
View File

@@ -0,0 +1,307 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {NodeKey} from './Recoil_Keys';
import type {RetainedBy} from './Recoil_RetainedBy';
import type {Retainable, Store, StoreState, TreeState} from './Recoil_State';
const {cleanUpNode} = require('./Recoil_FunctionalCore');
const {deleteNodeConfigIfPossible, getNode} = require('./Recoil_Node');
const {RetentionZone} = require('./Recoil_RetentionZone');
const gkx = require('recoil-shared/util/Recoil_gkx');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
const someSet = require('recoil-shared/util/Recoil_someSet');
// Components that aren't mounted after suspending for this long will be assumed
// to be discarded and their resources released.
const SUSPENSE_TIMEOUT_MS = 120000;
const emptySet = new Set<NodeKey>();
function releaseRetainablesNowOnCurrentTree(
store: Store,
retainables: Set<Retainable>,
) {
const storeState = store.getState();
const treeState = storeState.currentTree;
if (storeState.nextTree) {
recoverableViolation(
'releaseNodesNowOnCurrentTree should only be called at the end of a batch',
'recoil',
);
return; // leak memory rather than erase something that's about to be used.
}
const nodes = new Set<NodeKey>();
for (const r of retainables) {
if (r instanceof RetentionZone) {
for (const n of nodesRetainedByZone(storeState, r)) {
nodes.add(n);
}
} else {
nodes.add(r);
}
}
const releasableNodes = findReleasableNodes(store, nodes);
for (const node of releasableNodes) {
releaseNode(store, treeState, node);
}
}
function findReleasableNodes(
store: Store,
searchFromNodes: Set<NodeKey>,
): Set<NodeKey> {
const storeState = store.getState();
const treeState = storeState.currentTree;
const graph = store.getGraph(treeState.version);
const releasableNodes: Set<NodeKey> = new Set(); // mutated to collect answer
const nonReleasableNodes: Set<NodeKey> = new Set();
findReleasableNodesInner(searchFromNodes);
return releasableNodes;
function findReleasableNodesInner(searchFromNodes: Set<NodeKey>): void {
const releasableNodesFoundThisIteration = new Set<NodeKey>();
const downstreams = getDownstreamNodesInTopologicalOrder(
store,
treeState,
searchFromNodes,
releasableNodes, // don't descend into these
nonReleasableNodes, // don't descend into these
);
// Find which of the downstream nodes are releasable and which are not:
for (const node of downstreams) {
// Not releasable if configured to be retained forever:
if (getNode(node).retainedBy === 'recoilRoot') {
nonReleasableNodes.add(node);
continue;
}
// Not releasable if retained directly by a component:
if ((storeState.retention.referenceCounts.get(node) ?? 0) > 0) {
nonReleasableNodes.add(node);
continue;
}
// Not releasable if retained by a zone:
if (
zonesThatCouldRetainNode(node).some(z =>
storeState.retention.referenceCounts.get(z),
)
) {
nonReleasableNodes.add(node);
continue;
}
// Not releasable if it has a non-releasable child (which will already be in
// nonReleasableNodes because we are going in topological order):
const nodeChildren = graph.nodeToNodeSubscriptions.get(node);
if (
nodeChildren &&
someSet(nodeChildren, child => nonReleasableNodes.has(child))
) {
nonReleasableNodes.add(node);
continue;
}
releasableNodes.add(node);
releasableNodesFoundThisIteration.add(node);
}
// If we found any releasable nodes, we need to walk UP from those nodes to
// find whether their parents can now be released as well:
const parents = new Set<NodeKey>();
for (const node of releasableNodesFoundThisIteration) {
for (const parent of graph.nodeDeps.get(node) ?? emptySet) {
if (!releasableNodes.has(parent)) {
parents.add(parent);
}
}
}
if (parents.size) {
findReleasableNodesInner(parents);
}
}
}
// Children before parents
function getDownstreamNodesInTopologicalOrder(
store: Store,
treeState: TreeState,
nodes: Set<NodeKey>, // Mutable set is destroyed in place
doNotDescendInto1: Set<NodeKey>,
doNotDescendInto2: Set<NodeKey>,
): Array<NodeKey> {
const graph = store.getGraph(treeState.version);
const answer = [];
const visited = new Set<NodeKey>();
while (nodes.size > 0) {
visit(nullthrows(nodes.values().next().value));
}
return answer;
function visit(node: NodeKey): void {
if (doNotDescendInto1.has(node) || doNotDescendInto2.has(node)) {
nodes.delete(node);
return;
}
if (visited.has(node)) {
return;
}
const children = graph.nodeToNodeSubscriptions.get(node);
if (children) {
for (const child of children) {
visit(child);
}
}
visited.add(node);
nodes.delete(node);
answer.push(node);
}
}
function releaseNode(store: Store, treeState: TreeState, node: NodeKey) {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
// Atom effects, in-closure caches, etc.:
cleanUpNode(store, node);
// Delete from store state:
const storeState = store.getState();
storeState.knownAtoms.delete(node);
storeState.knownSelectors.delete(node);
storeState.nodeTransactionSubscriptions.delete(node);
storeState.retention.referenceCounts.delete(node);
const zones = zonesThatCouldRetainNode(node);
for (const zone of zones) {
storeState.retention.nodesRetainedByZone.get(zone)?.delete(node);
}
// Note that we DO NOT delete from nodeToComponentSubscriptions because this
// already happens when the last component that was retaining the node unmounts,
// and this could happen either before or after that.
// Delete from TreeState and dep graph:
treeState.atomValues.delete(node);
treeState.dirtyAtoms.delete(node);
treeState.nonvalidatedAtoms.delete(node);
const graph = storeState.graphsByVersion.get(treeState.version);
if (graph) {
const deps = graph.nodeDeps.get(node);
if (deps !== undefined) {
graph.nodeDeps.delete(node);
for (const dep of deps) {
graph.nodeToNodeSubscriptions.get(dep)?.delete(node);
}
}
// No need to delete sub's deps as there should be no subs at this point.
// But an invariant would require deleting nodes in topological order.
graph.nodeToNodeSubscriptions.delete(node);
}
// Node config (for family members only as their configs can be recreated, and
// only if they are not retained within any other Stores):
deleteNodeConfigIfPossible(node);
}
function nodesRetainedByZone(
storeState: StoreState,
zone: RetentionZone,
): Set<NodeKey> {
return storeState.retention.nodesRetainedByZone.get(zone) ?? emptySet;
}
function zonesThatCouldRetainNode(node: NodeKey): Array<RetentionZone> {
const retainedBy = getNode(node).retainedBy;
if (
retainedBy === undefined ||
retainedBy === 'components' ||
retainedBy === 'recoilRoot'
) {
return [];
} else if (retainedBy instanceof RetentionZone) {
return [retainedBy];
} else {
return retainedBy; // it's an array of zones
}
}
function scheduleOrPerformPossibleReleaseOfRetainable(
store: Store,
retainable: Retainable,
) {
const state = store.getState();
if (state.nextTree) {
state.retention.retainablesToCheckForRelease.add(retainable);
} else {
releaseRetainablesNowOnCurrentTree(store, new Set([retainable]));
}
}
function updateRetainCount(
store: Store,
retainable: Retainable,
delta: 1 | -1,
): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const map = store.getState().retention.referenceCounts;
const newCount = (map.get(retainable) ?? 0) + delta;
if (newCount === 0) {
updateRetainCountToZero(store, retainable);
} else {
map.set(retainable, newCount);
}
}
function updateRetainCountToZero(store: Store, retainable: Retainable): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const map = store.getState().retention.referenceCounts;
map.delete(retainable);
scheduleOrPerformPossibleReleaseOfRetainable(store, retainable);
}
function releaseScheduledRetainablesNow(store: Store) {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const state = store.getState();
releaseRetainablesNowOnCurrentTree(
store,
state.retention.retainablesToCheckForRelease,
);
state.retention.retainablesToCheckForRelease.clear();
}
function retainedByOptionWithDefault(r: RetainedBy | void): RetainedBy {
// The default will change from 'recoilRoot' to 'components' in the future.
return r === undefined ? 'recoilRoot' : r;
}
module.exports = {
SUSPENSE_TIMEOUT_MS,
updateRetainCount,
updateRetainCountToZero,
releaseScheduledRetainablesNow,
retainedByOptionWithDefault,
};

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
class RetentionZone {}
function retentionZone(): RetentionZone {
return new RetentionZone();
}
module.exports = {
RetentionZone,
retentionZone,
};

430
node_modules/recoil/cjs/core/Recoil_Snapshot.js.flow generated vendored Normal file
View File

@@ -0,0 +1,430 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {
ResetRecoilState,
SetRecoilState,
ValueOrUpdater,
} from '../recoil_values/Recoil_callbackTypes';
import type {RecoilValueInfo} from './Recoil_FunctionalCore';
import type {Graph} from './Recoil_GraphTypes';
import type {NodeKey, StoreID} from './Recoil_Keys';
import type {RecoilState, RecoilValue} from './Recoil_RecoilValue';
import type {StateID, Store, StoreState, TreeState} from './Recoil_State';
const {batchUpdates} = require('./Recoil_Batching');
const {initializeNode, peekNodeInfo} = require('./Recoil_FunctionalCore');
const {graph} = require('./Recoil_Graph');
const {getNextStoreID} = require('./Recoil_Keys');
const {
DEFAULT_VALUE,
recoilValues,
recoilValuesForKeys,
} = require('./Recoil_Node');
const {
AbstractRecoilValue,
getRecoilValueAsLoadable,
setRecoilValue,
setUnvalidatedRecoilValue,
} = require('./Recoil_RecoilValueInterface');
const {updateRetainCount} = require('./Recoil_Retention');
const {setInvalidateMemoizedSnapshot} = require('./Recoil_SnapshotCache');
const {
getNextTreeStateVersion,
makeEmptyStoreState,
} = require('./Recoil_State');
const concatIterables = require('recoil-shared/util/Recoil_concatIterables');
const {isSSR} = require('recoil-shared/util/Recoil_Environment');
const err = require('recoil-shared/util/Recoil_err');
const filterIterable = require('recoil-shared/util/Recoil_filterIterable');
const gkx = require('recoil-shared/util/Recoil_gkx');
const mapIterable = require('recoil-shared/util/Recoil_mapIterable');
const {
memoizeOneWithArgsHashAndInvalidation,
} = require('recoil-shared/util/Recoil_Memoize');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
// Opaque at this surface because it's part of the public API from here.
export type SnapshotID = StateID;
const retainWarning = `
Recoil Snapshots only last for the duration of the callback they are provided to. To keep a Snapshot longer, do this:
const release = snapshot.retain();
try {
await doSomethingWithSnapshot(snapshot);
} finally {
release();
}
This is currently a DEV-only warning but will become a thrown exception in the next release of Recoil.
`;
// A "Snapshot" is "read-only" and captures a specific set of values of atoms.
// However, the data-flow-graph and selector values may evolve as selector
// evaluation functions are executed and async selectors resolve.
class Snapshot {
// eslint-disable-next-line fb-www/no-uninitialized-properties
_store: Store;
_refCount: number = 1;
constructor(storeState: StoreState, parentStoreID?: StoreID) {
this._store = {
storeID: getNextStoreID(),
parentStoreID,
getState: () => storeState,
replaceState: replacer => {
// no batching, so nextTree is never active
storeState.currentTree = replacer(storeState.currentTree);
},
getGraph: version => {
const graphs = storeState.graphsByVersion;
if (graphs.has(version)) {
return nullthrows(graphs.get(version));
}
const newGraph = graph();
graphs.set(version, newGraph);
return newGraph;
},
subscribeToTransactions: () => ({release: () => {}}),
addTransactionMetadata: () => {
throw err('Cannot subscribe to Snapshots');
},
};
// Initialize any nodes that are live in the parent store (primarily so that
// this snapshot gets counted towards the node's live stores count).
// TODO Optimize this when cloning snapshots for callbacks
for (const nodeKey of this._store.getState().knownAtoms) {
initializeNode(this._store, nodeKey, 'get');
updateRetainCount(this._store, nodeKey, 1);
}
this.autoRelease_INTERNAL();
}
retain(): () => void {
if (this._refCount <= 0) {
if (__DEV__) {
throw err('Snapshot has already been released.');
} else {
recoverableViolation(
'Attempt to retain() Snapshot that was already released.',
'recoil',
);
}
}
this._refCount++;
let released = false;
return () => {
if (!released) {
released = true;
this._release();
}
};
}
/**
* Release the snapshot on the next tick. This means the snapshot is retained
* during the execution of the current function using it.
*/
autoRelease_INTERNAL(): void {
if (!isSSR) {
// Use timeout of 10 to workaround Firefox issue: https://github.com/facebookexperimental/Recoil/issues/1936
window.setTimeout(() => this._release(), 10);
}
}
_release(): void {
this._refCount--;
if (this._refCount === 0) {
this._store.getState().nodeCleanupFunctions.forEach(cleanup => cleanup());
this._store.getState().nodeCleanupFunctions.clear();
if (!gkx('recoil_memory_managament_2020')) {
return;
}
// Temporarily nerfing this to allow us to find broken call sites without
// actually breaking anybody yet.
// for (const k of this._store.getState().knownAtoms) {
// updateRetainCountToZero(this._store, k);
// }
} else if (this._refCount < 0) {
if (__DEV__) {
recoverableViolation('Snapshot released an extra time.', 'recoil');
}
}
}
isRetained(): boolean {
return this._refCount > 0;
}
checkRefCount_INTERNAL(): void {
if (gkx('recoil_memory_managament_2020') && this._refCount <= 0) {
if (__DEV__) {
recoverableViolation(retainWarning, 'recoil');
}
// What we will ship later:
// throw err(retainWarning);
}
}
getStore_INTERNAL(): Store {
this.checkRefCount_INTERNAL();
return this._store;
}
getID(): SnapshotID {
this.checkRefCount_INTERNAL();
return this._store.getState().currentTree.stateID;
}
getStoreID(): StoreID {
this.checkRefCount_INTERNAL();
return this._store.storeID;
}
// We want to allow the methods to be destructured and used as accessors
/* eslint-disable fb-www/extra-arrow-initializer */
getLoadable: <T>(RecoilValue<T>) => Loadable<T> = <T>(
recoilValue: RecoilValue<T>,
): Loadable<T> => {
this.checkRefCount_INTERNAL();
return getRecoilValueAsLoadable(this._store, recoilValue);
};
getPromise: <T>(RecoilValue<T>) => Promise<T> = <T>(
recoilValue: RecoilValue<T>,
): Promise<T> => {
this.checkRefCount_INTERNAL();
return this.getLoadable(recoilValue).toPromise();
};
getNodes_UNSTABLE: (
{
isModified?: boolean,
isInitialized?: boolean,
} | void,
) => Iterable<RecoilValue<mixed>> = opt => {
this.checkRefCount_INTERNAL();
// TODO Deal with modified selectors
if (opt?.isModified === true) {
if (opt?.isInitialized === false) {
return [];
}
const state = this._store.getState().currentTree;
return recoilValuesForKeys(state.dirtyAtoms);
}
const knownAtoms = this._store.getState().knownAtoms;
const knownSelectors = this._store.getState().knownSelectors;
return opt?.isInitialized == null
? recoilValues.values()
: opt.isInitialized === true
? recoilValuesForKeys(concatIterables([knownAtoms, knownSelectors]))
: filterIterable(
recoilValues.values(),
({key}) => !knownAtoms.has(key) && !knownSelectors.has(key),
);
};
// Report the current status of a node.
// This peeks the current state and does not affect the snapshot state at all
getInfo_UNSTABLE: <T>(RecoilValue<T>) => RecoilValueInfo<T> = <T>({
key,
}: RecoilValue<T>): RecoilValueInfo<T> => {
this.checkRefCount_INTERNAL();
return peekNodeInfo(this._store, this._store.getState().currentTree, key);
};
map: ((MutableSnapshot) => void) => Snapshot = mapper => {
this.checkRefCount_INTERNAL();
const mutableSnapshot = new MutableSnapshot(this, batchUpdates);
mapper(mutableSnapshot); // if removing batchUpdates from `set` add it here
return mutableSnapshot;
};
asyncMap: ((MutableSnapshot) => Promise<void>) => Promise<Snapshot> =
async mapper => {
this.checkRefCount_INTERNAL();
const mutableSnapshot = new MutableSnapshot(this, batchUpdates);
mutableSnapshot.retain(); // Retain new snapshot during async mapper
await mapper(mutableSnapshot);
// Continue to retain the new snapshot for the user, but auto-release it
// after the next tick, the same as a new synchronous snapshot.
mutableSnapshot.autoRelease_INTERNAL();
return mutableSnapshot;
};
/* eslint-enable fb-www/extra-arrow-initializer */
}
function cloneStoreState(
store: Store,
treeState: TreeState,
bumpVersion: boolean = false,
): StoreState {
const storeState = store.getState();
const version = bumpVersion ? getNextTreeStateVersion() : treeState.version;
return {
// Always clone the TreeState to isolate stores from accidental mutations.
// For example, reading a selector from a cloned snapshot shouldn't cache
// in the original treestate which may cause the original to skip
// initialization of upstream atoms.
currentTree: {
// TODO snapshots shouldn't really have versions because a new version number
// is always assigned when the snapshot is gone to.
version: bumpVersion ? version : treeState.version,
stateID: bumpVersion ? version : treeState.stateID,
transactionMetadata: {...treeState.transactionMetadata},
dirtyAtoms: new Set(treeState.dirtyAtoms),
atomValues: treeState.atomValues.clone(),
nonvalidatedAtoms: treeState.nonvalidatedAtoms.clone(),
},
commitDepth: 0,
nextTree: null,
previousTree: null,
knownAtoms: new Set(storeState.knownAtoms), // FIXME here's a copy
knownSelectors: new Set(storeState.knownSelectors), // FIXME here's a copy
transactionSubscriptions: new Map(),
nodeTransactionSubscriptions: new Map(),
nodeToComponentSubscriptions: new Map(),
queuedComponentCallbacks_DEPRECATED: [],
suspendedComponentResolvers: new Set(),
graphsByVersion: new Map<StateID, Graph>().set(
version,
store.getGraph(treeState.version),
),
retention: {
referenceCounts: new Map(),
nodesRetainedByZone: new Map(),
retainablesToCheckForRelease: new Set(),
},
// FIXME here's a copy
// Create blank cleanup handlers for atoms so snapshots don't re-run
// atom effects.
nodeCleanupFunctions: new Map(
mapIterable(storeState.nodeCleanupFunctions.entries(), ([key]) => [
key,
() => {},
]),
),
};
}
// Factory to build a fresh snapshot
function freshSnapshot(initializeState?: MutableSnapshot => void): Snapshot {
const snapshot = new Snapshot(makeEmptyStoreState());
return initializeState != null ? snapshot.map(initializeState) : snapshot;
}
// Factory to clone a snapshot state
const [memoizedCloneSnapshot, invalidateMemoizedSnapshot] =
memoizeOneWithArgsHashAndInvalidation(
// $FlowFixMe[missing-local-annot]
(store, version) => {
const storeState = store.getState();
const treeState =
version === 'latest'
? storeState.nextTree ?? storeState.currentTree
: nullthrows(storeState.previousTree);
return new Snapshot(cloneStoreState(store, treeState), store.storeID);
},
(store, version) =>
String(version) +
String(store.storeID) +
String(store.getState().nextTree?.version) +
String(store.getState().currentTree.version) +
String(store.getState().previousTree?.version),
);
// Avoid circular dependencies
setInvalidateMemoizedSnapshot(invalidateMemoizedSnapshot);
function cloneSnapshot(
store: Store,
version: 'latest' | 'previous' = 'latest',
): Snapshot {
const snapshot = memoizedCloneSnapshot(store, version);
if (!snapshot.isRetained()) {
invalidateMemoizedSnapshot();
return memoizedCloneSnapshot(store, version);
}
return snapshot;
}
class MutableSnapshot extends Snapshot {
_batch: (() => void) => void;
constructor(snapshot: Snapshot, batch: (() => void) => void) {
super(
cloneStoreState(
snapshot.getStore_INTERNAL(),
snapshot.getStore_INTERNAL().getState().currentTree,
true,
),
snapshot.getStoreID(),
);
this._batch = batch;
}
set: SetRecoilState = <T>(
recoilState: RecoilState<T>,
newValueOrUpdater: ValueOrUpdater<T>,
) => {
this.checkRefCount_INTERNAL();
const store = this.getStore_INTERNAL();
// This batchUpdates ensures this `set` is applied immediately and you can
// read the written value after calling `set`. I would like to remove this
// behavior and only batch in `Snapshot.map`, but this would be a breaking
// change potentially.
this._batch(() => {
updateRetainCount(store, recoilState.key, 1);
setRecoilValue(this.getStore_INTERNAL(), recoilState, newValueOrUpdater);
});
};
reset: ResetRecoilState = <T>(recoilState: RecoilState<T>) => {
this.checkRefCount_INTERNAL();
const store = this.getStore_INTERNAL();
// See note at `set` about batched updates.
this._batch(() => {
updateRetainCount(store, recoilState.key, 1);
setRecoilValue(this.getStore_INTERNAL(), recoilState, DEFAULT_VALUE);
});
};
setUnvalidatedAtomValues_DEPRECATED: (Map<NodeKey, mixed>) => void = (
values: Map<NodeKey, mixed>,
) => {
this.checkRefCount_INTERNAL();
const store = this.getStore_INTERNAL();
// See note at `set` about batched updates.
batchUpdates(() => {
for (const [k, v] of values.entries()) {
updateRetainCount(store, k, 1);
setUnvalidatedRecoilValue(store, new AbstractRecoilValue(k), v);
}
});
};
}
module.exports = {
Snapshot,
MutableSnapshot,
freshSnapshot,
cloneSnapshot,
};

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
let _invalidateMemoizedSnapshot: ?() => void = null;
function setInvalidateMemoizedSnapshot(invalidate: () => void): void {
_invalidateMemoizedSnapshot = invalidate;
}
function invalidateMemoizedSnapshot(): void {
_invalidateMemoizedSnapshot?.();
}
module.exports = {
setInvalidateMemoizedSnapshot,
invalidateMemoizedSnapshot,
};

184
node_modules/recoil/cjs/core/Recoil_State.js.flow generated vendored Normal file
View File

@@ -0,0 +1,184 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {PersistentMap} from '../adt/Recoil_PersistentMap';
import type {Graph} from './Recoil_GraphTypes';
import type {ComponentID, NodeKey, StateID, StoreID} from './Recoil_Keys';
import type {RetentionZone} from './Recoil_RetentionZone';
const {persistentMap} = require('../adt/Recoil_PersistentMap');
const {graph} = require('./Recoil_Graph');
const {getNextTreeStateVersion} = require('./Recoil_Keys');
export type {ComponentID, NodeKey, StateID, StoreID} from './Recoil_Keys';
// flowlint-next-line unclear-type:off
export type AtomValues = PersistentMap<NodeKey, Loadable<any>>;
// flowlint-next-line unclear-type:off
export type AtomWrites = Map<NodeKey, Loadable<any>>;
type ComponentCallback = TreeState => void;
export type Retainable = RetentionZone | NodeKey;
// TreeState represents the state of a rendered React tree. As such, multiple
// TreeStates may be in play at one time due to concurrent rendering, and each
// TreeState is immutable.
export type TreeState = $ReadOnly<{
// Version always increments when moving from one state to another, even
// if the same state has been seen before.
version: StateID,
// State ID usually increments, but when going to a snapshot that was
// previously rendered the state ID will be re-used:
stateID: StateID,
transactionMetadata: {...},
// Atoms:
dirtyAtoms: Set<NodeKey>,
atomValues: AtomValues,
nonvalidatedAtoms: PersistentMap<NodeKey, mixed>,
}>;
// StoreState represents the state of a Recoil context. It is global and mutable.
// It is updated only during effects, except that the nextTree property is updated
// when atom values change and async requests resolve, and suspendedComponentResolvers
// is updated when components are suspended.
export type StoreState = {
// The "current" TreeState being either directly read from (legacy) or passed
// to useMutableSource (when in use). It is replaced with nextTree when
// a transaction is completed or async request finishes:
currentTree: TreeState,
// The TreeState that is written to when during the course of a transaction
// (generally equal to a React batch) when atom values are updated.
nextTree: null | TreeState,
// This TreeState exists only during the time that components and observers
// are being notified of a newly-committed tree:
previousTree: null | TreeState,
// Incremented when finishing a batch; used to detect cascading updates.
commitDepth: number,
// Node lifetimes
knownAtoms: Set<NodeKey>,
knownSelectors: Set<NodeKey>,
+retention: {
referenceCounts: Map<NodeKey | RetentionZone, number>,
nodesRetainedByZone: Map<RetentionZone, Set<NodeKey>>,
retainablesToCheckForRelease: Set<Retainable>,
},
// Between the time a component is first used and when it is released,
// there will be a function in this map that cleans up the node upon release
// (or upon root unmount).
+nodeCleanupFunctions: Map<NodeKey, () => void>,
// Which components depend on a specific node. (COMMIT/SUSPEND updates).
+nodeToComponentSubscriptions: Map<
NodeKey,
Map<ComponentID, [string, ComponentCallback]>,
>,
// Which nodes depend on which. A pure function of the version (atom state)
// and nodeToComponentSubscriptions. Recomputed when:
// (1) A transaction occurs (atoms written) or
// (2) An async request is completed or
// (3) (IN FUTURE) nodeToComponentSubscriptions is updated
// How incremental computation is performed:
// In case of transactions, we walk downward from the updated atoms
// In case of async request completion, we walk downward from updated selector
// In (future) case of component subscriptions updated, we walk upwards from
// component and then downward from any no-longer-depended on nodes
+graphsByVersion: Map<StateID, Graph>,
// Side note: it would be useful to consider async request completion as
// another type of transaction since it should increase version etc. and many
// things have to happen in both of these cases.
// For observing transactions:
+transactionSubscriptions: Map<number, (Store) => void>,
+nodeTransactionSubscriptions: Map<NodeKey, Map<number, (Store) => void>>,
// Callbacks to render external components that are subscribed to nodes
// These are executed at the end of the transaction or asynchronously.
// FIXME remove when removing useInterface
+queuedComponentCallbacks_DEPRECATED: Array<ComponentCallback>,
// Promise resolvers to wake any components we suspended with React Suspense
+suspendedComponentResolvers: Set<() => void>,
};
// The Store is just the interface that is made available via the context.
// It is constant within a given Recoil root.
export type Store = $ReadOnly<{
storeID: StoreID,
parentStoreID?: StoreID,
getState: () => StoreState,
replaceState: ((TreeState) => TreeState) => void,
getGraph: StateID => Graph,
subscribeToTransactions: ((Store) => void, ?NodeKey) => {release: () => void},
addTransactionMetadata: ({...}) => void,
}>;
export type StoreRef = {
current: Store,
};
function makeEmptyTreeState(): TreeState {
const version = getNextTreeStateVersion();
return {
version,
stateID: version,
transactionMetadata: {},
dirtyAtoms: new Set(),
atomValues: persistentMap(),
nonvalidatedAtoms: persistentMap(),
};
}
function makeEmptyStoreState(): StoreState {
const currentTree = makeEmptyTreeState();
return {
currentTree,
nextTree: null,
previousTree: null,
commitDepth: 0,
knownAtoms: new Set(),
knownSelectors: new Set(),
transactionSubscriptions: new Map(),
nodeTransactionSubscriptions: new Map(),
nodeToComponentSubscriptions: new Map(),
queuedComponentCallbacks_DEPRECATED: [],
suspendedComponentResolvers: new Set(),
graphsByVersion: new Map<StateID, Graph>().set(
currentTree.version,
graph(),
),
retention: {
referenceCounts: new Map(),
nodesRetainedByZone: new Map(),
retainablesToCheckForRelease: new Set(),
},
nodeCleanupFunctions: new Map(),
};
}
module.exports = {
makeEmptyTreeState,
makeEmptyStoreState,
getNextTreeStateVersion,
};

View File

@@ -0,0 +1,476 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Store} from '../Recoil_State';
import type {MutableSnapshot} from 'Recoil_Snapshot';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
useSetRecoilState,
atom,
constSelector,
selector,
asyncSelector,
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
RecoilRoot,
useStoreRef;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
({useSetRecoilState} = require('../../hooks/Recoil_Hooks'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
asyncSelector,
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({RecoilRoot, useStoreRef} = require('../Recoil_RecoilRoot'));
});
describe('initializeState', () => {
testRecoil('initialize atom', () => {
const myAtom = atom({
key: 'RecoilRoot - initializeState - atom',
default: 'DEFAULT',
});
const mySelector = constSelector(myAtom);
function initializeState({set, getLoadable}: MutableSnapshot) {
expect(getLoadable(myAtom).contents).toEqual('DEFAULT');
expect(getLoadable(mySelector).contents).toEqual('DEFAULT');
set(myAtom, 'INITIALIZE');
expect(getLoadable(myAtom).contents).toEqual('INITIALIZE');
expect(getLoadable(mySelector).contents).toEqual('INITIALIZE');
}
const container = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadsAtom atom={myAtom} />
<ReadsAtom atom={mySelector} />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"');
});
testRecoil('initialize selector', () => {
const myAtom = atom({
key: 'RecoilRoot - initializeState - selector',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'RecoilRoot - initializeState - selector selector',
get: ({get}) => get(myAtom),
// $FlowFixMe[incompatible-call]
set: ({set}, newValue) => set(myAtom, newValue),
});
function initializeState({set, getLoadable}: MutableSnapshot) {
expect(getLoadable(myAtom).contents).toEqual('DEFAULT');
expect(getLoadable(mySelector).contents).toEqual('DEFAULT');
set(mySelector, 'INITIALIZE');
expect(getLoadable(myAtom).contents).toEqual('INITIALIZE');
expect(getLoadable(mySelector).contents).toEqual('INITIALIZE');
}
const container = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadsAtom atom={myAtom} />
<ReadsAtom atom={mySelector} />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"');
});
testRecoil(
'Atom Effects run with global initialization',
async ({strictMode, concurrentMode}) => {
let effectRan = 0;
let effectCleanup = 0;
const myAtom = atom<string>({
key: 'RecoilRoot - initializeState - atom effects',
default: 'DEFAULT',
effects: [
({setSelf}) => {
effectRan++;
setSelf('EFFECT');
return () => {
effectCleanup++;
};
},
],
});
function initializeState({set}: MutableSnapshot) {
set(myAtom, current => {
// Effects are run first
expect(current).toEqual('EFFECT');
return 'INITIALIZE';
});
}
expect(effectRan).toEqual(0);
const container1 = renderElements(
<RecoilRoot initializeState={initializeState}>NO READ</RecoilRoot>,
);
// Effects are run when initialized with initializeState, even if not read.
// Effects are run twice, once before initializeState, then again when rendering.
expect(container1.textContent).toEqual('NO READ');
expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2);
// Auto-release of the initializing snapshot
await flushPromisesAndTimers();
expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1);
// Test again when atom is actually used by the root
effectRan = 0;
effectCleanup = 0;
const container2 = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadsAtom atom={myAtom} />
</RecoilRoot>,
);
// Effects takes precedence
expect(container2.textContent).toEqual('"EFFECT"');
expect(effectRan).toEqual(strictMode ? (concurrentMode ? 4 : 3) : 2);
await flushPromisesAndTimers();
expect(effectCleanup).toEqual(strictMode ? (concurrentMode ? 3 : 2) : 1);
},
);
testRecoil(
'onSet() called when atom initialized with initializeState',
() => {
const setValues = [];
const myAtom = atom({
key: 'RecoilRoot - initializeState - onSet',
default: 0,
effects: [
({onSet, setSelf}) => {
onSet(value => {
setValues.push(value);
// Confirm setSelf() works when initialized with initializeState
setSelf(value + 1);
});
},
],
});
const [MyAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(
<RecoilRoot initializeState={({set}) => set(myAtom, 1)}>
<MyAtom />
</RecoilRoot>,
);
expect(c.textContent).toBe('1');
expect(setValues).toEqual([]);
act(() => setAtom(2));
expect(setValues).toEqual([2]);
expect(c.textContent).toBe('3');
},
);
testRecoil(
'Selectors from global initialization are not canceled',
async () => {
const [asyncSel, resolve] = asyncSelector<string, _>();
// $FlowFixMe[incompatible-call]
const depSel = selector({
key: 'RecoilRoot - initializeSTate - async selector',
get: ({get}) => get(asyncSel),
});
const container = renderUnwrappedElements(
<RecoilRoot
// Call initializeState to force a snapshot to be mapped
initializeState={({getLoadable}) => {
getLoadable(asyncSel);
getLoadable(depSel);
}}>
<React.Suspense fallback="loading">
<ReadsAtom atom={asyncSel} />
<ReadsAtom atom={depSel} />
</React.Suspense>
</RecoilRoot>,
);
expect(container.textContent).toEqual('loading');
// Wait for any potential auto-release of initializing snapshot
await flushPromisesAndTimers();
// Ensure that async selectors resolve and are not canceled
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(container.textContent).toEqual('"RESOLVE""RESOLVE"');
},
);
testRecoil('initialize with nested store', () => {
const GetStore = ({children}: {children: Store => React.Node}) => {
return children(useStoreRef().current);
};
const container = renderElements(
<RecoilRoot>
<GetStore>
{storeA => (
<RecoilRoot store_INTERNAL={storeA}>
<GetStore>
{storeB => {
expect(storeA === storeB).toBe(true);
return 'NESTED_ROOT/';
}}
</GetStore>
</RecoilRoot>
)}
</GetStore>
ROOT
</RecoilRoot>,
);
expect(container.textContent).toEqual('NESTED_ROOT/ROOT');
});
testRecoil('initializeState is only called once', ({strictMode}) => {
if (strictMode) {
return;
}
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const initializeState = jest.fn(({set}) => set(myAtom, 'INIT'));
let forceUpdate: () => void = () => {
throw new Error('not rendered');
};
let setRootKey: number => void = _ => {
throw new Error('');
};
function MyRoot() {
const [counter, setCounter] = useState(0);
forceUpdate = () => setCounter(counter + 1);
const [key, setKey] = useState(0);
setRootKey = setKey;
return (
<RecoilRoot key={key} initializeState={initializeState}>
{counter}
<ReadsWritesAtom />
</RecoilRoot>
);
}
const container = renderElements(<MyRoot />);
expect(container.textContent).toEqual('0"INIT"');
act(forceUpdate);
expect(initializeState).toHaveBeenCalledTimes(1);
expect(container.textContent).toEqual('1"INIT"');
act(() => setAtom('SET'));
expect(initializeState).toHaveBeenCalledTimes(1);
expect(container.textContent).toEqual('1"SET"');
act(forceUpdate);
expect(initializeState).toHaveBeenCalledTimes(1);
expect(container.textContent).toEqual('2"SET"');
act(() => setRootKey(1));
expect(initializeState).toHaveBeenCalledTimes(2);
expect(container.textContent).toEqual('2"INIT"');
});
});
testRecoil(
'Impure state updater functions that trigger atom updates are detected',
() => {
// This test ensures that we throw a clean error rather than mysterious breakage
// if the user supplies a state updater function that triggers another update
// within its execution. These state updater functions are supposed to be pure.
// We can't detect all forms of impurity but this one in particular will make
// Recoil break, so we detect it and throw an error.
const atomA = atom({
key: 'RecoilRoot/impureUpdater/a',
default: 0,
});
const atomB = atom({
key: 'RecoilRoot/impureUpdater/b',
default: 0,
});
let update;
function Component() {
const updateA = useSetRecoilState(atomA);
const updateB = useSetRecoilState(atomB);
update = () => {
updateA(() => {
updateB(1);
return 1;
});
};
return null;
}
renderElements(<Component />);
expect(() =>
act(() => {
update();
}),
).toThrow('pure function');
},
);
describe('override prop', () => {
testRecoil(
'RecoilRoots create a new Recoil scope when override is true or undefined',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot>
<ReadsAtom atom={myAtom} />
<RecoilRoot>
<ReadsWritesAtom />
</RecoilRoot>
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"DEFAULT""SET"');
},
);
testRecoil(
'A RecoilRoot performs no function if override is false and it has an ancestor RecoilRoot',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot>
<ReadsAtom atom={myAtom} />
<RecoilRoot override={false}>
<ReadsAtom atom={myAtom} />
<RecoilRoot override={false}>
<ReadsWritesAtom />
</RecoilRoot>
</RecoilRoot>
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT""DEFAULT""DEFAULT"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"SET""SET""SET"');
},
);
testRecoil(
'Unmounting a nested RecoilRoot with override set to false does not clean up ancestor Recoil atoms',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
let setRenderNestedRoot;
const NestedRootContainer = () => {
const [renderNestedRoot, _setRenderNestedRoot] = useState(true);
setRenderNestedRoot = _setRenderNestedRoot;
return (
renderNestedRoot && (
<RecoilRoot override={false}>
<ReadsWritesAtom />
</RecoilRoot>
)
);
};
const container = renderElements(
<RecoilRoot>
<ReadsAtom atom={myAtom} />
<NestedRootContainer />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET'));
act(() => setRenderNestedRoot(false));
expect(container.textContent).toEqual('"SET"');
},
);
testRecoil(
'A RecoilRoot functions normally if override is false and it does not have an ancestor RecoilRoot',
() => {
const myAtom = atom({
key: 'RecoilRoot/override/atom',
default: 'DEFAULT',
});
const [ReadsWritesAtom, setAtom] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot override={false}>
<ReadsWritesAtom />
</RecoilRoot>,
);
expect(container.textContent).toEqual('"DEFAULT"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"SET"');
},
);
});

View File

@@ -0,0 +1,221 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let act,
atom,
selector,
getRecoilValueAsLoadable,
setRecoilValue,
setUnvalidatedRecoilValue,
subscribeToRecoilValue,
refreshRecoilValue,
a,
dependsOnAFn,
dependsOnA,
dependsOnDependsOnA,
b,
store;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
getRecoilValueAsLoadable,
setRecoilValue,
setUnvalidatedRecoilValue,
subscribeToRecoilValue,
refreshRecoilValue,
} = require('../Recoil_RecoilValueInterface'));
a = atom<number>({key: 'a', default: 0});
dependsOnAFn = jest.fn(x => x + 1);
dependsOnA = selector({
key: 'dependsOnA',
get: ({get}) => dependsOnAFn(get(a)),
});
dependsOnDependsOnA = selector({
key: 'dependsOnDependsOnA',
// $FlowFixMe[unsafe-addition]
get: ({get}) => get(dependsOnA) + 1,
});
b = atom<number>({
key: 'b',
default: 0,
persistence_UNSTABLE: {
type: 'url',
validator: x => parseInt(x, 10),
},
});
store = makeStore();
});
testRecoil('read default value', () => {
expect(getRecoilValueAsLoadable(store, a)).toMatchObject({
state: 'hasValue',
contents: 0,
});
});
testRecoil('read written value, visited contains written value', () => {
setRecoilValue(store, a, 1);
expect(getRecoilValueAsLoadable(store, a)).toMatchObject({
state: 'hasValue',
contents: 1,
});
});
testRecoil('read selector based on default upstream', () => {
expect(getRecoilValueAsLoadable(store, dependsOnA).contents).toEqual(1);
});
testRecoil('read selector based on written upstream', () => {
setRecoilValue(store, a, 1);
expect(getRecoilValueAsLoadable(store, dependsOnA).contents).toEqual(2);
});
testRecoil('selector subscriber is called when upstream changes', () => {
const callback = jest.fn();
const {release} = subscribeToRecoilValue(store, dependsOnA, callback);
getRecoilValueAsLoadable(store, dependsOnA);
expect(callback).toHaveBeenCalledTimes(0);
setRecoilValue(store, a, 1);
expect(callback).toHaveBeenCalledTimes(1);
release();
setRecoilValue(store, a, 2);
expect(callback).toHaveBeenCalledTimes(1);
});
testRecoil(
'selector is recursively visited when subscribed and upstream changes',
() => {
const callback = jest.fn();
const {release} = subscribeToRecoilValue(
store,
dependsOnDependsOnA,
callback,
);
getRecoilValueAsLoadable(store, dependsOnDependsOnA);
expect(callback).toHaveBeenCalledTimes(0);
setRecoilValue(store, a, 1);
expect(callback).toHaveBeenCalledTimes(1);
release();
setRecoilValue(store, a, 2);
expect(callback).toHaveBeenCalledTimes(1);
},
);
testRecoil('selector function is evaluated only on first read', () => {
dependsOnAFn.mockClear();
const callback = jest.fn();
subscribeToRecoilValue(store, dependsOnA, callback);
getRecoilValueAsLoadable(store, dependsOnA);
expect(dependsOnAFn).toHaveBeenCalledTimes(1); // called once on initial read
act(() => setRecoilValue(store, a, 1337)); // input number must not be used in any other test due to selector-internal caching
getRecoilValueAsLoadable(store, dependsOnA);
expect(dependsOnAFn).toHaveBeenCalledTimes(2); // called again on read following upstream change
getRecoilValueAsLoadable(store, dependsOnA);
expect(dependsOnAFn).toHaveBeenCalledTimes(2); // not called on subsequent read with no upstream change
});
testRecoil('selector cache refresh', () => {
const getA = jest.fn(() => 'A');
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilRefresher ancestors A',
get: getA,
});
const getB = jest.fn(({get}) => get(selectorA) + 'B');
const selectorB = selector({
key: 'useRecoilRefresher ancestors B',
get: getB,
});
const getC = jest.fn(({get}) => get(selectorB) + 'C');
const selectorC = selector({
key: 'useRecoilRefresher ancestors C',
get: getC,
});
expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
act(() => {
refreshRecoilValue(store, selectorC);
});
expect(getRecoilValueAsLoadable(store, selectorC).contents).toEqual('ABC');
expect(getC).toHaveBeenCalledTimes(2);
expect(getB).toHaveBeenCalledTimes(2);
expect(getA).toHaveBeenCalledTimes(2);
});
testRecoil('atom can go from unvalidated to normal value', () => {
setUnvalidatedRecoilValue(store, b, '1');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 1,
});
setRecoilValue(store, b, 2);
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 2,
});
});
testRecoil('atom can go from normal to unvalidated value', () => {
setRecoilValue(store, b, 1);
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 1,
});
setUnvalidatedRecoilValue(store, b, '2');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 2,
});
});
testRecoil('atom can go from unvalidated to unvalidated value', () => {
// Regression test for an issue where setting an unvalidated value when
// already in a has-unvalidated-value state would result in a stale value:
setUnvalidatedRecoilValue(store, b, '1');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 1,
});
setUnvalidatedRecoilValue(store, b, '2');
expect(getRecoilValueAsLoadable(store, b)).toMatchObject({
state: 'hasValue',
contents: 2,
});
});

View File

@@ -0,0 +1,518 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilState} from '../../core/Recoil_RecoilValue';
import type {RetentionZone} from 'Recoil_RetentionZone';
const {
getRecoilTestFn,
} = require('recoil-shared/__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('recoil-shared/__test_utils__/Recoil_TestingUtils'));
gkx = require('recoil-shared/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;
function atomRetainedBy(
retainedBy:
| void
| RetentionZone
| $TEMPORARY$string<'components'>
| $TEMPORARY$array<RetentionZone>,
) {
return atom({
key: `retention/${nextKey++}`,
default: 0,
retainedBy_UNSTABLE: retainedBy,
});
}
function switchComponent(defaultVisible: boolean) {
let innerSetVisible = (_: boolean) => undefined;
const setVisible = (v: boolean) => innerSetVisible(v); // acts like a ref basically
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function Switch({children}) {
let visible;
[visible, innerSetVisible] = useState(defaultVisible);
return visible ? children : null;
}
return [Switch, setVisible];
}
// 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.
function testWhetherAtomIsRetained(
shouldBeRetained: boolean,
node: RecoilState<number>,
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
otherChildren = null,
): void {
const [AtomSwitch, setAtomVisible] = switchComponent(false);
const [OtherChildrenSwitch, setOtherChildrenVisible] = switchComponent(false);
const [ReadsAtomComp, updateAtom] = componentThatReadsAndWritesAtom(node);
const container = renderElements(
<>
<AtomSwitch>
<ReadsAtomComp />
</AtomSwitch>
<OtherChildrenSwitch>{otherChildren}</OtherChildrenSwitch>
</>,
);
expect(container.textContent).toEqual('');
act(() => {
setAtomVisible(true);
setOtherChildrenVisible(true);
});
expect(container.textContent).toEqual('0');
act(() => updateAtom(1));
expect(container.textContent).toEqual('1');
act(() => setAtomVisible(false));
expect(container.textContent).toEqual('');
act(() => setAtomVisible(true));
if (shouldBeRetained) {
expect(container.textContent).toEqual('1');
} else {
expect(container.textContent).toEqual('0');
}
if (otherChildren) {
act(() => {
setAtomVisible(false);
setOtherChildrenVisible(false);
});
expect(container.textContent).toEqual('');
act(() => setAtomVisible(true));
expect(container.textContent).toEqual('0'); // Not expected for root-retained but this doesn't occur in these tests
}
}
describe('Default retention', () => {
testRecoil(
'By default, atoms are retained for the lifetime of the root',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
testWhetherAtomIsRetained(true, atomRetainedBy(undefined));
},
);
});
describe('Component-level retention', () => {
testRecoil(
'With retainedBy: components, atoms are released when not in use',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
testWhetherAtomIsRetained(false, atomRetainedBy('components'));
},
);
testRecoil(
'An atom is retained by a component being subscribed to it',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const anAtom = atomRetainedBy('components');
function Subscribes() {
useRecoilValue(anAtom);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <Subscribes />);
},
);
testRecoil(
'An atom is retained by a component retaining it explicitly',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const anAtom = atomRetainedBy('components');
function Retains() {
useRetain(anAtom);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <Retains />);
},
);
});
describe('RetentionZone retention', () => {
testRecoil('An atom can be retained via a retention zone', ({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const zone = retentionZone();
const anAtom = atomRetainedBy(zone);
function RetainsZone() {
useRetain(zone);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <RetainsZone />);
});
});
describe('Retention of and via selectors', () => {
testRecoil(
'An atom is retained when a depending selector is retained',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const anAtom = atomRetainedBy('components');
// $FlowFixMe[incompatible-call]
const aSelector = selector({
key: '...',
retainedBy_UNSTABLE: 'components',
get: ({get}) => {
return get(anAtom);
},
});
function SubscribesToSelector() {
useRecoilValue(aSelector);
return null;
}
testWhetherAtomIsRetained(true, anAtom, <SubscribesToSelector />);
},
);
const flushPromises = async () =>
await act(() => new Promise(window.setImmediate));
testRecoil(
'An async selector is not released when its only subscribed component suspends',
async ({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
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;
});
},
});
function SubscribesToSelector() {
// $FlowFixMe[incompatible-type]
return useRecoilValue(aSelector);
}
// $FlowFixMe[incompatible-type-arg]
const c = renderElements(<SubscribesToSelector />);
expect(c.textContent).toEqual('loading');
expect(evalCount).toBe(1);
// $FlowFixMe[incompatible-call]
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 ({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
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;
});
},
});
function SubscribesToSelector() {
// Test without using Suspense to avoid complications with Jest promises
// and timeouts when using Suspense. This doesn't affect what's under test.
const l = useRecoilValueLoadable(aSelector);
// $FlowFixMe[incompatible-type]
return l.state === 'loading' ? 'loading' : l.getValue();
}
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
// $FlowFixMe[incompatible-call]
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');
// $FlowFixMe[incompatible-call]
act(() => resolve(123));
await flushPromises();
expect(c.textContent).toEqual('123');
},
);
testRecoil(
'Selector changing deps releases old deps, retains new ones',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const switchAtom = atom({
key: 'switch',
default: false,
});
const depA = atomRetainedBy('components');
const depB = atomRetainedBy('components');
// $FlowFixMe[incompatible-call]
const theSelector = selector({
key: 'sel',
get: ({get}) => {
if (get(switchAtom)) {
return get(depB);
} else {
return get(depA);
}
},
retainedBy_UNSTABLE: 'components',
});
let setup;
function Setup() {
setup = useRecoilCallback(({set}) => () => {
set(depA, 123);
set(depB, 456);
});
return null;
}
function ReadsSelector() {
useRecoilValue(theSelector);
return null;
}
let depAValue;
function ReadsDepA() {
depAValue = useRecoilValue(depA);
return null;
}
let depBValue;
function ReadsDepB() {
depBValue = useRecoilValue(depB);
return null;
}
const [MountSwitch, setAtomsMountedDirectly] = switchComponent(true);
function unmountAndRemount() {
act(() => setAtomsMountedDirectly(false));
act(() => setAtomsMountedDirectly(true));
}
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',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
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',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const zoneA = retentionZone();
const zoneB = retentionZone();
const anAtom = atomRetainedBy([zoneA, zoneB]);
function RetainsZone({zone}: $TEMPORARY$object<{zone: RetentionZone}>) {
useRetain(zone);
return null;
}
// 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',
({strictMode}) => {
// TODO Retention does not work properly in strict mode
if (strictMode) {
return;
}
const zone = retentionZone();
const anAtom = atomRetainedBy(zone);
function RetainsZone() {
useRetain(zone);
return null;
}
function RetainsAtom() {
useRetain(anAtom);
return null;
}
// It's the no-longer-retained-when-unmounting-otherChildren part that is
// important for this test.
testWhetherAtomIsRetained(
true,
anAtom,
<>
<RetainsZone />
<RetainsAtom />
</>,
);
},
);
});

View File

@@ -0,0 +1,687 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Snapshot} from '../Recoil_Snapshot';
import type {StateID} from 'Recoil_Keys';
import type {RecoilState, RecoilValueReadOnly} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
useState,
useGotoRecoilSnapshot,
useRecoilTransactionObserver,
atom,
constSelector,
selector,
ReadsAtom,
flushPromisesAndTimers,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
freshSnapshot,
RecoilRoot;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = React);
({act} = require('ReactTestUtils'));
({
useGotoRecoilSnapshot,
useRecoilTransactionObserver,
} = require('../../hooks/Recoil_SnapshotHooks'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
flushPromisesAndTimers,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({freshSnapshot} = require('../Recoil_Snapshot'));
({RecoilRoot} = require('../Recoil_RecoilRoot'));
});
// Use this to spread proxy results into an object for Jest's toMatchObject()
function getInfo(
snapshot: Snapshot,
node: RecoilState<string> | RecoilValueReadOnly<string>,
) {
return {...snapshot.getInfo_UNSTABLE(node)};
}
// Test first since we are testing all registered nodes
testRecoil('getNodes', () => {
const snapshot = freshSnapshot();
const {getNodes_UNSTABLE} = snapshot;
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(0);
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
0,
);
// expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0);
// Test atoms
const myAtom = atom({key: 'snapshot getNodes atom', default: 'DEFAULT'});
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(1);
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
0,
);
expect(snapshot.getLoadable(myAtom).contents).toEqual('DEFAULT');
const nodesAfterGet = Array.from(getNodes_UNSTABLE());
expect(nodesAfterGet.length).toEqual(1);
expect(nodesAfterGet[0]).toBe(myAtom);
expect(snapshot.getLoadable(nodesAfterGet[0]).contents).toEqual('DEFAULT');
// Test selectors
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'snapshot getNodes selector',
get: ({get}) => get(myAtom) + '-SELECTOR',
});
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2);
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
1,
);
expect(snapshot.getLoadable(mySelector).contents).toEqual('DEFAULT-SELECTOR');
expect(Array.from(getNodes_UNSTABLE({isInitialized: true})).length).toEqual(
2,
);
// expect(Array.from(getNodes_UNSTABLE({types: ['atom']})).length).toEqual(1);
// const selectorNodes = Array.from(getNodes_UNSTABLE({types: ['selector']}));
// expect(selectorNodes.length).toEqual(1);
// expect(selectorNodes[0]).toBe(mySelector);
// Test dirty atoms
expect(Array.from(getNodes_UNSTABLE()).length).toEqual(2);
// expect(Array.from(getNodes_UNSTABLE({isSet: true})).length).toEqual(0);
expect(
Array.from(snapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(0);
const updatedSnapshot = snapshot.map(({set}) => set(myAtom, 'SET'));
expect(
Array.from(snapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(0);
expect(
Array.from(updatedSnapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(1);
// expect(
// Array.from(snapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(0);
// expect(
// Array.from(updatedSnapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(1);
const dirtyAtom = Array.from(
updatedSnapshot.getNodes_UNSTABLE({isModified: true}),
)[0];
expect(snapshot.getLoadable(dirtyAtom).contents).toEqual('DEFAULT');
expect(updatedSnapshot.getLoadable(dirtyAtom).contents).toEqual('SET');
// Test reset
const resetSnapshot = updatedSnapshot.map(({reset}) => reset(myAtom));
expect(
Array.from(resetSnapshot.getNodes_UNSTABLE({isModified: true})).length,
).toEqual(1);
// expect(
// Array.from(resetSnapshot.getNodes_UNSTABLE({isSet: true})).length,
// ).toEqual(0);
// TODO Test dirty selectors
});
testRecoil(
'State ID after going to snapshot matches the ID of the snapshot',
() => {
const seenIDs = new Set<StateID>();
const snapshots = [];
let expectedSnapshotID = null;
const myAtom = atom({key: 'Snapshot ID atom', default: 0});
const mySelector = constSelector(myAtom); // For read-only testing below
const transactionObserver = ({
snapshot,
}: {
previousSnapshot: Snapshot,
snapshot: Snapshot,
}) => {
const snapshotID = snapshot.getID();
if (expectedSnapshotID != null) {
expect(seenIDs.has(snapshotID)).toBe(true);
expect(snapshotID).toBe(expectedSnapshotID);
} else {
expect(seenIDs.has(snapshotID)).toBe(false);
}
seenIDs.add(snapshotID);
snapshot.retain();
snapshots.push({snapshotID, snapshot});
};
function TransactionObserver() {
useRecoilTransactionObserver(transactionObserver);
return null;
}
let gotoSnapshot;
function GotoSnapshot() {
gotoSnapshot = useGotoRecoilSnapshot();
return null;
}
const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(
<>
<TransactionObserver />
<GotoSnapshot />
<WriteAtom />
<ReadsAtom atom={mySelector} />
</>,
);
expect(c.textContent).toBe('00');
// Test changing state produces a new state version
act(() => setAtom(1));
act(() => setAtom(2));
expect(snapshots.length).toBe(2);
expect(seenIDs.size).toBe(2);
// Test going to a previous snapshot re-uses the state ID
expectedSnapshotID = snapshots[0].snapshotID;
act(() => gotoSnapshot(snapshots[0].snapshot));
// Test changing state after going to a previous snapshot uses a new version
expectedSnapshotID = null;
act(() => setAtom(3));
// Test mutating a snapshot creates a new version
const transactionSnapshot = snapshots[0].snapshot.map(({set}) => {
set(myAtom, 4);
});
act(() => gotoSnapshot(transactionSnapshot));
expect(seenIDs.size).toBe(4);
expect(snapshots.length).toBe(5);
// Test that added read-only selector doesn't cause an issue getting the
// current version to see the current deps of the selector since we mutated a
// state after going to a snapshot, so that version may not be known by the store.
// If there was a problem, then the component may throw an error when evaluating the selector.
expect(c.textContent).toBe('44');
},
);
testRecoil('Read default loadable from snapshot', () => {
const snapshot: Snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Atom Default',
default: 'DEFAULT',
});
const atomLoadable = snapshot.getLoadable(myAtom);
expect(atomLoadable.state).toEqual('hasValue');
expect(atomLoadable.contents).toEqual('DEFAULT');
const mySelector = constSelector(myAtom);
const selectorLoadable = snapshot.getLoadable(mySelector);
expect(selectorLoadable.state).toEqual('hasValue');
expect(selectorLoadable.contents).toEqual('DEFAULT');
});
testRecoil('Read async selector from snapshot', async () => {
const snapshot = freshSnapshot();
const otherA = freshSnapshot();
const otherB = freshSnapshot();
const [asyncSel, resolve] = asyncSelector<string, _>();
const nestSel = constSelector(asyncSel);
expect(snapshot.getLoadable(asyncSel).state).toEqual('loading');
expect(snapshot.getLoadable(nestSel).state).toEqual('loading');
expect(otherA.getLoadable(nestSel).state).toEqual('loading');
const otherC = snapshot.map(() => {});
// eslint-disable-next-line jest/valid-expect
const ptest = expect(snapshot.getPromise(asyncSel)).resolves.toEqual(
'SET VALUE',
);
act(() => resolve('SET VALUE'));
await ptest;
await expect(snapshot.getPromise(asyncSel)).resolves.toEqual('SET VALUE');
expect(snapshot.getLoadable(asyncSel).contents).toEqual('SET VALUE');
await expect(snapshot.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherA.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherB.getPromise(nestSel)).resolves.toEqual('SET VALUE');
await expect(otherC.getPromise(nestSel)).resolves.toEqual('SET VALUE');
});
testRecoil('Sync map of snapshot', () => {
const snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Map Sync',
default: 'DEFAULT',
});
const mySelector = constSelector(myAtom);
const atomLoadable = snapshot.getLoadable(myAtom);
expect(atomLoadable.state).toEqual('hasValue');
expect(atomLoadable.contents).toEqual('DEFAULT');
const selectorLoadable = snapshot.getLoadable(mySelector);
expect(selectorLoadable.state).toEqual('hasValue');
expect(selectorLoadable.contents).toEqual('DEFAULT');
const setSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET');
});
const setAtomLoadable = setSnapshot.getLoadable(myAtom);
expect(setAtomLoadable.state).toEqual('hasValue');
expect(setAtomLoadable.contents).toEqual('SET');
const setSelectorLoadable = setSnapshot.getLoadable(myAtom);
expect(setSelectorLoadable.state).toEqual('hasValue');
expect(setSelectorLoadable.contents).toEqual('SET');
const resetSnapshot = snapshot.map(({reset}) => {
reset(myAtom);
});
const resetAtomLoadable = resetSnapshot.getLoadable(myAtom);
expect(resetAtomLoadable.state).toEqual('hasValue');
expect(resetAtomLoadable.contents).toEqual('DEFAULT');
const resetSelectorLoadable = resetSnapshot.getLoadable(myAtom);
expect(resetSelectorLoadable.state).toEqual('hasValue');
expect(resetSelectorLoadable.contents).toEqual('DEFAULT');
});
testRecoil('Async map of snapshot', async () => {
const snapshot = freshSnapshot();
const myAtom = atom({
key: 'Snapshot Map Async',
default: 'DEFAULT',
});
const [beforeAsyncSel, resolveBeforeMap] = asyncSelector<string, _>();
const [duringAsyncSel, resolveDuringMap] = asyncSelector<string, _>();
const [afterAsyncSel, resolveAfterMap] = asyncSelector<string, _>();
const depAsyncSel = selector({
key: 'snapshot asyncMap dep selector',
get: () => afterAsyncSel,
});
resolveBeforeMap('BEFORE');
const newSnapshotPromise = snapshot.asyncMap(async ({getPromise, set}) => {
await expect(getPromise(beforeAsyncSel)).resolves.toBe('BEFORE');
await expect(getPromise(duringAsyncSel)).resolves.toBe('DURING');
// Test that depAsyncSel is first used while mapping the snapshot.
// If the snapshot is auto-released too early the async selector will be
// canceled.
getPromise(depAsyncSel);
// Test that mapped snapshot is not auto-released too early
await flushPromisesAndTimers();
set(myAtom, 'VALUE');
});
resolveDuringMap('DURING');
const newSnapshot = await newSnapshotPromise;
expect(newSnapshot.isRetained()).toBe(true);
resolveAfterMap('AFTER');
await expect(newSnapshot.getPromise(myAtom)).resolves.toBe('VALUE');
await expect(newSnapshot.getPromise(beforeAsyncSel)).resolves.toBe('BEFORE');
await expect(newSnapshot.getPromise(duringAsyncSel)).resolves.toBe('DURING');
await expect(newSnapshot.getPromise(afterAsyncSel)).resolves.toBe('AFTER');
await expect(newSnapshot.getPromise(depAsyncSel)).resolves.toBe('AFTER');
});
testRecoil('getInfo', () => {
const snapshot = freshSnapshot();
const myAtom = atom<string>({
key: 'snapshot getInfo atom',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'getInfo A',
get: ({get}) => get(myAtom),
});
const selectorB = selector({
key: 'getInfo B',
get: ({get}) => get(selectorA) + get(myAtom),
});
// Initial status
expect(getInfo(snapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: false,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getInfo(snapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(snapshot, myAtom).subscribers.nodes)).toEqual([]);
expect(getInfo(snapshot, selectorA)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(snapshot, selectorA).deps)).toEqual([]);
expect(Array.from(getInfo(snapshot, selectorA).subscribers.nodes)).toEqual(
[],
);
// $FlowFixMe[incompatible-call]
expect(getInfo(snapshot, selectorB)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).deps)).toEqual([]);
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).subscribers.nodes)).toEqual(
[],
);
// After reading values
snapshot.getLoadable(selectorB);
expect(getInfo(snapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getInfo(snapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(snapshot, myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
expect(getInfo(snapshot, selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(snapshot, selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getInfo(snapshot, selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
// $FlowFixMe[incompatible-call]
expect(getInfo(snapshot, selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(snapshot, selectorB).subscribers.nodes)).toEqual(
[],
);
// After setting a value
const setSnapshot = snapshot.map(({set}) => set(myAtom, 'SET'));
setSnapshot.getLoadable(selectorB); // Read value to prime
expect(getInfo(setSnapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: true,
isModified: true,
type: 'atom',
});
expect(Array.from(getInfo(setSnapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(setSnapshot, myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
expect(getInfo(setSnapshot, selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(setSnapshot, selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getInfo(setSnapshot, selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
// $FlowFixMe[incompatible-call]
expect(getInfo(setSnapshot, selectorB)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SETSET'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(setSnapshot, selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(setSnapshot, selectorB).subscribers.nodes)).toEqual(
[],
);
// After reseting a value
const resetSnapshot = setSnapshot.map(({reset}) => reset(myAtom));
resetSnapshot.getLoadable(selectorB); // prime snapshot
expect(getInfo(resetSnapshot, myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: true,
type: 'atom',
});
expect(Array.from(getInfo(resetSnapshot, myAtom).deps)).toEqual([]);
expect(Array.from(getInfo(resetSnapshot, myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
expect(getInfo(resetSnapshot, selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'DEFAULT'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getInfo(resetSnapshot, selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(
Array.from(getInfo(resetSnapshot, selectorA).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorB]));
// $FlowFixMe[incompatible-call]
expect(getInfo(resetSnapshot, selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getInfo(resetSnapshot, selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
expect(
// $FlowFixMe[incompatible-call]
Array.from(getInfo(resetSnapshot, selectorB).subscribers.nodes),
).toEqual([]);
});
describe('Retention', () => {
testRecoil('auto-release', async () => {
const snapshot = freshSnapshot();
expect(snapshot.isRetained()).toBe(true);
await flushPromisesAndTimers();
expect(snapshot.isRetained()).toBe(false);
const devStatus = window.__DEV__;
window.__DEV__ = true;
expect(() => snapshot.retain()).toThrow('released');
window.__DEV__ = false;
expect(() => snapshot.retain()).not.toThrow('released');
window.__DEV__ = devStatus;
// TODO enable when recoil_memory_managament_2020 is enforced
// expect(() => snapshot.getID()).toThrow('release');
});
testRecoil('retain()', async () => {
const snapshot = freshSnapshot();
expect(snapshot.isRetained()).toBe(true);
const release2 = snapshot.retain();
await flushPromisesAndTimers();
expect(snapshot.isRetained()).toBe(true);
release2();
expect(snapshot.isRetained()).toBe(false);
});
});
describe('Atom effects', () => {
testRecoil('Standalone snapshot', async ({gks}) => {
let effectsRefCount = 0;
const myAtom = atom({
key: 'snapshot effects standalone',
default: 'DEFAULT',
effects: [
({setSelf}) => {
effectsRefCount++;
setSelf('INIT');
return () => {
effectsRefCount--;
};
},
],
});
expect(effectsRefCount).toBe(0);
const fresh = freshSnapshot();
expect(fresh.getLoadable(myAtom).getValue()).toBe('INIT');
expect(effectsRefCount).toBe(1);
// Auto-release snapshot
await flushPromisesAndTimers();
expect(effectsRefCount).toBe(0);
});
testRecoil('RecoilRoot Snapshot', () => {
let effectsRefCount = 0;
const myAtom = atom({
key: 'snapshot effects RecoilRoot',
default: 'DEFAULT',
effects: [
({setSelf}) => {
effectsRefCount++;
setSelf('INIT');
return () => {
effectsRefCount--;
};
},
],
});
let setMount: boolean => void = () => {
throw new Error('Test Error');
};
function Component() {
const [mount, setState] = useState(false);
setMount = setState;
return mount ? (
<RecoilRoot>
<ReadsAtom atom={myAtom} />
</RecoilRoot>
) : (
'UNMOUNTED'
);
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('UNMOUNTED');
expect(effectsRefCount).toBe(0);
act(() => setMount(true));
expect(c.textContent).toBe('"INIT"');
expect(effectsRefCount).toBe(1);
act(() => setMount(false));
expect(c.textContent).toBe('UNMOUNTED');
expect(effectsRefCount).toBe(0);
});
testRecoil('getStoreID()', () => {
const myAtom = atom({
key: 'snapshot effects storeID',
default: 'DEFAULT',
effects: [
({setSelf, storeID}) => {
// $FlowFixMe[incompatible-call]
setSelf(storeID);
},
],
});
const testSnapshot = freshSnapshot();
expect(testSnapshot.getLoadable(myAtom).getValue()).toBe(
testSnapshot.getStoreID(),
);
});
testRecoil('Parent StoreID', () => {
const myAtom = atom({
key: 'snapshot effects parentStoreID',
effects: [
// $FlowFixMe[missing-local-annot]
({storeID, parentStoreID_UNSTABLE, setSelf}) => {
setSelf({storeID, parentStoreID: parentStoreID_UNSTABLE});
},
],
});
const testSnapshot = freshSnapshot();
const mappedSnapshot = testSnapshot.map(() => {});
expect(mappedSnapshot.getLoadable(myAtom).getValue().storeID).toBe(
mappedSnapshot.getStoreID(),
);
expect(mappedSnapshot.getLoadable(myAtom).getValue().parentStoreID).toBe(
testSnapshot.getStoreID(),
);
});
});

View File

@@ -0,0 +1,58 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let unstable_batchedUpdates, batchUpdates, getBatcher, setBatcher;
const testRecoil = getRecoilTestFn(() => {
({unstable_batchedUpdates} = require('ReactDOMLegacy_DEPRECATED'));
({batchUpdates, getBatcher, setBatcher} = require('../Recoil_Batching'));
});
/**
* Cleanup function that will reset the batcher back
* to ReactDOM's resetBatcherToDefault.
*
* Call this at the end of a test that calls setBatcher
* to maintain test purity.
*/
const resetBatcherToDefault = () => {
setBatcher(unstable_batchedUpdates);
};
describe('batcher', () => {
testRecoil('default batcher is ReactDOM unstable_batchedUpdates', () => {
expect(getBatcher()).toEqual(unstable_batchedUpdates);
});
testRecoil('setBatcher sets the batcher function', () => {
const batcherFn = jest.fn();
setBatcher(batcherFn);
expect(getBatcher()).toEqual(batcherFn);
resetBatcherToDefault();
});
testRecoil('batchUpdates calls the batcher', () => {
const batcherFn = jest.fn();
setBatcher(batcherFn);
batchUpdates(() => {});
expect(batcherFn).toHaveBeenCalledTimes(1);
resetBatcherToDefault();
});
});

View File

@@ -0,0 +1,45 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let a, atom, store, nullthrows, getNodeLoadable, setNodeValue;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
atom = require('../../recoil_values/Recoil_atom');
nullthrows = require('recoil-shared/util/Recoil_nullthrows');
({getNodeLoadable, setNodeValue} = require('../Recoil_FunctionalCore'));
a = atom<number>({key: 'a', default: 0}).key;
store = makeStore();
});
testRecoil('read default value', () => {
expect(getNodeLoadable(store, store.getState().currentTree, a)).toMatchObject(
{
state: 'hasValue',
contents: 0,
},
);
});
testRecoil('setNodeValue returns written value when writing atom', () => {
const writes = setNodeValue(store, store.getState().currentTree, a, 1);
expect(nullthrows(writes.get(a)).contents).toBe(1);
});

View File

@@ -0,0 +1,197 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall obviz
*/
'use strict';
import type {Loadable, RecoilState, RecoilValue} from '../../Recoil_index';
const {atom, selector, selectorFamily} = require('../../Recoil_index');
const {waitForAll} = require('../../recoil_values/Recoil_WaitFor');
const {
getRecoilValueAsLoadable,
setRecoilValue,
} = require('../Recoil_RecoilValueInterface');
const {performance} = require('perf_hooks');
const {makeStore} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
const ITERATIONS = [1]; // Avoid iterating for automated testing
// const ITERATIONS = [100];
// const ITERATIONS = [1000];
// const ITERATIONS = [10, 100, 1000];
// const ITERATIONS = [10, 100, 1000, 10000];
// const ITERATIONS = [10, 100, 1000, 10000, 100000];
function testPerf(
name: string,
fn: ({iterations: number, perf: (() => void) => void}) => void,
) {
test.each(ITERATIONS)(name, iterations => {
store = makeStore();
const perf = (cb: () => void) => {
const BEGIN = performance.now();
cb();
const END = performance.now();
console.log(`${name}(${iterations})`, END - BEGIN);
};
fn({iterations, perf});
});
}
let store = makeStore();
function getNodeLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T> {
return getRecoilValueAsLoadable(store, recoilValue);
}
function getNodeValue<T>(recoilValue: RecoilValue<T>): T {
return getNodeLoadable(recoilValue).getValue();
}
function setNode(recoilValue: RecoilState<string>, value: mixed) {
setRecoilValue(store, recoilValue, value);
// $FlowFixMe[cannot-write]
// $FlowFixMe[unsafe-arithmetic]
store.getState().currentTree.version++;
}
let nextAtomKey = 0;
function createAtoms(num: number): Array<RecoilState<string>> {
const atoms = Array(num);
const atomKey = nextAtomKey++;
for (let i = 0; i < num; i++) {
atoms[i] = atom({
key: `PERF-${atomKey}-${i}`,
default: 'DEFAULT',
});
}
return atoms;
}
const helpersSelector = () =>
// $FlowFixMe[incompatible-call]
selector({
key: `PERF-helpers-${nextAtomKey++}`,
get: ({getCallback}) => ({
getSnapshot: getCallback(
({snapshot}) =>
() =>
snapshot,
),
}),
});
const getHelpers = () => getNodeValue(helpersSelector());
testPerf('create n atoms', ({iterations}) => {
createAtoms(iterations);
});
testPerf('get n atoms', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
for (const node of atoms) {
getNodeValue(node);
}
});
});
testPerf('set n atoms', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
for (const node of atoms) {
setNode(node, 'SET');
}
});
});
testPerf('get n selectors', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
const testFamily = selectorFamily({
key: 'PERF-getselectors',
get:
(id: number) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(atoms[id]) + get(atoms[0]),
});
perf(() => {
for (let i = 0; i < iterations; i++) {
getNodeValue(testFamily(i));
}
});
});
testPerf('clone n snapshots', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
const {getSnapshot} = getHelpers();
perf(() => {
for (const node of atoms) {
// Set node to avoid hitting cached snapshots
setNode(node, 'SET');
const snapshot = getSnapshot();
expect(getNodeValue(node)).toBe('SET');
expect(snapshot.getLoadable(node).contents).toBe('SET');
}
});
});
testPerf('get 1 selector with n dependencies', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
getNodeValue(waitForAll(atoms));
});
});
testPerf('get 1 selector with n dependencies n times', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
perf(() => {
for (let i = 0; i < iterations; i++) {
getNodeValue(waitForAll(atoms));
}
});
});
testPerf('get n selectors n times', ({iterations, perf}) => {
const atoms = createAtoms(iterations);
const testFamily = selectorFamily({
key: 'PERF-getselectors',
get:
(id: number) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(atoms[id]) + get(atoms[0]),
});
perf(() => {
for (let i = 0; i < iterations; i++) {
for (let j = 0; j < iterations; j++) {
getNodeValue(testFamily(i));
}
}
});
});
testPerf(
'get n selectors with n dependencies n times',
({iterations, perf}) => {
const atoms = createAtoms(iterations);
const testFamily = selectorFamily<_, number>({
key: 'PERF-getselectors',
get: () => () => waitForAll(atoms),
});
perf(() => {
for (let i = 0; i < iterations; i++) {
for (let j = 0; j < iterations; j++) {
getNodeValue(testFamily(i));
}
}
});
},
);

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {StoreID as StoreIDType} from 'Recoil_Keys';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React, renderElements, RecoilRoot, useRecoilStoreID;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({RecoilRoot, useRecoilStoreID} = require('../Recoil_RecoilRoot'));
});
testRecoil('useRecoilStoreID', () => {
const storeIDs: {[string]: StoreIDType} = {};
function StoreID({
rootKey,
}:
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A1'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A2'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'B'>}>) {
const storeID = useRecoilStoreID();
storeIDs[rootKey] = storeID;
return null;
}
function MyApp() {
return (
<div>
<RecoilRoot>
<StoreID rootKey="A" />
<RecoilRoot>
<StoreID rootKey="A1" />
</RecoilRoot>
<RecoilRoot override={false}>
<StoreID rootKey="A2" />
</RecoilRoot>
</RecoilRoot>
<RecoilRoot>
<StoreID rootKey="B" />
</RecoilRoot>
</div>
);
}
renderElements(<MyApp />);
expect('A' in storeIDs).toEqual(true);
expect('A1' in storeIDs).toEqual(true);
expect('A2' in storeIDs).toEqual(true);
expect('B' in storeIDs).toEqual(true);
expect(storeIDs.A).not.toEqual(storeIDs.B);
expect(storeIDs.A).not.toEqual(storeIDs.A1);
expect(storeIDs.A).toEqual(storeIDs.A2);
expect(storeIDs.B).not.toEqual(storeIDs.A1);
expect(storeIDs.B).not.toEqual(storeIDs.A2);
});

809
node_modules/recoil/cjs/hooks/Recoil_Hooks.js.flow generated vendored Normal file
View File

@@ -0,0 +1,809 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {DefaultValue} from '../core/Recoil_Node';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
import type {ComponentSubscription} from '../core/Recoil_RecoilValueInterface';
import type {
NodeKey,
StoreRef,
StoreState,
TreeState,
} from '../core/Recoil_State';
const {batchUpdates} = require('../core/Recoil_Batching');
const {DEFAULT_VALUE} = require('../core/Recoil_Node');
const {
currentRendererSupportsUseSyncExternalStore,
reactMode,
useMutableSource,
useSyncExternalStore,
} = require('../core/Recoil_ReactMode');
const {
useRecoilMutableSource,
useStoreRef,
} = require('../core/Recoil_RecoilRoot');
const {isRecoilValue} = require('../core/Recoil_RecoilValue');
const {
AbstractRecoilValue,
getRecoilValueAsLoadable,
setRecoilValue,
setUnvalidatedRecoilValue,
subscribeToRecoilValue,
} = require('../core/Recoil_RecoilValueInterface');
const useRetain = require('./Recoil_useRetain');
const {useCallback, useEffect, useMemo, useRef, useState} = require('react');
const {setByAddingToSet} = require('recoil-shared/util/Recoil_CopyOnWrite');
const differenceSets = require('recoil-shared/util/Recoil_differenceSets');
const {isSSR} = require('recoil-shared/util/Recoil_Environment');
const err = require('recoil-shared/util/Recoil_err');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const gkx = require('recoil-shared/util/Recoil_gkx');
const isPromise = require('recoil-shared/util/Recoil_isPromise');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
const useComponentName = require('recoil-shared/util/Recoil_useComponentName');
function handleLoadable<T>(
loadable: Loadable<T>,
recoilValue: RecoilValue<T>,
storeRef: StoreRef,
): T {
// We can't just throw the promise we are waiting on to Suspense. If the
// upstream dependencies change it may produce a state in which the component
// can render, but it would still be suspended on a Promise that may never resolve.
if (loadable.state === 'hasValue') {
return loadable.contents;
} else if (loadable.state === 'loading') {
const promise = new Promise(resolve => {
const suspendedComponentResolvers =
storeRef.current.getState().suspendedComponentResolvers;
suspendedComponentResolvers.add(resolve);
// SSR should clear out the wake-up resolver if the Promise is resolved
// to avoid infinite loops. (See https://github.com/facebookexperimental/Recoil/pull/2073)
if (isSSR && isPromise(loadable.contents)) {
loadable.contents.finally(() => {
suspendedComponentResolvers.delete(resolve);
});
}
});
// $FlowExpectedError Flow(prop-missing) for integrating with tools that inspect thrown promises @fb-only
// @fb-only: promise.displayName = `Recoil State: ${recoilValue.key}`;
throw promise;
} else if (loadable.state === 'hasError') {
throw loadable.contents;
} else {
throw err(`Invalid value of loadable atom "${recoilValue.key}"`);
}
}
function validateRecoilValue<T>(
recoilValue: RecoilValue<T>,
hookName:
| $TEMPORARY$string<'useRecoilState'>
| $TEMPORARY$string<'useRecoilStateLoadable'>
| $TEMPORARY$string<'useRecoilState_TRANSITION_SUPPORT_UNSTABLE'>
| $TEMPORARY$string<'useRecoilValue'>
| $TEMPORARY$string<'useRecoilValueLoadable'>
| $TEMPORARY$string<'useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE'>
| $TEMPORARY$string<'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE'>
| $TEMPORARY$string<'useResetRecoilState'>
| $TEMPORARY$string<'useSetRecoilState'>,
// $FlowFixMe[missing-local-annot]
) {
if (!isRecoilValue(recoilValue)) {
throw err(
`Invalid argument to ${hookName}: expected an atom or selector but got ${String(
recoilValue,
)}`,
);
}
}
export type SetterOrUpdater<T> = ((T => T) | T) => void;
export type Resetter = () => void;
export type RecoilInterface = {
getRecoilValue: <T>(RecoilValue<T>) => T,
getRecoilValueLoadable: <T>(RecoilValue<T>) => Loadable<T>,
getRecoilState: <T>(RecoilState<T>) => [T, SetterOrUpdater<T>],
getRecoilStateLoadable: <T>(
RecoilState<T>,
) => [Loadable<T>, SetterOrUpdater<T>],
getSetRecoilState: <T>(RecoilState<T>) => SetterOrUpdater<T>,
getResetRecoilState: <T>(RecoilState<T>) => Resetter,
};
/**
* Various things are broken with useRecoilInterface, particularly concurrent
* mode, React strict mode, and memory management. They will not be fixed.
* */
function useRecoilInterface_DEPRECATED(): RecoilInterface {
const componentName = useComponentName();
const storeRef = useStoreRef();
// eslint-disable-next-line fb-www/react-no-unused-state-hook
const [, forceUpdate] = useState(([]: Array<$FlowFixMe>));
const recoilValuesUsed = useRef<$ReadOnlySet<NodeKey>>(new Set());
recoilValuesUsed.current = new Set(); // Track the RecoilValues used just during this render
const previousSubscriptions = useRef<$ReadOnlySet<NodeKey>>(new Set());
const subscriptions = useRef<Map<NodeKey, ComponentSubscription>>(new Map());
const unsubscribeFrom = useCallback(
(key: NodeKey) => {
const sub = subscriptions.current.get(key);
if (sub) {
sub.release();
subscriptions.current.delete(key);
}
},
[subscriptions],
);
const updateState = useCallback(
(_state: TreeState | StoreState, key: NodeKey) => {
if (subscriptions.current.has(key)) {
forceUpdate([]);
}
},
[],
);
// Effect to add/remove subscriptions as nodes are used
useEffect(() => {
const store = storeRef.current;
differenceSets(
recoilValuesUsed.current,
previousSubscriptions.current,
).forEach(key => {
if (subscriptions.current.has(key)) {
expectationViolation(`Double subscription to RecoilValue "${key}"`);
return;
}
const sub = subscribeToRecoilValue(
store,
new AbstractRecoilValue(key),
state => updateState(state, key),
componentName,
);
subscriptions.current.set(key, sub);
/**
* Since we're subscribing in an effect we need to update to the latest
* value of the atom since it may have changed since we rendered. We can
* go ahead and do that now, unless we're in the middle of a batch --
* in which case we should do it at the end of the batch, due to the
* following edge case: Suppose an atom is updated in another useEffect
* of this same component. Then the following sequence of events occur:
* 1. Atom is updated and subs fired (but we may not be subscribed
* yet depending on order of effects, so we miss this) Updated value
* is now in nextTree, but not currentTree.
* 2. This effect happens. We subscribe and update.
* 3. From the update we re-render and read currentTree, with old value.
* 4. Batcher's effect sets currentTree to nextTree.
* In this sequence we miss the update. To avoid that, add the update
* to queuedComponentCallback if a batch is in progress.
*/
// FIXME delete queuedComponentCallbacks_DEPRECATED when deleting useInterface.
const state = store.getState();
if (state.nextTree) {
store.getState().queuedComponentCallbacks_DEPRECATED.push(() => {
updateState(store.getState(), key);
});
} else {
updateState(store.getState(), key);
}
});
differenceSets(
previousSubscriptions.current,
recoilValuesUsed.current,
).forEach(key => {
unsubscribeFrom(key);
});
previousSubscriptions.current = recoilValuesUsed.current;
});
// Effect to unsubscribe from all when unmounting
useEffect(() => {
const currentSubscriptions = subscriptions.current;
// Restore subscriptions that were cleared due to StrictMode running this effect twice
differenceSets(
recoilValuesUsed.current,
new Set(currentSubscriptions.keys()),
).forEach(key => {
const sub = subscribeToRecoilValue(
storeRef.current,
new AbstractRecoilValue(key),
state => updateState(state, key),
componentName,
);
currentSubscriptions.set(key, sub);
});
return () => currentSubscriptions.forEach((_, key) => unsubscribeFrom(key));
}, [componentName, storeRef, unsubscribeFrom, updateState]);
return useMemo(() => {
// eslint-disable-next-line no-shadow
function useSetRecoilState<T>(
recoilState: RecoilState<T>,
): SetterOrUpdater<T> {
if (__DEV__) {
validateRecoilValue(recoilState, 'useSetRecoilState');
}
return (
newValueOrUpdater: (T => T | DefaultValue) | T | DefaultValue,
) => {
setRecoilValue(storeRef.current, recoilState, newValueOrUpdater);
};
}
// eslint-disable-next-line no-shadow
function useResetRecoilState<T>(recoilState: RecoilState<T>): Resetter {
if (__DEV__) {
validateRecoilValue(recoilState, 'useResetRecoilState');
}
return () => setRecoilValue(storeRef.current, recoilState, DEFAULT_VALUE);
}
// eslint-disable-next-line no-shadow
function useRecoilValueLoadable<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
if (__DEV__) {
validateRecoilValue(recoilValue, 'useRecoilValueLoadable');
}
if (!recoilValuesUsed.current.has(recoilValue.key)) {
recoilValuesUsed.current = setByAddingToSet(
recoilValuesUsed.current,
recoilValue.key,
);
}
// TODO Restore optimization to memoize lookup
const storeState = storeRef.current.getState();
return getRecoilValueAsLoadable(
storeRef.current,
recoilValue,
reactMode().early
? storeState.nextTree ?? storeState.currentTree
: storeState.currentTree,
);
}
// eslint-disable-next-line no-shadow
function useRecoilValue<T>(recoilValue: RecoilValue<T>): T {
if (__DEV__) {
validateRecoilValue(recoilValue, 'useRecoilValue');
}
const loadable = useRecoilValueLoadable(recoilValue);
return handleLoadable(loadable, recoilValue, storeRef);
}
// eslint-disable-next-line no-shadow
function useRecoilState<T>(
recoilState: RecoilState<T>,
): [T, SetterOrUpdater<T>] {
if (__DEV__) {
validateRecoilValue(recoilState, 'useRecoilState');
}
return [useRecoilValue(recoilState), useSetRecoilState(recoilState)];
}
// eslint-disable-next-line no-shadow
function useRecoilStateLoadable<T>(
recoilState: RecoilState<T>,
): [Loadable<T>, SetterOrUpdater<T>] {
if (__DEV__) {
validateRecoilValue(recoilState, 'useRecoilStateLoadable');
}
return [
useRecoilValueLoadable(recoilState),
useSetRecoilState(recoilState),
];
}
return {
getRecoilValue: useRecoilValue,
getRecoilValueLoadable: useRecoilValueLoadable,
getRecoilState: useRecoilState,
getRecoilStateLoadable: useRecoilStateLoadable,
getSetRecoilState: useSetRecoilState,
getResetRecoilState: useResetRecoilState,
};
}, [recoilValuesUsed, storeRef]);
}
const recoilComponentGetRecoilValueCount_FOR_TESTING = {current: 0};
function useRecoilValueLoadable_SYNC_EXTERNAL_STORE<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
const storeRef = useStoreRef();
const componentName = useComponentName();
const getSnapshot = useCallback(() => {
if (__DEV__) {
recoilComponentGetRecoilValueCount_FOR_TESTING.current++;
}
const store = storeRef.current;
const storeState = store.getState();
const treeState = reactMode().early
? storeState.nextTree ?? storeState.currentTree
: storeState.currentTree;
const loadable = getRecoilValueAsLoadable(store, recoilValue, treeState);
return {loadable, key: recoilValue.key};
}, [storeRef, recoilValue]);
// Memoize the state to avoid unnecessary rerenders
const memoizePreviousSnapshot = useCallback(
(getState: () => {key: NodeKey, loadable: Loadable<T>}) => {
let prevState;
return () => {
const nextState = getState();
if (
prevState?.loadable.is(nextState.loadable) &&
prevState?.key === nextState.key
) {
return prevState;
}
prevState = nextState;
return nextState;
};
},
[],
);
const getMemoizedSnapshot = useMemo(
() => memoizePreviousSnapshot(getSnapshot),
[getSnapshot, memoizePreviousSnapshot],
);
const subscribe = useCallback(
(notify: () => void) => {
const store = storeRef.current;
const subscription = subscribeToRecoilValue(
store,
recoilValue,
notify,
componentName,
);
return subscription.release;
},
[storeRef, recoilValue, componentName],
);
return useSyncExternalStore(
subscribe,
getMemoizedSnapshot, // getSnapshot()
getMemoizedSnapshot, // getServerSnapshot() for SSR support
).loadable;
}
function useRecoilValueLoadable_MUTABLE_SOURCE<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
const storeRef = useStoreRef();
const getLoadable = useCallback(() => {
const store = storeRef.current;
const storeState = store.getState();
const treeState = reactMode().early
? storeState.nextTree ?? storeState.currentTree
: storeState.currentTree;
return getRecoilValueAsLoadable(store, recoilValue, treeState);
}, [storeRef, recoilValue]);
const getLoadableWithTesting = useCallback(() => {
if (__DEV__) {
recoilComponentGetRecoilValueCount_FOR_TESTING.current++;
}
return getLoadable();
}, [getLoadable]);
const componentName = useComponentName();
const subscribe: (_storeState: empty, notify: () => void) => () => void =
useCallback(
(_storeState, notify: () => void) => {
const store = storeRef.current;
const subscription: ComponentSubscription = subscribeToRecoilValue(
store,
recoilValue,
() => {
if (!gkx('recoil_suppress_rerender_in_callback')) {
return notify();
}
// Only re-render if the value has changed.
// This will evaluate the atom/selector now as well as when the
// component renders, but that may help with prefetching.
const newLoadable = getLoadable();
if (!prevLoadableRef.current.is(newLoadable)) {
notify();
}
// If the component is suspended then the effect setting prevLoadableRef
// will not run. So, set the previous value here when its subscription
// is fired to wake it up. We can't just rely on this, though, because
// this only executes when an atom/selector is dirty and the atom/selector
// passed to the hook can dynamically change.
prevLoadableRef.current = newLoadable;
},
componentName,
);
return subscription.release;
},
[storeRef, recoilValue, componentName, getLoadable],
);
const source = useRecoilMutableSource();
if (source == null) {
throw err(
'Recoil hooks must be used in components contained within a <RecoilRoot> component.',
);
}
const loadable: Loadable<T> = useMutableSource(
source,
getLoadableWithTesting,
subscribe,
);
const prevLoadableRef: {current: Loadable<T>} = useRef(loadable);
useEffect(() => {
prevLoadableRef.current = loadable;
});
return loadable;
}
function useRecoilValueLoadable_TRANSITION_SUPPORT<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
const storeRef = useStoreRef();
const componentName = useComponentName();
// Accessors to get the current state
const getLoadable = useCallback(() => {
if (__DEV__) {
recoilComponentGetRecoilValueCount_FOR_TESTING.current++;
}
const store = storeRef.current;
const storeState = store.getState();
const treeState = reactMode().early
? storeState.nextTree ?? storeState.currentTree
: storeState.currentTree;
return getRecoilValueAsLoadable(store, recoilValue, treeState);
}, [storeRef, recoilValue]);
const getState = useCallback(
() => ({loadable: getLoadable(), key: recoilValue.key}),
[getLoadable, recoilValue.key],
);
// Memoize state snapshots
const updateState = useCallback(
(prevState: {key: NodeKey, loadable: Loadable<T>}) => {
const nextState = getState();
return prevState.loadable.is(nextState.loadable) &&
prevState.key === nextState.key
? prevState
: nextState;
},
[getState],
);
// Subscribe to Recoil state changes
useEffect(() => {
const subscription = subscribeToRecoilValue(
storeRef.current,
recoilValue,
_state => {
setState(updateState);
},
componentName,
);
// Update state in case we are using a different key
setState(updateState);
return subscription.release;
}, [componentName, recoilValue, storeRef, updateState]);
// Get the current state
const [state, setState] = useState(getState);
// If we changed keys, then return the state for the new key.
// This is important in case the old key would cause the component to suspend.
// We don't have to set the new state here since the subscribing effect above
// will do that.
return state.key !== recoilValue.key ? getState().loadable : state.loadable;
}
function useRecoilValueLoadable_LEGACY<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
const storeRef = useStoreRef();
// eslint-disable-next-line fb-www/react-no-unused-state-hook
const [, forceUpdate] = useState(([]: Array<$FlowFixMe>));
const componentName = useComponentName();
const getLoadable = useCallback(() => {
if (__DEV__) {
recoilComponentGetRecoilValueCount_FOR_TESTING.current++;
}
const store = storeRef.current;
const storeState = store.getState();
const treeState = reactMode().early
? storeState.nextTree ?? storeState.currentTree
: storeState.currentTree;
return getRecoilValueAsLoadable(store, recoilValue, treeState);
}, [storeRef, recoilValue]);
const loadable = getLoadable();
const prevLoadableRef = useRef(loadable);
useEffect(() => {
prevLoadableRef.current = loadable;
});
useEffect(() => {
const store = storeRef.current;
const storeState = store.getState();
const subscription = subscribeToRecoilValue(
store,
recoilValue,
_state => {
if (!gkx('recoil_suppress_rerender_in_callback')) {
return forceUpdate([]);
}
const newLoadable = getLoadable();
if (!prevLoadableRef.current?.is(newLoadable)) {
// $FlowFixMe[incompatible-call]
forceUpdate(newLoadable);
}
prevLoadableRef.current = newLoadable;
},
componentName,
);
/**
* Since we're subscribing in an effect we need to update to the latest
* value of the atom since it may have changed since we rendered. We can
* go ahead and do that now, unless we're in the middle of a batch --
* in which case we should do it at the end of the batch, due to the
* following edge case: Suppose an atom is updated in another useEffect
* of this same component. Then the following sequence of events occur:
* 1. Atom is updated and subs fired (but we may not be subscribed
* yet depending on order of effects, so we miss this) Updated value
* is now in nextTree, but not currentTree.
* 2. This effect happens. We subscribe and update.
* 3. From the update we re-render and read currentTree, with old value.
* 4. Batcher's effect sets currentTree to nextTree.
* In this sequence we miss the update. To avoid that, add the update
* to queuedComponentCallback if a batch is in progress.
*/
if (storeState.nextTree) {
store.getState().queuedComponentCallbacks_DEPRECATED.push(() => {
// $FlowFixMe[incompatible-type]
prevLoadableRef.current = null;
forceUpdate([]);
});
} else {
if (!gkx('recoil_suppress_rerender_in_callback')) {
return forceUpdate([]);
}
const newLoadable = getLoadable();
if (!prevLoadableRef.current?.is(newLoadable)) {
// $FlowFixMe[incompatible-call]
forceUpdate(newLoadable);
}
prevLoadableRef.current = newLoadable;
}
return subscription.release;
}, [componentName, getLoadable, recoilValue, storeRef]);
return loadable;
}
/**
Like useRecoilValue(), but either returns the value if available or
just undefined if not available for any reason, such as pending or error.
*/
function useRecoilValueLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T> {
if (__DEV__) {
validateRecoilValue(recoilValue, 'useRecoilValueLoadable');
}
if (gkx('recoil_memory_managament_2020')) {
// eslint-disable-next-line fb-www/react-hooks
useRetain(recoilValue);
}
return {
TRANSITION_SUPPORT: useRecoilValueLoadable_TRANSITION_SUPPORT,
// Recoil will attemp to detect if `useSyncExternalStore()` is supported with
// `reactMode()` before calling it. However, sometimes the host React
// environment supports it but uses additional React renderers (such as with
// `react-three-fiber`) which do not. While this is technically a user issue
// by using a renderer with React 18+ that doesn't fully support React 18 we
// don't want to break users if it can be avoided. As the current renderer can
// change at runtime, we need to dynamically check and fallback if necessary.
SYNC_EXTERNAL_STORE: currentRendererSupportsUseSyncExternalStore()
? useRecoilValueLoadable_SYNC_EXTERNAL_STORE
: useRecoilValueLoadable_TRANSITION_SUPPORT,
MUTABLE_SOURCE: useRecoilValueLoadable_MUTABLE_SOURCE,
LEGACY: useRecoilValueLoadable_LEGACY,
}[reactMode().mode](recoilValue);
}
/**
Returns the value represented by the RecoilValue.
If the value is pending, it will throw a Promise to suspend the component,
if the value is an error it will throw it for the nearest React error boundary.
This will also subscribe the component for any updates in the value.
*/
function useRecoilValue<T>(recoilValue: RecoilValue<T>): T {
if (__DEV__) {
validateRecoilValue(recoilValue, 'useRecoilValue');
}
const storeRef = useStoreRef();
const loadable = useRecoilValueLoadable(recoilValue);
return handleLoadable(loadable, recoilValue, storeRef);
}
/**
Returns a function that allows the value of a RecoilState to be updated, but does
not subscribe the component to changes to that RecoilState.
*/
function useSetRecoilState<T>(recoilState: RecoilState<T>): SetterOrUpdater<T> {
if (__DEV__) {
validateRecoilValue(recoilState, 'useSetRecoilState');
}
const storeRef = useStoreRef();
return useCallback(
(newValueOrUpdater: (T => T | DefaultValue) | T | DefaultValue) => {
setRecoilValue(storeRef.current, recoilState, newValueOrUpdater);
},
[storeRef, recoilState],
);
}
/**
Returns a function that will reset the value of a RecoilState to its default
*/
function useResetRecoilState<T>(recoilState: RecoilState<T>): Resetter {
if (__DEV__) {
validateRecoilValue(recoilState, 'useResetRecoilState');
}
const storeRef = useStoreRef();
return useCallback(() => {
setRecoilValue(storeRef.current, recoilState, DEFAULT_VALUE);
}, [storeRef, recoilState]);
}
/**
Equivalent to useState(). Allows the value of the RecoilState to be read and written.
Subsequent updates to the RecoilState will cause the component to re-render. If the
RecoilState is pending, this will suspend the component and initiate the
retrieval of the value. If evaluating the RecoilState resulted in an error, this will
throw the error so that the nearest React error boundary can catch it.
*/
function useRecoilState<T>(
recoilState: RecoilState<T>,
): [T, SetterOrUpdater<T>] {
if (__DEV__) {
validateRecoilValue(recoilState, 'useRecoilState');
}
return [useRecoilValue(recoilState), useSetRecoilState(recoilState)];
}
/**
Like useRecoilState(), but does not cause Suspense or React error handling. Returns
an object that indicates whether the RecoilState is available, pending, or
unavailable due to an error.
*/
function useRecoilStateLoadable<T>(
recoilState: RecoilState<T>,
): [Loadable<T>, SetterOrUpdater<T>] {
if (__DEV__) {
validateRecoilValue(recoilState, 'useRecoilStateLoadable');
}
return [useRecoilValueLoadable(recoilState), useSetRecoilState(recoilState)];
}
function useSetUnvalidatedAtomValues(): (
values: Map<NodeKey, mixed>,
transactionMetadata?: {...},
) => void {
const storeRef = useStoreRef();
return (values: Map<NodeKey, mixed>, transactionMetadata: {...} = {}) => {
batchUpdates(() => {
storeRef.current.addTransactionMetadata(transactionMetadata);
values.forEach((value, key) =>
setUnvalidatedRecoilValue(
storeRef.current,
new AbstractRecoilValue(key),
value,
),
);
});
};
}
/**
* Experimental variants of hooks with support for useTransition()
*/
function useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE<T>(
recoilValue: RecoilValue<T>,
): Loadable<T> {
if (__DEV__) {
validateRecoilValue(
recoilValue,
'useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE',
);
if (!reactMode().early) {
recoverableViolation(
'Attepmt to use a hook with UNSTABLE_TRANSITION_SUPPORT in a rendering mode incompatible with concurrent rendering. Try enabling the recoil_sync_external_store or recoil_transition_support GKs.',
'recoil',
);
}
}
if (gkx('recoil_memory_managament_2020')) {
// eslint-disable-next-line fb-www/react-hooks
useRetain(recoilValue);
}
return useRecoilValueLoadable_TRANSITION_SUPPORT(recoilValue);
}
function useRecoilValue_TRANSITION_SUPPORT_UNSTABLE<T>(
recoilValue: RecoilValue<T>,
): T {
if (__DEV__) {
validateRecoilValue(
recoilValue,
'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE',
);
}
const storeRef = useStoreRef();
const loadable =
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(recoilValue);
return handleLoadable(loadable, recoilValue, storeRef);
}
function useRecoilState_TRANSITION_SUPPORT_UNSTABLE<T>(
recoilState: RecoilState<T>,
): [T, SetterOrUpdater<T>] {
if (__DEV__) {
validateRecoilValue(
recoilState,
'useRecoilState_TRANSITION_SUPPORT_UNSTABLE',
);
}
return [
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(recoilState),
useSetRecoilState(recoilState),
];
}
module.exports = {
recoilComponentGetRecoilValueCount_FOR_TESTING,
useRecoilInterface: useRecoilInterface_DEPRECATED,
useRecoilState,
useRecoilStateLoadable,
useRecoilValue,
useRecoilValueLoadable,
useResetRecoilState,
useSetRecoilState,
useSetUnvalidatedAtomValues,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
};

View File

@@ -0,0 +1,276 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {PersistenceType} from '../core/Recoil_Node';
import type {Snapshot} from '../core/Recoil_Snapshot';
import type {NodeKey, Store, TreeState} from '../core/Recoil_State';
const {batchUpdates} = require('../core/Recoil_Batching');
const {DEFAULT_VALUE, getNode, nodes} = require('../core/Recoil_Node');
const {useStoreRef} = require('../core/Recoil_RecoilRoot');
const {
AbstractRecoilValue,
setRecoilValueLoadable,
} = require('../core/Recoil_RecoilValueInterface');
const {SUSPENSE_TIMEOUT_MS} = require('../core/Recoil_Retention');
const {cloneSnapshot} = require('../core/Recoil_Snapshot');
const {useCallback, useEffect, useRef, useState} = require('react');
const {isSSR} = require('recoil-shared/util/Recoil_Environment');
const filterMap = require('recoil-shared/util/Recoil_filterMap');
const filterSet = require('recoil-shared/util/Recoil_filterSet');
const mapMap = require('recoil-shared/util/Recoil_mapMap');
const mergeMaps = require('recoil-shared/util/Recoil_mergeMaps');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
const usePrevious = require('recoil-shared/util/Recoil_usePrevious');
function useTransactionSubscription(callback: Store => void) {
const storeRef = useStoreRef();
useEffect(() => {
const sub = storeRef.current.subscribeToTransactions(callback);
return sub.release;
}, [callback, storeRef]);
}
function externallyVisibleAtomValuesInState(
state: TreeState,
): Map<NodeKey, mixed> {
const atomValues = state.atomValues.toMap();
const persistedAtomContentsValues = mapMap(
filterMap(atomValues, (v, k) => {
const node = getNode(k);
const persistence = node.persistence_UNSTABLE;
return (
persistence != null &&
persistence.type !== 'none' &&
v.state === 'hasValue'
);
}),
v => v.contents,
);
// Merge in nonvalidated atoms; we may not have defs for them but they will
// all have persistence on or they wouldn't be there in the first place.
return mergeMaps(
state.nonvalidatedAtoms.toMap(),
persistedAtomContentsValues,
);
}
type ExternallyVisibleAtomInfo = {
persistence_UNSTABLE: {
type: PersistenceType,
backButton: boolean,
...
},
...
};
/**
Calls the given callback after any atoms have been modified and the consequent
component re-renders have been committed. This is intended for persisting
the values of the atoms to storage. The stored values can then be restored
using the useSetUnvalidatedAtomValues hook.
The callback receives the following info:
atomValues: The current value of every atom that is both persistable (persistence
type not set to 'none') and whose value is available (not in an
error or loading state).
previousAtomValues: The value of every persistable and available atom before
the transaction began.
atomInfo: A map containing the persistence settings for each atom. Every key
that exists in atomValues will also exist in atomInfo.
modifiedAtoms: The set of atoms that were written to during the transaction.
transactionMetadata: Arbitrary information that was added via the
useSetUnvalidatedAtomValues hook. Useful for ignoring the useSetUnvalidatedAtomValues
transaction, to avoid loops.
*/
function useTransactionObservation_DEPRECATED(
callback: ({
atomValues: Map<NodeKey, mixed>,
previousAtomValues: Map<NodeKey, mixed>,
atomInfo: Map<NodeKey, ExternallyVisibleAtomInfo>,
modifiedAtoms: $ReadOnlySet<NodeKey>,
transactionMetadata: {[NodeKey]: mixed, ...},
}) => void,
) {
useTransactionSubscription(
useCallback(
(store: Store) => {
let previousTree = store.getState().previousTree;
const currentTree = store.getState().currentTree;
if (!previousTree) {
recoverableViolation(
'Transaction subscribers notified without a previous tree being present -- this is a bug in Recoil',
'recoil',
);
previousTree = store.getState().currentTree; // attempt to trundle on
}
const atomValues = externallyVisibleAtomValuesInState(currentTree);
const previousAtomValues =
externallyVisibleAtomValuesInState(previousTree);
const atomInfo = mapMap(nodes, node => ({
persistence_UNSTABLE: {
type: node.persistence_UNSTABLE?.type ?? 'none',
backButton: node.persistence_UNSTABLE?.backButton ?? false,
},
}));
// Filter on existance in atomValues so that externally-visible rules
// are also applied to modified atoms (specifically exclude selectors):
const modifiedAtoms = filterSet(
currentTree.dirtyAtoms,
k => atomValues.has(k) || previousAtomValues.has(k),
);
callback({
atomValues,
previousAtomValues,
atomInfo,
modifiedAtoms,
transactionMetadata: {...currentTree.transactionMetadata},
});
},
[callback],
),
);
}
function useRecoilTransactionObserver(
callback: ({
snapshot: Snapshot,
previousSnapshot: Snapshot,
}) => void,
) {
useTransactionSubscription(
useCallback(
(store: Store) => {
const snapshot = cloneSnapshot(store, 'latest');
const previousSnapshot = cloneSnapshot(store, 'previous');
callback({
snapshot,
previousSnapshot,
});
},
[callback],
),
);
}
// Return a snapshot of the current state and subscribe to all state changes
function useRecoilSnapshot(): Snapshot {
const storeRef = useStoreRef();
const [snapshot, setSnapshot] = useState(() =>
cloneSnapshot(storeRef.current),
);
const previousSnapshot = usePrevious(snapshot);
const timeoutID = useRef<?TimeoutID>();
const releaseRef = useRef<?() => void>();
useTransactionSubscription(
useCallback((store: Store) => setSnapshot(cloneSnapshot(store)), []),
);
// Retain snapshot for duration component is mounted
useEffect(() => {
const release = snapshot.retain();
// Release the retain from the rendering call
if (timeoutID.current && !isSSR) {
window.clearTimeout(timeoutID.current);
timeoutID.current = null;
releaseRef.current?.();
releaseRef.current = null;
}
return () => {
// Defer the release. If "Fast Refresh"" is used then the component may
// re-render with the same state. The previous cleanup will then run and
// then the new effect will run. We don't want the snapshot to be released
// by that cleanup before the new effect has a chance to retain it again.
// Use timeout of 10 to workaround Firefox issue: https://github.com/facebookexperimental/Recoil/issues/1936
window.setTimeout(release, 10);
};
}, [snapshot]);
// Retain snapshot until above effect is run.
// Release after a threshold in case component is suspended.
if (previousSnapshot !== snapshot && !isSSR) {
// Release the previous snapshot
if (timeoutID.current) {
window.clearTimeout(timeoutID.current);
timeoutID.current = null;
releaseRef.current?.();
releaseRef.current = null;
}
releaseRef.current = snapshot.retain();
timeoutID.current = window.setTimeout(() => {
timeoutID.current = null;
releaseRef.current?.();
releaseRef.current = null;
}, SUSPENSE_TIMEOUT_MS);
}
return snapshot;
}
function gotoSnapshot(store: Store, snapshot: Snapshot): void {
const storeState = store.getState();
const prev = storeState.nextTree ?? storeState.currentTree;
const next = snapshot.getStore_INTERNAL().getState().currentTree;
batchUpdates(() => {
const keysToUpdate = new Set<NodeKey>();
for (const keys of [prev.atomValues.keys(), next.atomValues.keys()]) {
for (const key of keys) {
if (
prev.atomValues.get(key)?.contents !==
next.atomValues.get(key)?.contents &&
getNode(key).shouldRestoreFromSnapshots
) {
keysToUpdate.add(key);
}
}
}
keysToUpdate.forEach(key => {
setRecoilValueLoadable(
store,
new AbstractRecoilValue(key),
next.atomValues.has(key)
? nullthrows(next.atomValues.get(key))
: DEFAULT_VALUE,
);
});
store.replaceState(state => ({...state, stateID: snapshot.getID()}));
});
}
function useGotoRecoilSnapshot(): Snapshot => void {
const storeRef = useStoreRef();
return useCallback(
(snapshot: Snapshot) => gotoSnapshot(storeRef.current, snapshot),
[storeRef],
);
}
module.exports = {
useRecoilSnapshot,
gotoSnapshot,
useGotoRecoilSnapshot,
useRecoilTransactionObserver,
useTransactionObservation_DEPRECATED,
useTransactionSubscription_DEPRECATED: useTransactionSubscription,
};

View File

@@ -0,0 +1,28 @@
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValueInfo} from '../core/Recoil_FunctionalCore';
import type {RecoilValue} from '../core/Recoil_RecoilValue';
const {peekNodeInfo} = require('../core/Recoil_FunctionalCore');
const {useStoreRef} = require('../core/Recoil_RecoilRoot');
function useGetRecoilValueInfo(): <T>(RecoilValue<T>) => RecoilValueInfo<T> {
const storeRef = useStoreRef();
// $FlowFixMe[incompatible-return]
return <T>({key}): RecoilValueInfo<T> =>
peekNodeInfo<T>(
storeRef.current,
storeRef.current.getState().currentTree,
key,
);
}
module.exports = useGetRecoilValueInfo;

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {reactMode} = require('../core/Recoil_ReactMode');
const {RecoilRoot, useStoreRef} = require('../core/Recoil_RecoilRoot');
const React = require('react');
const {useMemo} = require('react');
export type RecoilBridge = React.AbstractComponent<{children: React.Node}>;
function useRecoilBridgeAcrossReactRoots(): RecoilBridge {
// The test fails when using useMutableSource(), but only if act() is used
// for the nested root. So, this may only be a testing environment issue.
if (reactMode().mode === 'MUTABLE_SOURCE') {
// eslint-disable-next-line fb-www/no-console
console.warn(
'Warning: There are known issues using useRecoilBridgeAcrossReactRoots() in recoil_mutable_source rendering mode. Please consider upgrading to recoil_sync_external_store mode.',
);
}
const store = useStoreRef().current;
return useMemo(() => {
// eslint-disable-next-line no-shadow
function RecoilBridge({
children,
}: $TEMPORARY$object<{children: React.Node}>) {
return <RecoilRoot store_INTERNAL={store}>{children}</RecoilRoot>;
}
return RecoilBridge;
}, [store]);
}
module.exports = useRecoilBridgeAcrossReactRoots;

View File

@@ -0,0 +1,131 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {TransactionInterface} from '../core/Recoil_AtomicUpdates';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
import type {Snapshot} from '../core/Recoil_Snapshot';
import type {Store} from '../core/Recoil_State';
const {atomicUpdater} = require('../core/Recoil_AtomicUpdates');
const {batchUpdates} = require('../core/Recoil_Batching');
const {DEFAULT_VALUE} = require('../core/Recoil_Node');
const {useStoreRef} = require('../core/Recoil_RecoilRoot');
const {
refreshRecoilValue,
setRecoilValue,
} = require('../core/Recoil_RecoilValueInterface');
const {cloneSnapshot} = require('../core/Recoil_Snapshot');
const {gotoSnapshot} = require('./Recoil_SnapshotHooks');
const {useCallback} = require('react');
const err = require('recoil-shared/util/Recoil_err');
const invariant = require('recoil-shared/util/Recoil_invariant');
const isPromise = require('recoil-shared/util/Recoil_isPromise');
const lazyProxy = require('recoil-shared/util/Recoil_lazyProxy');
export type RecoilCallbackInterface = $ReadOnly<{
set: <T>(RecoilState<T>, (T => T) | T) => void,
reset: <T>(RecoilState<T>) => void,
refresh: <T>(RecoilValue<T>) => void,
snapshot: Snapshot,
gotoSnapshot: Snapshot => void,
transact_UNSTABLE: ((TransactionInterface) => void) => void,
}>;
class Sentinel {}
const SENTINEL = new Sentinel();
function recoilCallback<Args: $ReadOnlyArray<mixed>, Return, ExtraInterface>(
store: Store,
fn: ({...ExtraInterface, ...RecoilCallbackInterface}) => (...Args) => Return,
args: Args,
extraInterface?: ExtraInterface,
): Return {
let ret: $FlowFixMe = SENTINEL;
let releaseSnapshot;
batchUpdates(() => {
const errMsg =
'useRecoilCallback() expects a function that returns a function: ' +
'it accepts a function of the type (RecoilInterface) => (Args) => ReturnType ' +
'and returns a callback function (Args) => ReturnType, where RecoilInterface is ' +
'an object {snapshot, set, ...} and Args and ReturnType are the argument and return ' +
'types of the callback you want to create. Please see the docs ' +
'at recoiljs.org for details.';
if (typeof fn !== 'function') {
throw err(errMsg);
}
// Clone the snapshot lazily to avoid overhead if the callback does not use it.
// Note that this means the snapshot may represent later state from when
// the callback was called if it first accesses the snapshot asynchronously.
const callbackInterface: {
...ExtraInterface,
...RecoilCallbackInterface,
} = lazyProxy(
{
...(extraInterface ?? ({}: any)), // flowlint-line unclear-type:off
// $FlowFixMe[missing-local-annot]
set: <T>(node: RecoilState<T>, newValue: T | (T => T)) =>
setRecoilValue(store, node, newValue),
// $FlowFixMe[missing-local-annot]
reset: <T>(node: RecoilState<T>) =>
setRecoilValue(store, node, DEFAULT_VALUE),
// $FlowFixMe[missing-local-annot]
refresh: <T>(node: RecoilValue<T>) => refreshRecoilValue(store, node),
gotoSnapshot: snapshot => gotoSnapshot(store, snapshot),
transact_UNSTABLE: transaction => atomicUpdater(store)(transaction),
},
{
snapshot: () => {
const snapshot = cloneSnapshot(store);
releaseSnapshot = snapshot.retain();
return snapshot;
},
},
);
const callback = fn(callbackInterface);
if (typeof callback !== 'function') {
throw err(errMsg);
}
ret = callback(...args);
});
invariant(
!(ret instanceof Sentinel),
'batchUpdates should return immediately',
);
if (isPromise(ret)) {
ret = ret.finally(() => {
releaseSnapshot?.();
});
} else {
releaseSnapshot?.();
}
return (ret: Return);
}
function useRecoilCallback<Args: $ReadOnlyArray<mixed>, Return>(
fn: RecoilCallbackInterface => (...Args) => Return,
deps?: $ReadOnlyArray<mixed>,
): (...Args) => Return {
const storeRef = useStoreRef();
return useCallback(
// $FlowIssue[incompatible-call]
(...args: Args): Return => {
return recoilCallback(storeRef.current, fn, args);
},
deps != null ? [...deps, storeRef] : undefined, // eslint-disable-line fb-www/react-hooks-deps
);
}
module.exports = {recoilCallback, useRecoilCallback};

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValue} from '../core/Recoil_RecoilValue';
const {useStoreRef} = require('../core/Recoil_RecoilRoot');
const {refreshRecoilValue} = require('../core/Recoil_RecoilValueInterface');
const {useCallback} = require('react');
function useRecoilRefresher<T>(recoilValue: RecoilValue<T>): () => void {
const storeRef = useStoreRef();
return useCallback(() => {
const store = storeRef.current;
refreshRecoilValue(store, recoilValue);
}, [recoilValue, storeRef]);
}
module.exports = useRecoilRefresher;

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {TransactionInterface} from '../core/Recoil_AtomicUpdates';
const {atomicUpdater} = require('../core/Recoil_AtomicUpdates');
const {useStoreRef} = require('../core/Recoil_RecoilRoot');
const {useMemo} = require('react');
function useRecoilTransaction<Arguments: $ReadOnlyArray<mixed>>(
fn: TransactionInterface => (...Arguments) => void,
deps?: $ReadOnlyArray<mixed>,
): (...Arguments) => void {
const storeRef = useStoreRef();
return useMemo(
() =>
(...args: Arguments): void => {
const atomicUpdate = atomicUpdater(storeRef.current);
atomicUpdate(transactionInterface => {
fn(transactionInterface)(...args);
});
},
deps != null ? [...deps, storeRef] : undefined, // eslint-disable-line fb-www/react-hooks-deps
);
}
module.exports = useRecoilTransaction;

101
node_modules/recoil/cjs/hooks/Recoil_useRetain.js.flow generated vendored Normal file
View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValue} from '../core/Recoil_RecoilValue';
const {useStoreRef} = require('../core/Recoil_RecoilRoot');
const {SUSPENSE_TIMEOUT_MS} = require('../core/Recoil_Retention');
const {updateRetainCount} = require('../core/Recoil_Retention');
const {RetentionZone} = require('../core/Recoil_RetentionZone');
const {useEffect, useRef} = require('react');
const {isSSR} = require('recoil-shared/util/Recoil_Environment');
const gkx = require('recoil-shared/util/Recoil_gkx');
const shallowArrayEqual = require('recoil-shared/util/Recoil_shallowArrayEqual');
const usePrevious = require('recoil-shared/util/Recoil_usePrevious');
// I don't see a way to avoid the any type here because we want to accept readable
// and writable values with any type parameter, but normally with writable ones
// RecoilState<SomeT> is not a subtype of RecoilState<mixed>.
type ToRetain =
| RecoilValue<any> // flowlint-line unclear-type:off
| RetentionZone
| $ReadOnlyArray<RecoilValue<any> | RetentionZone>; // flowlint-line unclear-type:off
function useRetain(toRetain: ToRetain): void {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
// eslint-disable-next-line fb-www/react-hooks
return useRetain_ACTUAL(toRetain);
}
function useRetain_ACTUAL(toRetain: ToRetain): void {
const array = Array.isArray(toRetain) ? toRetain : [toRetain];
const retainables = array.map(a => (a instanceof RetentionZone ? a : a.key));
const storeRef = useStoreRef();
useEffect(() => {
if (!gkx('recoil_memory_managament_2020')) {
return;
}
const store = storeRef.current;
if (timeoutID.current && !isSSR) {
// Already performed a temporary retain on render, simply cancel the release
// of that temporary retain.
window.clearTimeout(timeoutID.current);
timeoutID.current = null;
} else {
for (const r of retainables) {
updateRetainCount(store, r, 1);
}
}
return () => {
for (const r of retainables) {
updateRetainCount(store, r, -1);
}
};
// eslint-disable-next-line fb-www/react-hooks-deps
}, [storeRef, ...retainables]);
// We want to retain if the component suspends. This is terrible but the Suspense
// API affords us no better option. If we suspend and never commit after some
// seconds, then release. The 'actual' retain/release in the effect above
// cancels this.
const timeoutID = useRef<?TimeoutID>();
const previousRetainables = usePrevious(retainables);
if (
!isSSR &&
(previousRetainables === undefined ||
!shallowArrayEqual(previousRetainables, retainables))
) {
const store = storeRef.current;
for (const r of retainables) {
updateRetainCount(store, r, 1);
}
if (previousRetainables) {
for (const r of previousRetainables) {
updateRetainCount(store, r, -1);
}
}
if (timeoutID.current) {
window.clearTimeout(timeoutID.current);
}
timeoutID.current = window.setTimeout(() => {
timeoutID.current = null;
for (const r of retainables) {
updateRetainCount(store, r, -1);
}
}, SUSPENSE_TIMEOUT_MS);
}
}
module.exports = useRetain;

View File

@@ -0,0 +1,151 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
// Sanity tests for *_TRANSITION_SUPPORT_UNSTABLE() hooks. The actual tests
// for useTransition() support are in Recoil_useTransition-test.js
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
selector,
stringAtom,
asyncSelector,
flushPromisesAndTimers,
renderElements,
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useSetRecoilState,
reactMode;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
selector = require('../../recoil_values/Recoil_selector');
({
stringAtom,
asyncSelector,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useSetRecoilState,
} = require('../Recoil_Hooks'));
});
testRecoil('useRecoilValue_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [mySelector, resolve] = asyncSelector<string, _>();
let setAtom;
function Component() {
setAtom = useSetRecoilState(myAtom);
return [
useRecoilValue(myAtom),
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(myAtom),
useRecoilValue(mySelector),
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(mySelector),
].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');
act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
});
testRecoil('useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [mySelector, resolve] = asyncSelector<string, _>();
let setAtom;
function Component() {
setAtom = useSetRecoilState(myAtom);
return [
useRecoilValueLoadable(myAtom).getValue(),
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(myAtom).getValue(),
useRecoilValueLoadable(mySelector).getValue(),
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE(mySelector).getValue(),
].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');
act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
});
testRecoil('useRecoilState_TRANSITION_SUPPORT_UNSTABLE', async () => {
if (!reactMode().early) {
return;
}
const myAtom = stringAtom();
const [myAsyncSelector, resolve] = asyncSelector<string, _>();
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'useRecoilState_TRANSITION_SUPPORT_UNSTABLE selector',
get: () => myAsyncSelector,
// $FlowFixMe[incompatible-call]
set: ({set}, newValue) => set(myAtom, newValue),
});
let setAtom, setSelector;
function Component() {
const [v1] = useRecoilState(myAtom);
const [v2, setAtomValue] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom);
setAtom = setAtomValue;
const [v3] = useRecoilState(mySelector);
const [v4, setSelectorValue] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(mySelector);
setSelector = setSelectorValue;
return [v1, v2, v3, v4].join(' ');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
expect(c.textContent).toBe('DEFAULT DEFAULT RESOLVE RESOLVE');
act(() => setAtom('SET'));
expect(c.textContent).toBe('SET SET RESOLVE RESOLVE');
act(() => setSelector('SETS'));
expect(c.textContent).toBe('SETS SETS RESOLVE RESOLVE');
});

View File

@@ -0,0 +1,935 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
/* 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';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useEffect,
useState,
Profiler,
act,
Queue,
batchUpdates,
atom,
selector,
selectorFamily,
ReadsAtom,
renderElements,
renderUnwrappedElements,
recoilComponentGetRecoilValueCount_FOR_TESTING,
useRecoilState,
useRecoilStateLoadable,
useRecoilValue,
useSetRecoilState,
reactMode,
invariant;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useEffect, useState, Profiler} = require('react'));
({act} = require('ReactTestUtils'));
Queue = require('../../adt/Recoil_Queue');
({batchUpdates} = require('../../core/Recoil_Batching'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
selectorFamily = require('../../recoil_values/Recoil_selectorFamily');
({
ReadsAtom,
renderElements,
renderUnwrappedElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({
recoilComponentGetRecoilValueCount_FOR_TESTING,
useRecoilState,
useRecoilStateLoadable,
useRecoilValue,
useSetRecoilState,
} = require('../Recoil_Hooks'));
invariant = require('recoil-shared/util/Recoil_invariant');
});
let nextID = 0;
function counterAtom(persistence?: PersistenceSettings<number>) {
return atom({
key: `atom${nextID++}`,
default: 0,
persistence_UNSTABLE: persistence,
});
}
function plusOneSelector(dep: RecoilValue<number>) {
const fn = jest.fn(x => x + 1);
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [sel, fn];
}
function plusOneAsyncSelector(
dep: RecoilValue<number>,
): [RecoilValueReadOnly<number>, (number) => void] {
let nextTimeoutAmount = 100;
const fn = jest.fn(x => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x + 1);
}, nextTimeoutAmount);
});
});
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [
// $FlowFixMe[incompatible-return]
sel,
x => {
nextTimeoutAmount = x;
},
];
}
function additionSelector(
depA: RecoilValue<number>,
depB: RecoilValue<number>,
) {
const fn = jest.fn((a, b) => a + b);
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(depA), get(depB)),
});
return [sel, fn];
}
function componentThatReadsAndWritesAtom<T>(
recoilState: RecoilState<T>,
): [React.AbstractComponent<{...}>, ((T => T) | T) => void] {
let updateValue;
const Component = jest.fn(() => {
const [value, _updateValue] = useRecoilState(recoilState);
updateValue = _updateValue;
return value;
});
// flowlint-next-line unclear-type:off
return [(Component: any), (...args) => updateValue(...args)];
}
function componentThatWritesAtom<T>(
recoilState: RecoilState<T>,
// flowlint-next-line unclear-type:off
): [any, ((T => T) | T) => void] {
let updateValue;
const Component = jest.fn(() => {
updateValue = useSetRecoilState(recoilState);
return null;
});
// flowlint-next-line unclear-type:off
return [(Component: any), x => updateValue(x)];
}
function componentThatReadsTwoAtoms(
one: RecoilState<number>,
two: RecoilState<number> | RecoilValueReadOnly<number>,
) {
return (jest.fn(function ReadTwoAtoms() {
return `${useRecoilValue(one)},${useRecoilValue(two)}`;
}): any); // flowlint-line unclear-type:off
}
function componentThatReadsAtomWithCommitCount(
recoilState: RecoilState<number> | RecoilValueReadOnly<number>,
) {
const commit = jest.fn(() => {});
function ReadAtom() {
return (
// $FlowFixMe[invalid-tuple-arity]
<Profiler id="test" onRender={commit}>
{useRecoilValue(recoilState)}
</Profiler>
);
}
return [ReadAtom, commit];
}
function componentThatToggles(a: Node, b: null) {
const toggle = {current: () => invariant(false, 'bug in test code')};
const Toggle = () => {
const [value, setValue] = useState(false);
// $FlowFixMe[incompatible-type]
toggle.current = () => setValue(v => !v);
return value ? b : a;
};
return [Toggle, toggle];
}
function baseRenderCount(gks: Array<string>): number {
return reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')
? 1
: 0;
}
testRecoil('Component throws error when passing invalid node', async () => {
function Component() {
try {
// $FlowExpectedError[incompatible-call]
useRecoilValue('foo');
} catch (error) {
expect(error.message).toEqual(expect.stringContaining('useRecoilValue'));
return 'CAUGHT';
}
return 'INVALID';
}
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');
});
describe('Render counts', () => {
testRecoil(
'Component subscribed to atom is rendered just once',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const anAtom = counterAtom();
const [Component, updateValue] = componentThatReadsAndWritesAtom(anAtom);
renderElements(
<>
<Component />
</>,
);
expect(Component).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm);
act(() => updateValue(1));
expect(Component).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm);
},
);
testRecoil('Write-only components are not subscribed', ({strictMode}) => {
const anAtom = counterAtom();
const [Component, updateValue] = componentThatWritesAtom(anAtom);
renderElements(
<>
<Component />
</>,
);
expect(Component).toHaveBeenCalledTimes(strictMode ? 2 : 1);
act(() => updateValue(1));
expect(Component).toHaveBeenCalledTimes(strictMode ? 2 : 1);
});
testRecoil(
'Component that depends on atom in multiple ways is rendered just once',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const anAtom = counterAtom();
const [aSelector, _] = plusOneSelector(anAtom);
const [WriteComp, updateValue] = componentThatWritesAtom(anAtom);
// $FlowFixMe[incompatible-call]
const ReadComp = componentThatReadsTwoAtoms(anAtom, aSelector);
renderElements(
<>
<WriteComp />
<ReadComp />
</>,
);
expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 1) * sm);
act(() => updateValue(1));
expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm);
},
);
testRecoil(
'Component that depends on multiple atoms via selector is rendered just once',
({gks}) => {
const BASE_CALLS = baseRenderCount(gks);
const atomA = counterAtom();
const atomB = counterAtom();
const [aSelector, _] = additionSelector(atomA, atomB);
const [ComponentA, updateValueA] = componentThatWritesAtom(atomA);
const [ComponentB, updateValueB] = componentThatWritesAtom(atomB);
const [ReadComp, commit] =
// $FlowFixMe[incompatible-call]
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',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
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) * sm);
act(() => {
batchUpdates(() => {
updateValueA(1);
updateValueB(1);
});
});
expect(ReadComp).toHaveBeenCalledTimes((BASE_CALLS + 2) * sm);
},
);
testRecoil(
'Component is rendered just once when atom is changed twice',
({gks}) => {
const BASE_CALLS = baseRenderCount(gks);
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',
() => {
// useSyncExternalStore() will always call getSnapshot() to see if it has
// mutated between render and commit.
if (
reactMode().mode === 'LEGACY' ||
reactMode().mode === 'SYNC_EXTERNAL_STORE'
) {
return;
}
const atomA = counterAtom();
const atomB = counterAtom();
let _, setLocal;
let _a, setA;
let _b, _setB;
function Component() {
[_, setLocal] = useState(0);
[_a, setA] = useRecoilState(atomA);
[_b, _setB] = useRecoilState(atomB);
return null;
}
let __, setParentLocal;
function Parent() {
[__, setParentLocal] = useState(0);
return <Component />;
}
renderElements(<Parent />);
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(
'Components re-render only one time if selectorFamily changed',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const atomA = counterAtom();
const selectAFakeId = selectorFamily({
key: 'selectItem',
get:
(_id: number) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(atomA),
});
const Component = (jest.fn(function ReadFromSelector({id}) {
return useRecoilValue(selectAFakeId(id));
}): ({id: number}) => React.Node);
let increment;
const App = () => {
const [state, setState] = useRecoilState(atomA);
increment = () => setState(s => s + 1);
return <Component id={state} />;
};
const container = renderElements(<App />);
let baseCalls = BASE_CALLS;
expect(container.textContent).toEqual('0');
expect(Component).toHaveBeenCalledTimes((baseCalls + 1) * sm);
act(() => increment());
if (
(reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')) ||
reactMode().mode === 'TRANSITION_SUPPORT'
) {
baseCalls += 1;
}
expect(container.textContent).toEqual('1');
expect(Component).toHaveBeenCalledTimes((baseCalls + 2) * sm);
},
);
});
describe('Component Subscriptions', () => {
testRecoil(
'Can subscribe to and also change an atom in the same batch',
() => {
const anAtom = counterAtom();
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
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;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(true);
setVisible = mySetVisible;
return visible ? children : null;
}
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',
({gks, strictMode}) => {
const BASE_CALLS = baseRenderCount(gks);
const sm = strictMode ? 2 : 1;
const atomA = counterAtom();
const atomB = counterAtom();
const [WriteA, updateValueA] = componentThatWritesAtom(atomA);
const [WriteB, updateValueB] = componentThatWritesAtom(atomB);
const Component = (jest.fn(function Read({state}) {
const [value] = useRecoilState<mixed>(state);
return value;
}): any); // flowlint-line unclear-type:off
let toggleSwitch;
const Switch = () => {
const [value, setValue] = useState(false);
toggleSwitch = () => setValue(true);
return value ? (
<Component state={atomB} />
) : (
<Component state={atomA} />
);
};
const container = renderElements(
<>
<Switch />
<WriteA />
<WriteB />
</>,
);
let baseCalls = BASE_CALLS;
expect(container.textContent).toEqual('0');
expect(Component).toHaveBeenCalledTimes((baseCalls + 1) * sm);
act(() => updateValueA(1));
expect(container.textContent).toEqual('1');
expect(Component).toHaveBeenCalledTimes((baseCalls + 2) * sm);
if (
(reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')) ||
reactMode().mode === 'TRANSITION_SUPPORT'
) {
baseCalls += 1;
}
act(() => toggleSwitch());
expect(container.textContent).toEqual('0');
expect(Component).toHaveBeenCalledTimes((baseCalls + 3) * sm);
// 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) * sm); // 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) * sm);
},
);
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;
const Switch = () => {
const [value, setValue] = useState(false);
toggleSwitch = () => setValue(true);
return value ? <ReadsAtom atom={atomB} /> : <ReadsAtom atom={sel2} />;
};
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);
// $FlowFixMe[incompatible-call]
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;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(true);
setVisible = mySetVisible;
return visible ? children : null;
}
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(
'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 set an atom during rendering', () => {
const anAtom = counterAtom();
function SetsDuringRendering() {
const [value, setValue] = useRecoilState(anAtom);
if (value !== 1) {
setValue(1);
}
return null;
}
const container = renderElements(
<>
<SetsDuringRendering />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toEqual('1');
});
testRecoil(
'Does not re-create "setter" function after setting a value',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
const anAtom = counterAtom();
const anotherAtom = counterAtom();
let useRecoilStateCounter = 0;
let useRecoilStateErrorStatesCounter = 0;
let useTwoAtomsCounter = 0;
function Component1() {
const [_, setValue] = useRecoilState(anAtom);
useEffect(() => {
setValue(1);
useRecoilStateCounter += 1;
}, [setValue]);
return null;
}
function Component2() {
const [_, setValue] = useRecoilStateLoadable(anAtom);
useEffect(() => {
setValue(2);
useRecoilStateErrorStatesCounter += 1;
}, [setValue]);
return null;
}
// It is important to test here that the component will re-render with the
// new setValue() function for a new atom, even if the value of the new
// atom is the same as the previous value of the previous atom.
function Component3() {
const a = useTwoAtomsCounter > 0 ? anotherAtom : anAtom;
// setValue fn should change when we use a different atom.
const [, setValue] = useRecoilState(a);
useEffect(() => {
setValue(1);
useTwoAtomsCounter += 1;
}, [setValue]);
return null;
}
renderElements(
<>
<Component1 />
<Component2 />
<Component3 />
</>,
);
expect(useRecoilStateCounter).toBe(1 * sm);
expect(useRecoilStateErrorStatesCounter).toBe(1 * sm);
// Component3's effect is ran twice because the atom changes and we get a new setter.
// StrictMode renders twice, but we only change atoms once. So, only one extra count.
expect(useTwoAtomsCounter).toBe(strictMode && concurrentMode ? 3 : 2);
},
);
testRecoil(
'Can set atom during post-atom-setting effect (NOT during initial render)',
async () => {
const anAtom = counterAtom();
let done = false;
function SetsDuringEffect() {
const setValue = useSetRecoilState(anAtom);
useEffect(() => {
Queue.enqueueExecution('SetsDuringEffect', () => {
if (!done) {
setValue(1);
done = true;
}
});
});
return null;
}
const container = renderElements(
<>
<SetsDuringEffect />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toEqual('1');
},
);
testRecoil(
'Can set atom during post-atom-setting effect regardless of effect order',
async ({concurrentMode}) => {
// TODO Test doesn't work in ConcurrentMode. Haven't investigated why,
// but it seems fragile with the Queue for enforcing order.
if (concurrentMode) {
return;
}
function testWithOrder(
order: $TEMPORARY$array<
$TEMPORARY$string<'Batcher'> | $TEMPORARY$string<'SetsDuringEffect'>,
>,
) {
const anAtom = counterAtom();
let q: Array<[string, () => mixed]> = [];
let seen = false;
const original = Queue.enqueueExecution;
try {
Queue.enqueueExecution = (s, f) => {
if (s === order[0] || seen) {
seen = true;
f();
q.forEach(([_, g]) => g());
} else {
q.push([s, f]);
}
};
function SetsDuringEffect() {
const [value, setValue] = useRecoilState(anAtom);
useEffect(() => {
Queue.enqueueExecution('SetsDuringEffect', () => {
if (value !== 1) {
setValue(1);
}
});
});
return null;
}
const [Comp, updateValue] = componentThatWritesAtom(anAtom);
const container = renderElements(
<>
<SetsDuringEffect />
<ReadsAtom atom={anAtom} />
<Comp />
</>,
);
q = [];
seen = false;
// Thus it appears that it only breaks on the initial render.
act(() => {
updateValue(0);
});
expect(container.textContent).toEqual('1');
} finally {
Queue.enqueueExecution = original;
}
}
testWithOrder(['SetsDuringEffect', 'Batcher']);
testWithOrder(['Batcher', 'SetsDuringEffect']);
},
);
testRecoil('Hooks cannot be used outside of RecoilRoot', () => {
const myAtom = atom({key: 'hook outside RecoilRoot', default: 'INVALID'});
function Test() {
useRecoilValue(myAtom);
return 'TEST';
}
// Make sure there is a friendly error message mentioning <RecoilRoot>
expect(() => renderUnwrappedElements(<Test />)).toThrow('<RecoilRoot>');
});

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
flushSync,
act,
atom,
renderElements,
useRecoilState,
reactMode;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
// @fb-only: ({flushSync} = require('ReactDOMComet'));
({flushSync} = require('react-dom')); // @oss-only
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({useRecoilState} = require('../Recoil_Hooks'));
});
testRecoil('Sync React and Recoil state changes', ({gks}) => {
if (
reactMode().mode === 'MUTABLE_SOURCE' &&
!gks.includes('recoil_suppress_rerender_in_callback')
) {
return;
}
const myAtom = atom({key: 'sync react recoil', default: 0});
let setReact, setRecoil;
function Component() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] = useRecoilState(myAtom);
setReact = setReactState;
setRecoil = setRecoilState;
expect(reactState).toBe(recoilState);
return `${reactState} - ${recoilState}`;
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('0 - 0');
// Set both React and Recoil state in the same batch and ensure the component
// render always seems consistent picture of both state changes.
act(() => {
flushSync(() => {
setReact(1);
setRecoil(1);
});
});
expect(c.textContent).toBe('1 - 1');
});
testRecoil('React and Recoil state change ordering', ({gks}) => {
if (
reactMode().mode === 'MUTABLE_SOURCE' &&
!gks.includes('recoil_suppress_rerender_in_callback')
) {
return;
}
const myAtom = atom({key: 'sync react recoil', default: 0});
let setReact, setRecoil;
function Component() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] = useRecoilState(myAtom);
setReact = setReactState;
setRecoil = setRecoilState;
// State changes may not be atomic. However, render functions should
// still see state changes in the order in which they were made.
expect(reactState).toBeGreaterThanOrEqual(recoilState);
return `${reactState} - ${recoilState}`;
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('0 - 0');
// Test that changing React state before Recoil is seen in order
act(() => {
setReact(1);
setRecoil(1);
});
expect(c.textContent).toBe('1 - 1');
// Test that changing Recoil state before React is seen in order
act(() => {
setRecoil(0);
setReact(0);
});
expect(c.textContent).toBe('0 - 0');
});

View File

@@ -0,0 +1,321 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilState, RecoilValueReadOnly} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
atom,
selector,
ReadsAtom,
componentThatReadsAndWritesAtom,
renderElements,
useGetRecoilValueInfo;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
useGetRecoilValueInfo = require('../Recoil_useGetRecoilValueInfo');
});
testRecoil('useGetRecoilValueInfo', ({gks}) => {
const myAtom = atom<string>({
key: 'useGetRecoilValueInfo atom',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useGetRecoilValueInfo A',
get: ({get}) => get(myAtom),
});
const selectorB = selector({
key: 'useGetRecoilValueInfo B',
get: ({get}) => get(selectorA) + get(myAtom),
});
let getNodeInfo = (_: RecoilState<string> | RecoilValueReadOnly<string>) => {
expect(false).toBe(true);
throw new Error('getRecoilValue not set');
};
function GetRecoilValueInfo() {
const getRecoilValueInfo = useGetRecoilValueInfo();
// $FlowFixMe[incompatible-type]
getNodeInfo = node => ({...getRecoilValueInfo(node)});
return null;
}
// Initial status
renderElements(<GetRecoilValueInfo />);
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: false,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual([]);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: undefined,
isActive: false,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual([]);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual(
[],
);
}
// After reading values
const [ReadWriteAtom, setAtom, resetAtom] =
componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(
<>
<GetRecoilValueInfo />
<ReadWriteAtom />
<ReadsAtom atom={selectorB} />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULTDEFAULT"');
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([
{name: 'ReadsAndWritesAtom'},
]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([
{name: 'ReadsAtom'},
]);
}
// After setting a value
act(() => setAtom('SET'));
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: true,
isModified: true,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([
{name: 'ReadsAndWritesAtom'},
]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: expect.objectContaining({state: 'hasValue', contents: 'SET'}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'SETSET',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([
{name: 'ReadsAtom'},
]);
}
// After reseting a value
act(resetAtom);
expect(getNodeInfo(myAtom)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: true,
type: 'atom',
});
expect(Array.from(getNodeInfo(myAtom).deps)).toEqual([]);
expect(Array.from(getNodeInfo(myAtom).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorA, selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(myAtom).subscribers.components)).toEqual([
{name: 'ReadsAndWritesAtom'},
]);
}
expect(getNodeInfo(selectorA)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
expect(Array.from(getNodeInfo(selectorA).deps)).toEqual(
expect.arrayContaining([myAtom]),
);
expect(Array.from(getNodeInfo(selectorA).subscribers.nodes)).toEqual(
expect.arrayContaining([selectorB]),
);
if (gks.includes('recoil_infer_component_names')) {
expect(Array.from(getNodeInfo(selectorA).subscribers.components)).toEqual(
[],
);
}
// $FlowFixMe[incompatible-call]
expect(getNodeInfo(selectorB)).toMatchObject({
loadable: expect.objectContaining({
state: 'hasValue',
contents: 'DEFAULTDEFAULT',
}),
isActive: true,
isSet: false,
isModified: false,
type: 'selector',
});
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).deps)).toEqual(
expect.arrayContaining([myAtom, selectorA]),
);
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.nodes)).toEqual([]);
if (gks.includes('recoil_infer_component_names')) {
// $FlowFixMe[incompatible-call]
expect(Array.from(getNodeInfo(selectorB).subscribers.components)).toEqual([
{name: 'ReadsAtom'},
]);
}
});

View File

@@ -0,0 +1,281 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
freshSnapshot,
useGotoRecoilSnapshot,
useRecoilCallback,
useRecoilValue,
atom,
constSelector,
selector,
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
({freshSnapshot} = require('../../core/Recoil_Snapshot'));
({useRecoilValue} = require('../../hooks/Recoil_Hooks'));
({useGotoRecoilSnapshot} = require('../../hooks/Recoil_SnapshotHooks'));
({useRecoilCallback} = require('../../hooks/Recoil_useRecoilCallback'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
});
testRecoil('Goto mapped snapshot', async () => {
const snapshot = freshSnapshot();
snapshot.retain();
const myAtom = atom({
key: 'Goto Snapshot Atom',
default: 'DEFAULT',
});
const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const mySelector = constSelector(myAtom);
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
updatedSnapshot.retain();
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<GotoRecoilSnapshot />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
await expect(updatedSnapshot.getPromise(myAtom)).resolves.toEqual(
'SET IN SNAPSHOT',
);
act(() => gotoRecoilSnapshot(updatedSnapshot));
expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"');
act(() => setAtom('SET AGAIN IN CURRENT'));
expect(c.textContent).toEqual('"SET AGAIN IN CURRENT""SET AGAIN IN CURRENT"');
// Test that atoms set after snapshot were created are reset
act(() => gotoRecoilSnapshot(snapshot));
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
});
testRecoil('Goto callback snapshot', () => {
const myAtom = atom({
key: 'Goto Snapshot From Callback',
default: 'DEFAULT',
});
const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const mySelector = constSelector(myAtom);
let cb;
function RecoilCallback() {
const gotoSnapshot = useGotoRecoilSnapshot();
cb = useRecoilCallback(({snapshot}) => () => {
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
gotoSnapshot(updatedSnapshot);
});
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<RecoilCallback />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
act(cb);
expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"');
});
testRecoil('Goto snapshot with dependent async selector', async () => {
const snapshot = freshSnapshot();
snapshot.retain();
const myAtom = atom({
key: 'atom for dep async snapshot',
default: 'DEFAULT',
});
const [ReadsAndWritesAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'selector for async snapshot',
get: ({get}) => {
const dep = get(myAtom);
return Promise.resolve(dep);
},
});
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
updatedSnapshot.retain();
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<GotoRecoilSnapshot />
</>,
);
expect(c.textContent).toEqual('loading');
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
await expect(updatedSnapshot.getPromise(myAtom)).resolves.toEqual(
'SET IN SNAPSHOT',
);
act(() => gotoRecoilSnapshot(updatedSnapshot));
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"SET IN SNAPSHOT""SET IN SNAPSHOT"');
});
testRecoil('Goto snapshot with async selector', async () => {
const snapshot = freshSnapshot();
snapshot.retain();
const [mySelector, resolve] = asyncSelector<string, _>();
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={mySelector} />
<GotoRecoilSnapshot />
</>,
);
expect(c.textContent).toEqual('loading');
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"RESOLVE"');
act(() => gotoRecoilSnapshot(snapshot));
expect(c.textContent).toEqual('"RESOLVE"');
});
// Test that going to a snapshot where an atom was not yet initialized will
// not cause the atom to be re-initialized when used again.
testRecoil(
'Effects going to previous snapshot',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
let init = 0;
const myAtom = atom({
key: 'gotoSnapshot effect',
default: 'DEFAULT',
effects: [
() => {
init++;
},
],
});
let forceUpdate;
function ReadAtom() {
const [_, setValue] = useState<{}>({});
forceUpdate = () => setValue({});
return useRecoilValue(myAtom);
}
let gotoRecoilSnapshot;
function GotoRecoilSnapshot() {
gotoRecoilSnapshot = useGotoRecoilSnapshot();
return null;
}
expect(init).toEqual(0);
renderElements(
<>
<ReadAtom />
<GotoRecoilSnapshot />
</>,
);
expect(init).toEqual(1 * sm);
act(forceUpdate);
expect(init).toEqual(1 * sm);
act(() => gotoRecoilSnapshot?.(freshSnapshot()));
expect(init).toEqual(1 * sm);
act(forceUpdate);
expect(init).toEqual(1 * sm);
act(forceUpdate);
expect(init).toEqual(1 * sm);
},
);

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {StoreID} from '../../core/Recoil_Keys';
import type {MutableSnapshot} from 'Recoil_Snapshot';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
renderElements,
renderUnwrappedElements,
useEffect,
useRef,
reactMode,
act,
RecoilRoot,
useRecoilStoreID,
atom,
componentThatReadsAndWritesAtom,
useRecoilBridgeAcrossReactRoots;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useEffect, useRef} = React);
({act} = require('ReactTestUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
({
renderElements,
renderUnwrappedElements,
componentThatReadsAndWritesAtom,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot'));
atom = require('../../recoil_values/Recoil_atom');
useRecoilBridgeAcrossReactRoots = require('../Recoil_useRecoilBridgeAcrossReactRoots');
});
function NestedReactRoot({children}: $TEMPORARY$object<{children: Node}>) {
const ref = useRef<?HTMLDivElement>();
const RecoilBridge = useRecoilBridgeAcrossReactRoots();
useEffect(() => {
renderUnwrappedElements(
<RecoilBridge>{children}</RecoilBridge>,
ref.current,
);
}, [RecoilBridge, children]);
return <div ref={ref} />;
}
testRecoil(
'useRecoilBridgeAcrossReactRoots - create a context bridge',
async ({concurrentMode}) => {
// Test fails with useRecoilBridgeAcrossReactRoots() and useMutableSource().
// It only reproduces if act() is used in renderElements() for the nested
// root, so it may just be a testing environment issue.
if (concurrentMode && reactMode().mode === 'MUTABLE_SOURCE') {
return;
}
const myAtom = atom({
key: 'useRecoilBridgeAcrossReactRoots - context bridge',
default: 'DEFAULT',
});
function initializeState({set, getLoadable}: MutableSnapshot) {
expect(getLoadable(myAtom).contents).toEqual('DEFAULT');
set(myAtom, 'INITIALIZE');
expect(getLoadable(myAtom).contents).toEqual('INITIALIZE');
}
const [ReadWriteAtom, setAtom] = componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(
<RecoilRoot initializeState={initializeState}>
<ReadWriteAtom />
<NestedReactRoot>
<ReadWriteAtom />
</NestedReactRoot>
</RecoilRoot>,
);
expect(container.textContent).toEqual('"INITIALIZE""INITIALIZE"');
act(() => setAtom('SET'));
expect(container.textContent).toEqual('"SET""SET"');
},
);
testRecoil('StoreID matches bridged store', () => {
function RecoilStoreID({storeIDRef}: {storeIDRef: {current: ?StoreID}}) {
storeIDRef.current = useRecoilStoreID();
return null;
}
const rootStoreIDRef = {current: null};
const nestedStoreIDRef = {current: null};
const c = renderElements(
<>
<RecoilStoreID storeIDRef={rootStoreIDRef} />
<NestedReactRoot>
<RecoilStoreID storeIDRef={nestedStoreIDRef} />
</NestedReactRoot>
RENDER
</>,
);
expect(c.textContent).toEqual('RENDER');
expect(rootStoreIDRef.current).toBe(nestedStoreIDRef.current);
expect(rootStoreIDRef.current).not.toBe(null);
});

View File

@@ -0,0 +1,845 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Snapshot} from '../../core/Recoil_Snapshot';
import type {RecoilCallbackInterface} from '../Recoil_useRecoilCallback';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useRef,
useState,
useEffect,
act,
useStoreRef,
atom,
atomFamily,
selector,
useRecoilCallback,
useRecoilValue,
useRecoilState,
useSetRecoilState,
useResetRecoilState,
ReadsAtom,
flushPromisesAndTimers,
renderElements,
stringAtom,
invariant;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useRef, useState, useEffect} = require('react'));
({act} = require('ReactTestUtils'));
({useStoreRef} = require('../../core/Recoil_RecoilRoot'));
({
atom,
atomFamily,
selector,
useRecoilCallback,
useSetRecoilState,
useResetRecoilState,
useRecoilValue,
useRecoilState,
} = require('../../Recoil_index'));
({
ReadsAtom,
flushPromisesAndTimers,
renderElements,
stringAtom,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
invariant = require('recoil-shared/util/Recoil_invariant');
});
testRecoil('Reads Recoil values', async () => {
const anAtom = atom({key: 'atom1', default: 'DEFAULT'});
let pTest: ?Promise<mixed> = Promise.reject(
new Error("Callback didn't resolve"),
);
let cb;
function Component() {
cb = useRecoilCallback(({snapshot}) => () => {
// eslint-disable-next-line jest/valid-expect
pTest = expect(snapshot.getPromise(anAtom)).resolves.toBe('DEFAULT');
});
return null;
}
renderElements(<Component />);
act(() => void cb());
await pTest;
});
testRecoil('Can read Recoil values without throwing', async () => {
const anAtom = atom({key: 'atom2', default: 123});
const asyncSelector = selector({
key: 'sel',
get: () => {
return new Promise(() => undefined);
},
});
let didRun = false;
let cb;
function Component() {
cb = useRecoilCallback(({snapshot}) => () => {
expect(snapshot.getLoadable(anAtom)).toMatchObject({
state: 'hasValue',
contents: 123,
});
expect(snapshot.getLoadable(asyncSelector)).toMatchObject({
state: 'loading',
});
didRun = true; // ensure these assertions do get made
});
return null;
}
renderElements(<Component />);
act(() => void cb());
expect(didRun).toBe(true);
});
testRecoil('Sets Recoil values (by queueing them)', async () => {
const anAtom = atom({key: 'atom3', default: 'DEFAULT'});
let cb;
let pTest: ?Promise<mixed> = Promise.reject(
new Error("Callback didn't resolve"),
);
function Component() {
// $FlowFixMe[missing-local-annot]
cb = useRecoilCallback(({snapshot, set}) => value => {
set(anAtom, value);
// eslint-disable-next-line jest/valid-expect
pTest = expect(snapshot.getPromise(anAtom)).resolves.toBe('DEFAULT');
});
return null;
}
const container = renderElements(
<>
<Component />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toBe('"DEFAULT"');
act(() => void cb(123));
expect(container.textContent).toBe('123');
await pTest;
});
testRecoil('Reset Recoil values', async () => {
const anAtom = atom({key: 'atomReset', default: 'DEFAULT'});
let setCB, resetCB;
function Component() {
setCB = useRecoilCallback(
({set}) =>
// $FlowFixMe[missing-local-annot]
value =>
set(anAtom, value),
);
resetCB = useRecoilCallback(
({reset}) =>
() =>
reset(anAtom),
);
return null;
}
const container = renderElements(
<>
<Component />
<ReadsAtom atom={anAtom} />
</>,
);
expect(container.textContent).toBe('"DEFAULT"');
act(() => void setCB(123));
expect(container.textContent).toBe('123');
act(() => void resetCB());
expect(container.textContent).toBe('"DEFAULT"');
});
testRecoil('Sets Recoil values from async callback', async () => {
const anAtom = atom({key: 'set async callback', default: 'DEFAULT'});
let cb;
const pTest = [];
function Component() {
// $FlowFixMe[missing-local-annot]
cb = useRecoilCallback(({snapshot, set}) => async value => {
set(anAtom, value);
pTest.push(
// eslint-disable-next-line jest/valid-expect
expect(snapshot.getPromise(anAtom)).resolves.toBe(
value === 123 ? 'DEFAULT' : 123,
),
);
});
return null;
}
const container = renderElements([
<Component />,
<ReadsAtom atom={anAtom} />,
]);
expect(container.textContent).toBe('"DEFAULT"');
act(() => void cb(123));
expect(container.textContent).toBe('123');
act(() => void cb(456));
expect(container.textContent).toBe('456');
for (const aTest of pTest) {
await aTest;
}
});
testRecoil('Reads from a snapshot created at callback call time', async () => {
const anAtom = atom({key: 'atom4', default: 123});
let cb;
let setter;
let seenValue = null;
let delay = () => new Promise(r => r()); // no delay initially
function Component() {
setter = useSetRecoilState(anAtom);
cb = useRecoilCallback(({snapshot}) => async () => {
snapshot.retain();
await delay();
seenValue = await snapshot.getPromise(anAtom);
});
return null;
}
// It sees an update flushed after the cb is created:
renderElements(<Component />);
act(() => setter(345));
act(() => void cb());
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(seenValue).toBe(345);
// But does not see an update flushed while the cb is in progress:
seenValue = null;
let resumeCallback: () => void = () =>
invariant(false, 'must be initialized');
delay = () => {
return new Promise(resolve => {
resumeCallback = resolve;
});
};
act(() => void cb());
act(() => setter(678));
resumeCallback();
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(seenValue).toBe(345);
});
testRecoil('Setter updater sees latest state', () => {
const myAtom = atom({key: 'useRecoilCallback updater', default: 'DEFAULT'});
let setAtom;
let cb;
function Component() {
setAtom = useSetRecoilState(myAtom);
// $FlowFixMe[missing-local-annot]
cb = useRecoilCallback(({snapshot, set}) => prevValue => {
// snapshot sees a snapshot with the latest set state
expect(snapshot.getLoadable(myAtom).contents).toEqual(prevValue);
// Test that callback sees value updates from within the same transaction
set(myAtom, value => {
expect(value).toEqual(prevValue);
return 'UPDATE';
});
set(myAtom, value => {
expect(value).toEqual('UPDATE');
return 'UPDATE AGAIN';
});
});
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={myAtom} />
<Component />
</>,
);
expect(c.textContent).toEqual('"DEFAULT"');
// Set and callback in the same transaction
act(() => {
setAtom('SET');
cb('SET');
cb('UPDATE AGAIN');
});
expect(c.textContent).toEqual('"UPDATE AGAIN"');
});
testRecoil('Snapshot from effect uses rendered state', () => {
const myAtom = stringAtom();
let setState,
actCallback,
effectCallback,
actCallbackValue,
effectCallbackValue,
effectValue;
function Component() {
setState = useSetRecoilState(myAtom);
const value = useRecoilValue(myAtom);
effectCallback = useRecoilCallback(
({snapshot}) =>
() => {
effectCallbackValue = snapshot.getLoadable(myAtom).getValue();
},
[],
);
actCallback = useRecoilCallback(
({snapshot}) =>
() => {
actCallbackValue = snapshot.getLoadable(myAtom).getValue();
},
[],
);
useEffect(() => {
effectValue = value;
effectCallback();
}, [value]);
return null;
}
renderElements(<Component />);
act(() => {
setState('SET');
actCallback();
});
expect(actCallbackValue).toBe('SET');
expect(effectValue).toBe('SET');
expect(effectCallbackValue).toBe('SET');
});
testRecoil('goes to snapshot', async () => {
const myAtom = atom({
key: 'Goto Snapshot From Callback',
default: 'DEFAULT',
});
let cb;
function RecoilCallback() {
cb = useRecoilCallback(({snapshot, gotoSnapshot}) => () => {
const updatedSnapshot = snapshot.map(({set}) => {
set(myAtom, 'SET IN SNAPSHOT');
});
expect(updatedSnapshot.getLoadable(myAtom).contents).toEqual(
'SET IN SNAPSHOT',
);
gotoSnapshot(updatedSnapshot);
});
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={myAtom} />
<RecoilCallback />
</>,
);
expect(c.textContent).toEqual('"DEFAULT"');
act(() => void cb());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"SET IN SNAPSHOT"');
});
testRecoil('Updates are batched', () => {
const family = atomFamily<_, number>({
key: 'useRecoilCallback/batching/family',
default: 0,
});
let cb;
function RecoilCallback() {
cb = useRecoilCallback(({set}) => () => {
for (let i = 0; i < 100; i++) {
set(family(i), 1);
}
});
return null;
}
let store: any; // flowlint-line unclear-type:off
function GetStore() {
store = useStoreRef().current;
return null;
}
renderElements(
<>
<RecoilCallback />
<GetStore />
</>,
);
invariant(store, 'store should be initialized');
const originalReplaceState = store.replaceState;
store.replaceState = jest.fn(originalReplaceState);
expect(store.replaceState).toHaveBeenCalledTimes(0);
act(() => cb());
expect(store.replaceState).toHaveBeenCalledTimes(1);
store.replaceState = originalReplaceState;
});
// Test that we always get a consistent instance of the callback function
// from useRecoilCallback() when it is memoizaed
testRecoil('Consistent callback function', () => {
let setIteration;
const Component = () => {
const [iteration, _setIteration] = useState(0);
setIteration = _setIteration;
const callback = useRecoilCallback(() => () => {});
const callbackRef = useRef(callback);
iteration
? expect(callback).not.toBe(callbackRef.current)
: expect(callback).toBe(callbackRef.current);
const callbackMemoized = useRecoilCallback(() => () => {}, []);
const callbackMemoizedRef = useRef(callbackMemoized);
expect(callbackMemoized).toBe(callbackMemoizedRef.current);
return iteration;
};
const out = renderElements(<Component />);
expect(out.textContent).toBe('0');
act(() => setIteration(1)); // Force a re-render of the Component
expect(out.textContent).toBe('1');
});
describe('Atom Effects', () => {
testRecoil(
'Atom effects are initialized twice if first seen on snapshot and then on root store',
({strictMode, concurrentMode}) => {
const sm = strictMode ? 1 : 0;
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atomWithEffect',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
// StrictMode will render the component twice
let renderCount = 0;
const Component = () => {
const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => {
snapshot.getLoadable(atomWithEffect);
});
readAtomFromSnapshot(); // first initialization
expect(numTimesEffectInit).toBe(1 + sm * renderCount);
useRecoilValue(atomWithEffect); // second initialization
expect(numTimesEffectInit).toBe(2);
renderCount++;
return null;
};
const c = renderElements(<Component />);
expect(c.textContent).toBe(''); // Confirm no failures from rendering
expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 3 : 2);
},
);
testRecoil(
'Atom effects are initialized once if first seen on root store and then on snapshot',
({strictMode, concurrentMode}) => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atomWithEffect2',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
const Component = () => {
const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => {
snapshot.getLoadable(atomWithEffect);
});
useRecoilValue(atomWithEffect); // first initialization
expect(numTimesEffectInit).toBe(1);
/**
* should not re-initialize b/c snapshot should inherit from latest state,
* wherein atom was already initialized
*/
readAtomFromSnapshot();
expect(numTimesEffectInit).toBe(1);
return null;
};
const c = renderElements(<Component />);
expect(c.textContent).toBe(''); // Confirm no failures from rendering
expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 2 : 1);
},
);
testRecoil('onSet() called when atom initialized with snapshot', () => {
const setValues = [];
const myAtom = atom({
key: 'useRecoilCallback - atom effect - onSet',
default: 0,
effects: [
({onSet, setSelf}) => {
onSet(value => {
setValues.push(value);
// Confirm setSelf() still valid when initialized from snapshot
setSelf(value + 1);
});
},
],
});
let setAtom;
const Component = () => {
const readAtomFromSnapshot = useRecoilCallback(({snapshot}) => () => {
snapshot.getLoadable(myAtom);
});
// First initialization with snapshot
readAtomFromSnapshot();
// Second initialization with hook
let value;
[value, setAtom] = useRecoilState(myAtom);
return value;
};
const c = renderElements(<Component />);
expect(c.textContent).toBe('0');
expect(setValues).toEqual([]);
act(() => setAtom(1));
expect(setValues).toEqual([1]);
expect(c.textContent).toBe('2');
});
});
describe('Selector Cache', () => {
testRecoil('Refresh selector cache - transitive', () => {
const getA = jest.fn(() => 'A');
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilCallback refresh ancestors A',
get: getA,
});
const getB = jest.fn(({get}) => get(selectorA) + 'B');
const selectorB = selector({
key: 'useRecoilCallback refresh ancestors B',
get: getB,
});
const getC = jest.fn(({get}) => get(selectorB) + 'C');
const selectorC = selector({
key: 'useRecoilCallback refresh ancestors C',
get: getC,
});
let refreshSelector;
function Component() {
refreshSelector = useRecoilCallback(({refresh}) => () => {
refresh(selectorC);
});
return useRecoilValue(selectorC);
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
act(() => refreshSelector());
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(2);
expect(getB).toHaveBeenCalledTimes(2);
expect(getA).toHaveBeenCalledTimes(2);
});
testRecoil('Refresh selector cache - clears entire cache', async () => {
const myatom = atom({
key: 'useRecoilCallback refresh entire cache atom',
default: 'a',
});
let i = 0;
// $FlowFixMe[incompatible-call]
const myselector = selector({
key: 'useRecoilCallback refresh entire cache selector',
get: ({get}) => [get(myatom), i++],
});
let setMyAtom;
let refreshSelector;
function Component() {
const [atomValue, iValue] = useRecoilValue(myselector);
refreshSelector = useRecoilCallback(({refresh}) => () => {
refresh(myselector);
});
setMyAtom = useSetRecoilState(myatom);
return `${atomValue}-${iValue}`;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('a-0');
act(() => setMyAtom('b'));
expect(container.textContent).toBe('b-1');
act(() => refreshSelector());
expect(container.textContent).toBe('b-2');
act(() => setMyAtom('a'));
expect(container.textContent).toBe('a-3');
});
});
describe('Snapshot', () => {
testRecoil('Snapshot is retained for async callbacks', async ({gks}) => {
let callback,
callbackSnapshot,
resolveSelector,
resolveSelector2,
resolveCallback;
const myAtom = stringAtom();
// $FlowFixMe[incompatible-call]
const mySelector1 = selector({
key: 'useRecoilCallback snapshot retain 1',
get: async ({get}) => {
await new Promise(resolve => {
resolveSelector = resolve;
});
return get(myAtom);
},
});
// $FlowFixMe[incompatible-call]
const mySelector2 = selector({
key: 'useRecoilCallback snapshot retain 2',
get: async ({get}) => {
await new Promise(resolve => {
resolveSelector2 = resolve;
});
return get(myAtom);
},
});
function Component() {
callback = useRecoilCallback(({snapshot}) => async () => {
callbackSnapshot = snapshot;
return new Promise(resolve => {
resolveCallback = resolve;
});
});
return null;
}
renderElements(<Component />);
callback?.();
const selector1Promise = callbackSnapshot?.getPromise(mySelector1);
const selector2Promise = callbackSnapshot?.getPromise(mySelector2);
// Wait to allow callback snapshot to auto-release after clock tick.
// It should still be retained for duration of callback, though.
await flushPromisesAndTimers();
// Selectors resolving before callback is resolved should not be canceled
act(() => resolveSelector());
await expect(selector1Promise).resolves.toBe('DEFAULT');
// Selectors resolving after callback is resolved should be canceled
if (gks.includes('recoil_memory_managament_2020')) {
act(() => resolveCallback());
act(() => resolveSelector2());
await expect(selector2Promise).rejects.toEqual({});
}
});
testRecoil('Access snapshot asynchronously', async () => {
const myAtom = stringAtom();
let setAtom;
function Component() {
const childFunction = async (
args: RecoilCallbackInterface,
newValue: string,
) => {
const oldValue = await args.snapshot.getPromise(myAtom);
args.set(myAtom, newValue);
return oldValue;
};
const parentFunction = async (
args: RecoilCallbackInterface,
newValue: string,
) => {
await Promise.resolve();
return childFunction(args, newValue);
};
setAtom = useRecoilCallback(
// $FlowFixMe[missing-local-annot]
args => async newValue => parentFunction(args, newValue),
);
return useRecoilValue(myAtom);
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('DEFAULT');
let oldValue;
await act(async () => (oldValue = await setAtom('SET')));
expect(oldValue).toBe('DEFAULT');
expect(c.textContent).toBe('SET');
await act(async () => (oldValue = await setAtom('SET2')));
expect(oldValue).toBe('SET');
expect(c.textContent).toBe('SET2');
});
testRecoil('Snapshot is cached', () => {
const myAtom = stringAtom();
let getSnapshot;
let setMyAtom, resetMyAtom;
function Component() {
getSnapshot = useRecoilCallback(
({snapshot}) =>
() =>
snapshot,
);
setMyAtom = useSetRecoilState(myAtom);
resetMyAtom = useResetRecoilState(myAtom);
return null;
}
renderElements(<Component />);
const getAtom = (snapshot: void | Snapshot) =>
snapshot?.getLoadable(myAtom).getValue();
const initialSnapshot = getSnapshot?.();
expect(getAtom(initialSnapshot)).toEqual('DEFAULT');
// If there are no state changes, the snapshot should be cached
const nextSnapshot = getSnapshot?.();
expect(getAtom(nextSnapshot)).toEqual('DEFAULT');
expect(nextSnapshot).toBe(initialSnapshot);
// With a state change, there is a new snapshot
act(() => setMyAtom('SET'));
const setSnapshot = getSnapshot?.();
expect(getAtom(setSnapshot)).toEqual('SET');
expect(setSnapshot).not.toBe(initialSnapshot);
const nextSetSnapshot = getSnapshot?.();
expect(getAtom(nextSetSnapshot)).toEqual('SET');
expect(nextSetSnapshot).toBe(setSnapshot);
act(() => setMyAtom('SET2'));
const set2Snapshot = getSnapshot?.();
expect(getAtom(set2Snapshot)).toEqual('SET2');
expect(set2Snapshot).not.toBe(initialSnapshot);
expect(set2Snapshot).not.toBe(setSnapshot);
const nextSet2Snapshot = getSnapshot?.();
expect(getAtom(nextSet2Snapshot)).toEqual('SET2');
expect(nextSet2Snapshot).toBe(set2Snapshot);
act(() => resetMyAtom());
const resetSnapshot = getSnapshot?.();
expect(getAtom(resetSnapshot)).toEqual('DEFAULT');
expect(resetSnapshot).not.toBe(initialSnapshot);
expect(resetSnapshot).not.toBe(setSnapshot);
const nextResetSnapshot = getSnapshot?.();
expect(getAtom(nextResetSnapshot)).toEqual('DEFAULT');
expect(nextResetSnapshot).toBe(resetSnapshot);
});
testRecoil('cached snapshot is invalidated if not retained', async () => {
const myAtom = stringAtom();
let getSnapshot;
let setMyAtom;
function Component() {
getSnapshot = useRecoilCallback(
({snapshot}) =>
() =>
snapshot,
);
setMyAtom = useSetRecoilState(myAtom);
return null;
}
renderElements(<Component />);
const getAtom = (snapshot: void | Snapshot) =>
snapshot?.getLoadable(myAtom).getValue();
act(() => setMyAtom('SET'));
const setSnapshot = getSnapshot?.();
expect(getAtom(setSnapshot)).toEqual('SET');
// If cached snapshot is released, a new snapshot is provided
await flushPromisesAndTimers();
const nextSetSnapshot = getSnapshot?.();
expect(nextSetSnapshot).not.toBe(setSnapshot);
expect(getAtom(nextSetSnapshot)).toEqual('SET');
act(() => setMyAtom('SET2'));
const set2Snapshot = getSnapshot?.();
expect(getAtom(set2Snapshot)).toEqual('SET2');
expect(set2Snapshot).not.toBe(setSnapshot);
// If cached snapshot is retained, then it is used again
set2Snapshot?.retain();
await flushPromisesAndTimers();
const nextSet2Snapshot = getSnapshot?.();
expect(getAtom(nextSet2Snapshot)).toEqual('SET2');
expect(nextSet2Snapshot).toBe(set2Snapshot);
});
});

View File

@@ -0,0 +1,150 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilInterface} from 'Recoil_Hooks';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useRef,
useState,
act,
atom,
counterAtom,
renderElements,
useRecoilInterface;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useRef, useState} = require('react'));
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({useRecoilInterface} = require('../Recoil_Hooks'));
counterAtom = atom({
key: `counterAtom`,
default: 0,
});
});
testRecoil('Interface for non-react code - useRecoilState', () => {
function nonReactCode(recoilInterface: RecoilInterface) {
return recoilInterface.getRecoilState(counterAtom);
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
});
testRecoil('Interface for non-react code - useRecoilStateNoThrow', () => {
function nonReactCode(recoilInterface: RecoilInterface) {
const [loadable, setValue] =
recoilInterface.getRecoilStateLoadable(counterAtom);
const value = loadable.state === 'hasValue' ? loadable.contents : null;
return [value, setValue];
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
});
testRecoil(
'Interface for non-react code - useRecoilValue, useSetRecoilState',
() => {
function nonReactCode(recoilInterface: RecoilInterface) {
return [
recoilInterface.getRecoilValue(counterAtom),
recoilInterface.getSetRecoilState(counterAtom),
];
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
},
);
testRecoil('Interface for non-react code - useRecoilValueNoThrow', () => {
function nonReactCode(recoilInterface: RecoilInterface) {
const value = recoilInterface
.getRecoilValueLoadable(counterAtom)
.valueMaybe();
const setValue = recoilInterface.getSetRecoilState(counterAtom);
return [value, setValue];
}
let updateValue;
const Component = () => {
const recoilInterface = useRecoilInterface();
const [value, _updateValue] = nonReactCode(recoilInterface);
updateValue = _updateValue;
return value;
};
const container = renderElements(<Component />);
expect(container.textContent).toEqual('0');
act(() => updateValue(1));
expect(container.textContent).toEqual('1');
});
// Test that we always get a consistent instance of the interface object and
// hooks from useRecoilInterface() (at least for a given <AppRoot> store)
testRecoil('Consistent interface object', () => {
let setValue;
const Component = () => {
const [value, _setValue] = useState(0);
const recoilInterface = useRecoilInterface();
const recoilInterfaceRef = useRef(recoilInterface);
expect(recoilInterface).toBe(recoilInterfaceRef.current);
expect(recoilInterface.getRecoilState).toBe(recoilInterface.getRecoilState);
setValue = _setValue;
return value;
};
const out = renderElements(<Component />);
expect(out.textContent).toBe('0');
act(() => setValue(1)); // Force a re-render of the Component
expect(out.textContent).toBe('1');
});

View File

@@ -0,0 +1,147 @@
/**
* (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
atom,
selector,
renderElements,
useRecoilValue,
useSetRecoilState,
useRecoilRefresher;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
useRecoilRefresher = require('../Recoil_useRecoilRefresher');
({useRecoilValue, useSetRecoilState} = require('../Recoil_Hooks'));
});
testRecoil('useRecoilRefresher - no-op for atom', async () => {
const myAtom = atom({
key: 'useRecoilRefresher no-op',
default: 'default',
});
let refresh;
function Component() {
const value = useRecoilValue(myAtom);
refresh = useRecoilRefresher(myAtom);
return value;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('default');
act(() => refresh());
expect(container.textContent).toBe('default');
});
testRecoil('useRecoilRefresher - re-executes selector', async () => {
let i = 0;
const myselector = selector({
key: 'useRecoilRefresher re-execute',
get: () => i++,
});
let refresh;
function Component() {
const value = useRecoilValue(myselector);
refresh = useRecoilRefresher(myselector);
return value;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('0');
act(() => refresh());
expect(container.textContent).toBe('1');
});
testRecoil('useRecoilRefresher - clears entire cache', async () => {
const myatom = atom({
key: 'useRecoilRefresher entire cache atom',
default: 'a',
});
let i = 0;
// $FlowFixMe[incompatible-call]
const myselector = selector({
key: 'useRecoilRefresher entire cache selector',
get: ({get}) => [get(myatom), i++],
});
let setMyAtom;
let refresh;
function Component() {
const [atomValue, iValue] = useRecoilValue(myselector);
refresh = useRecoilRefresher(myselector);
setMyAtom = useSetRecoilState(myatom);
return `${atomValue}-${iValue}`;
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('a-0');
act(() => setMyAtom('b'));
expect(container.textContent).toBe('b-1');
act(() => refresh());
expect(container.textContent).toBe('b-2');
act(() => setMyAtom('a'));
expect(container.textContent).toBe('a-3');
});
testRecoil('useRecoilRefresher - clears ancestor selectors', async () => {
const getA = jest.fn(() => 'A');
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilRefresher ancestors A',
get: getA,
});
const getB = jest.fn(({get}) => get(selectorA) + 'B');
const selectorB = selector({
key: 'useRecoilRefresher ancestors B',
get: getB,
});
const getC = jest.fn(({get}) => get(selectorB) + 'C');
const selectorC = selector({
key: 'useRecoilRefresher ancestors C',
get: getC,
});
let refresh;
function Component() {
refresh = useRecoilRefresher(selectorC);
return useRecoilValue(selectorC);
}
const container = renderElements(<Component />);
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(1);
expect(getB).toHaveBeenCalledTimes(1);
expect(getA).toHaveBeenCalledTimes(1);
act(() => refresh());
expect(container.textContent).toBe('ABC');
expect(getC).toHaveBeenCalledTimes(2);
expect(getB).toHaveBeenCalledTimes(2);
expect(getA).toHaveBeenCalledTimes(2);
});

View File

@@ -0,0 +1,505 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useEffect,
useState,
act,
freshSnapshot,
atom,
constSelector,
selector,
ReadsAtom,
asyncSelector,
stringAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
useGotoRecoilSnapshot,
useRecoilSnapshot;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useEffect, useState} = React);
({act} = require('ReactTestUtils'));
({freshSnapshot} = require('../../core/Recoil_Snapshot'));
atom = require('../../recoil_values/Recoil_atom');
constSelector = require('../../recoil_values/Recoil_constSelector');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
asyncSelector,
stringAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({
useGotoRecoilSnapshot,
useRecoilSnapshot,
} = require('../Recoil_SnapshotHooks'));
});
testRecoil('useRecoilSnapshot - subscribe to updates', ({strictMode}) => {
if (strictMode) {
return;
}
const myAtom = stringAtom();
const [ReadsAndWritesAtom, setAtom, resetAtom] =
componentThatReadsAndWritesAtom(myAtom);
const mySelector = constSelector(myAtom);
const snapshots = [];
function RecoilSnapshotAndSubscribe() {
const snapshot = useRecoilSnapshot();
snapshot.retain();
snapshots.push(snapshot);
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<ReadsAtom atom={mySelector} />
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => setAtom('SET IN CURRENT'));
expect(c.textContent).toEqual('"SET IN CURRENT""SET IN CURRENT"');
act(resetAtom);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
expect(snapshots.length).toEqual(3);
expect(snapshots[0].getLoadable(myAtom).contents).toEqual('DEFAULT');
expect(snapshots[1].getLoadable(myAtom).contents).toEqual('SET IN CURRENT');
expect(snapshots[1].getLoadable(mySelector).contents).toEqual(
'SET IN CURRENT',
);
expect(snapshots[2].getLoadable(myAtom).contents).toEqual('DEFAULT');
});
testRecoil('useRecoilSnapshot - goto snapshots', ({strictMode}) => {
if (strictMode) {
return;
}
const atomA = atom({
key: 'useRecoilSnapshot - goto A',
default: 'DEFAULT',
});
const [ReadsAndWritesAtomA, setAtomA] =
componentThatReadsAndWritesAtom(atomA);
const atomB = atom<string | number>({
key: 'useRecoilSnapshot - goto B',
default: 'DEFAULT',
});
const [ReadsAndWritesAtomB, setAtomB] =
componentThatReadsAndWritesAtom(atomB);
const snapshots = [];
let gotoSnapshot;
function RecoilSnapshotAndSubscribe() {
gotoSnapshot = useGotoRecoilSnapshot();
const snapshot = useRecoilSnapshot();
snapshot.retain();
snapshots.push(snapshot);
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtomA />
<ReadsAndWritesAtomB />
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
// $FlowFixMe[incompatible-call]
act(() => setAtomA(1));
expect(c.textContent).toEqual('1"DEFAULT"');
act(() => setAtomB(2));
expect(c.textContent).toEqual('12');
expect(snapshots.length).toEqual(3);
act(() => gotoSnapshot(snapshots[1]));
expect(c.textContent).toEqual('1"DEFAULT"');
act(() => gotoSnapshot(snapshots[0]));
expect(c.textContent).toEqual('"DEFAULT""DEFAULT"');
act(() => gotoSnapshot(snapshots[2].map(({set}) => set(atomB, 3))));
expect(c.textContent).toEqual('13');
});
testRecoil(
'useRecoilSnapshot - async selector',
async ({strictMode, concurrentMode}) => {
const [mySelector, resolve] = asyncSelector<string, _>();
const snapshots = [];
function RecoilSnapshotAndSubscribe() {
const snapshot = useRecoilSnapshot();
snapshot.retain();
useEffect(() => {
snapshots.push(snapshot);
}, [snapshot]);
return null;
}
renderElements(<RecoilSnapshotAndSubscribe />);
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1);
act(() => resolve('RESOLVE'));
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1);
// On the first request the selector is unresolved and returns the promise
await expect(
snapshots[0].getLoadable(mySelector).contents,
).resolves.toEqual('RESOLVE');
// On the second request the resolved value is cached.
expect(snapshots[0].getLoadable(mySelector).contents).toEqual('RESOLVE');
},
);
testRecoil(
'useRecoilSnapshot - cloned async selector',
async ({strictMode, concurrentMode}) => {
const [mySelector, resolve] = asyncSelector<string, _>();
const snapshots = [];
function RecoilSnapshotAndSubscribe() {
const snapshot = useRecoilSnapshot();
snapshot.retain();
useEffect(() => {
snapshots.push(snapshot);
});
return null;
}
const c = renderElements(
<>
<React.Suspense fallback="loading">
<ReadsAtom atom={mySelector} />
</React.Suspense>
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('loading');
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 2 : 1);
act(() => resolve('RESOLVE'));
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"RESOLVE"');
expect(snapshots.length).toEqual(strictMode && concurrentMode ? 3 : 2);
// Snapshot contains cached result since it was cloned after resolved
expect(snapshots[0].getLoadable(mySelector).contents).toEqual('RESOLVE');
},
);
testRecoil('Subscriptions', async () => {
const myAtom = atom<string>({
key: 'useRecoilSnapshot Subscriptions atom',
default: 'ATOM',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilSnapshot Subscriptions A',
get: ({get}) => get(myAtom),
});
const selectorB = selector({
key: 'useRecoilSnapshot Subscriptions B',
get: ({get}) => get(selectorA) + get(myAtom),
});
const selectorC = selector({
key: 'useRecoilSnapshot Subscriptions C',
get: async ({get}) => {
const ret = get(selectorA) + get(selectorB);
await Promise.resolve();
return ret;
},
});
let snapshot = freshSnapshot();
function RecoilSnapshot() {
snapshot = useRecoilSnapshot();
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={selectorC} />
<RecoilSnapshot />
</>,
);
await flushPromisesAndTimers();
expect(c.textContent).toBe('"ATOMATOMATOM"');
expect(
Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes).length,
).toBe(3);
expect(
Array.from(snapshot.getInfo_UNSTABLE(myAtom).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorA, selectorB, selectorC]));
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes).length,
).toBe(2);
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorA).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorB, selectorC]));
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes).length,
).toBe(1);
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorB).subscribers.nodes),
).toEqual(expect.arrayContaining([selectorC]));
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes).length,
).toBe(0);
expect(
Array.from(snapshot.getInfo_UNSTABLE(selectorC).subscribers.nodes),
).toEqual(expect.arrayContaining([]));
});
describe('Snapshot Retention', () => {
testRecoil('Retained for duration component is mounted', async () => {
let retainedDuringEffect = false;
let setMount;
let checkRetention;
function UseRecoilSnapshot() {
const snapshot = useRecoilSnapshot();
expect(snapshot.isRetained()).toBe(true);
useEffect(() => {
retainedDuringEffect = snapshot.isRetained();
});
checkRetention = () => snapshot.isRetained();
return null;
}
function Component() {
const [mount, setMountState] = useState(false);
setMount = setMountState;
return mount ? <UseRecoilSnapshot /> : null;
}
renderElements(<Component />);
expect(retainedDuringEffect).toBe(false);
act(() => setMount(true));
expect(retainedDuringEffect).toBe(true);
expect(checkRetention?.()).toBe(true);
act(() => setMount(false));
await flushPromisesAndTimers();
expect(checkRetention?.()).toBe(false);
});
testRecoil('Snapshot auto-release', async ({gks}) => {
let rootFirstCnt = 0;
const rootFirstAtom = atom({
key: 'useRecoilSnapshot auto-release root-first',
default: 'DEFAULT',
effects: [
({setSelf}) => {
rootFirstCnt++;
setSelf('ROOT');
return () => {
rootFirstCnt--;
};
},
],
});
let snapshotFirstCnt = 0;
const snapshotFirstAtom = atom({
key: 'useRecoilSnapshot auto-release snapshot-first',
default: 'DEFAULT',
effects: [
({setSelf}) => {
snapshotFirstCnt++;
setSelf('SNAPSHOT FIRST');
return () => {
snapshotFirstCnt--;
};
},
],
});
let snapshotOnlyCnt = 0;
const snapshotOnlyAtom = atom({
key: 'useRecoilSnapshot auto-release snapshot-only',
default: 'DEFAULT',
effects: [
({setSelf}) => {
snapshotOnlyCnt++;
setSelf('SNAPSHOT ONLY');
return () => {
snapshotOnlyCnt--;
};
},
],
});
let rootOnlyCnt = 0;
const rootOnlyAtom = atom({
key: 'useRecoilSnapshot auto-release root-only',
default: 'DEFAULT',
effects: [
({setSelf}) => {
rootOnlyCnt++;
setSelf('RETAIN');
return () => {
rootOnlyCnt--;
};
},
],
});
let setMount: boolean => void = () => {
throw new Error('Test Error');
};
function UseRecoilSnapshot() {
const snapshot = useRecoilSnapshot();
return (
snapshot.getLoadable(snapshotFirstAtom).getValue() +
snapshot.getLoadable(snapshotOnlyAtom).getValue()
);
}
function Component() {
const [mount, setState] = useState(false);
setMount = setState;
return mount ? (
<>
<ReadsAtom atom={rootOnlyAtom} />
<ReadsAtom atom={rootFirstAtom} />
<UseRecoilSnapshot />
<ReadsAtom atom={snapshotFirstAtom} />
</>
) : (
<ReadsAtom atom={rootOnlyAtom} />
);
}
const c = renderElements(<Component />);
expect(c.textContent).toBe('"RETAIN"');
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(0);
expect(rootFirstCnt).toBe(0);
expect(snapshotFirstCnt).toBe(0);
act(() => setMount(true));
expect(c.textContent).toBe(
'"RETAIN""ROOT"SNAPSHOT FIRSTSNAPSHOT ONLY"SNAPSHOT FIRST"',
);
await flushPromisesAndTimers();
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(1);
expect(rootFirstCnt).toBe(1);
expect(snapshotFirstCnt).toBe(2);
// Confirm snapshot isn't released until component is unmounted
await flushPromisesAndTimers();
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(1);
expect(rootFirstCnt).toBe(1);
expect(snapshotFirstCnt).toBe(2);
// Auto-release snapshot
act(() => setMount(false));
await flushPromisesAndTimers();
expect(c.textContent).toBe('"RETAIN"');
expect(rootOnlyCnt).toBe(1);
expect(snapshotOnlyCnt).toBe(0);
if (gks.includes('recoil_memory_management_2020')) {
expect(rootFirstCnt).toBe(0);
expect(snapshotFirstCnt).toBe(0);
}
});
});
testRecoil('useRecoilSnapshot - re-render', () => {
const myAtom = stringAtom();
const [ReadsAndWritesAtom, setAtom, resetAtom] =
componentThatReadsAndWritesAtom(myAtom);
const snapshots = [];
let forceUpdate;
function RecoilSnapshotAndSubscribe() {
const [, setState] = useState(([]: Array<$FlowFixMe>));
forceUpdate = () => setState([]);
const snapshot = useRecoilSnapshot();
snapshots.push(snapshot);
return null;
}
const c = renderElements(
<>
<ReadsAndWritesAtom />
<RecoilSnapshotAndSubscribe />
</>,
);
expect(c.textContent).toEqual('"DEFAULT"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
act(forceUpdate);
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
act(() => setAtom('SET'));
expect(c.textContent).toEqual('"SET"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'SET',
);
act(forceUpdate);
expect(c.textContent).toEqual('"SET"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'SET',
);
act(resetAtom);
expect(c.textContent).toEqual('"DEFAULT"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
act(forceUpdate);
expect(c.textContent).toEqual('"DEFAULT"');
expect(snapshots[snapshots.length - 1].getLoadable(myAtom).contents).toBe(
'DEFAULT',
);
});

View File

@@ -0,0 +1,214 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
atom,
atomFamily,
selector,
selectorFamily,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
atomFamily = require('../../recoil_values/Recoil_atomFamily');
selector = require('../../recoil_values/Recoil_selector');
selectorFamily = require('../../recoil_values/Recoil_selectorFamily');
({
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
});
testRecoil('useRecoilValueReset - value default', () => {
const myAtom = atom({
key: 'useResetRecoilState/atom',
default: 'default',
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
});
testRecoil('useResetRecoilState - sync selector default', () => {
const mySelector = selector({
key: 'useResetRecoilState/sync_selector/default',
get: () => 'fallback',
});
const myAtom = atom({
key: 'useResetRecoilState/sync_selector',
default: mySelector,
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"fallback"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"fallback"');
});
// Test resetting an atom to a fallback selector with a pending async value
testRecoil('useResetRecoilState - async selector default', () => {
const [mySelector, resolve] = asyncSelector<string, _>();
const myAtom = atom({
key: 'useResetRecoilState/async_selector',
default: mySelector,
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('loading');
act(() => setValue('set value'));
act(() => jest.runAllTimers()); // Hmm, interesting this is required
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('loading');
act(() => resolve('resolved fallback'));
act(() => jest.runAllTimers());
expect(container.textContent).toBe('"resolved fallback"');
});
// Test resetting an atom to a fallback selector with a pending async value
testRecoil('useResetRecoilState - scoped atom', () => {
return; // @oss-only
const myAtom = atom({
key: 'useResetRecoilState/scoped_atom',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [
[atom({key: 'useResetRecoilState/scoped_atom/scope_rule', default: 0})],
],
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(myAtom);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
// TODO test resetting a scoped atom that was upgraded with a new rule
});
// Test resetting an atom to a fallback selector with a pending async value
testRecoil('useResetRecoilState - atom family', () => {
const myAtom = atomFamily<
_,
{default: string} | {default: string, secondParam: string},
>({
key: 'useResetRecoilState/atomFamily',
// $FlowFixMe[missing-local-annot]
default: ({default: def}) => def,
});
const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(
myAtom({default: 'default'}),
);
const [ComponentB, setValueB, resetValueB] = componentThatReadsAndWritesAtom(
myAtom({default: 'default', secondParam: 'superset'}),
);
const container = renderElements(
<>
<Component />
<ComponentB />
</>,
);
expect(container.textContent).toBe('"default""default"');
// $FlowFixMe[incompatible-call]
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value""default"');
act(() => resetValue());
expect(container.textContent).toBe('"default""default"');
// $FlowFixMe[incompatible-call]
act(() => setValue('set value A'));
expect(container.textContent).toBe('"set value A""default"');
// $FlowFixMe[incompatible-call]
act(() => setValueB('set value B'));
expect(container.textContent).toBe('"set value A""set value B"');
act(() => resetValueB());
expect(container.textContent).toBe('"set value A""default"');
});
testRecoil('useResetRecoilState - selector', () => {
const myAtom = atom({
key: 'useResetRecoilState/selector/atom',
default: 'default',
});
// $FlowFixMe[incompatible-call]
const mySelector = selector({
key: 'useResetRecoilState/selector',
get: ({get}) => get(myAtom),
// $FlowFixMe[incompatible-call]
set: ({set}, value) => set(myAtom, value),
});
const [Component, setValue, resetValue] =
componentThatReadsAndWritesAtom(mySelector);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
});
testRecoil('useResetRecoilState - parameterized selector', () => {
const myAtom = atom({
key: 'useResetRecoilState/parameterized_selector/atom',
default: 'default',
});
const mySelector = selectorFamily<_, string>({
key: 'useResetRecoilState/parameterized_selector',
get:
() =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom),
set:
() =>
// $FlowFixMe[missing-local-annot]
({set}, value) =>
set(myAtom, value),
});
const [Component, setValue, resetValue] = componentThatReadsAndWritesAtom(
mySelector('parameter'),
);
const container = renderElements(<Component />);
expect(container.textContent).toBe('"default"');
act(() => setValue('set value'));
expect(container.textContent).toBe('"set value"');
act(() => resetValue());
expect(container.textContent).toBe('"default"');
});

View File

@@ -0,0 +1,367 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {act} = require('ReactTestUtils');
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
useEffect,
atom,
useRecoilValue,
useRecoilState,
useRecoilTransaction,
useRecoilSnapshot,
renderElements,
flushPromisesAndTimers,
ReadsAtom;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState, useEffect} = React);
({
atom,
useRecoilValue,
useRecoilState,
useRecoilTransaction_UNSTABLE: useRecoilTransaction,
useRecoilSnapshot,
} = require('../../Recoil_index'));
({
renderElements,
flushPromisesAndTimers,
ReadsAtom,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
});
describe('Atoms', () => {
testRecoil('Get with transaction', () => {
const myAtom = atom({
key: 'useRecoilTransaction atom get',
default: 'DEFAULT',
});
let readAtom;
let ranTransaction = false;
function Component() {
readAtom = useRecoilTransaction(({get}) => () => {
expect(get(myAtom)).toEqual('DEFAULT');
ranTransaction = true;
});
return null;
}
renderElements(<Component />);
expect(ranTransaction).toBe(false);
act(readAtom);
expect(ranTransaction).toBe(true);
});
testRecoil('Set with transaction', () => {
const myAtom = atom<string>({
key: 'useRecoilTransaction atom set',
default: 'DEFAULT',
});
function Component() {
// $FlowFixMe[missing-local-annot]
const transact = useRecoilTransaction(({set, get}) => value => {
set(myAtom, 'TMP');
expect(get(myAtom)).toEqual('TMP');
set(myAtom, old => {
expect(old).toEqual('TMP');
return value;
});
expect(get(myAtom)).toEqual(value);
});
useEffect(() => {
transact('TRANSACT');
});
return null;
}
const c = renderElements(
<>
<ReadsAtom atom={myAtom} />
<Component />
</>,
);
expect(c.textContent).toEqual('"TRANSACT"');
});
testRecoil('Dirty atoms', async () => {
const beforeAtom = atom({
key: 'useRecoilTransaction dirty before',
default: 'DEFAULT',
});
const duringAtomA = atom({
key: 'useRecoilTransaction dirty during A',
default: 'DEFAULT',
});
const duringAtomB = atom({
key: 'useRecoilTransaction dirty during B',
default: 'DEFAULT',
});
const afterAtom = atom({
key: 'useRecoilTransaction dirty after',
default: 'DEFAULT',
});
let snapshot;
let firstEffect = true;
function Component() {
const [beforeValue, setBefore] = useState('INITIAL');
const [beforeAtomValue, setBeforeAtom] = useRecoilState(beforeAtom);
const duringAValue = useRecoilValue(duringAtomA);
const duringBValue = useRecoilValue(duringAtomB);
const [afterAtomValue, setAfterAtom] = useRecoilState(afterAtom);
const [afterValue, setAfter] = useState('INITIAL');
const transaction = useRecoilTransaction(({set, get}) => () => {
expect(get(beforeAtom)).toEqual('BEFORE');
expect(get(duringAtomA)).toEqual('DEFAULT');
expect(get(duringAtomB)).toEqual('DEFAULT');
expect(get(afterAtom)).toEqual('DEFAULT');
set(duringAtomA, 'DURING_A');
set(duringAtomB, 'DURING_B');
});
snapshot = useRecoilSnapshot();
useEffect(() => {
setTimeout(() => {
act(() => {
if (firstEffect) {
setBefore('BEFORE');
setBeforeAtom('BEFORE');
transaction();
setAfterAtom('AFTER');
setAfter('AFTER');
}
firstEffect = false;
});
}, 0);
});
return [
beforeValue,
beforeAtomValue,
duringAValue,
duringBValue,
afterAtomValue,
afterValue,
].join(',');
}
const c = renderElements(<Component />);
expect(c.textContent).toBe(
'INITIAL,DEFAULT,DEFAULT,DEFAULT,DEFAULT,INITIAL',
);
expect(
Array.from(snapshot?.getNodes_UNSTABLE({isModified: true}) ?? []),
).toEqual([]);
await flushPromisesAndTimers();
expect(c.textContent).toBe('BEFORE,BEFORE,DURING_A,DURING_B,AFTER,AFTER');
expect(
Array.from(snapshot?.getNodes_UNSTABLE({isModified: true}) ?? []).map(
({key}) => key,
),
).toEqual([
'useRecoilTransaction dirty before',
'useRecoilTransaction dirty during A',
'useRecoilTransaction dirty during B',
'useRecoilTransaction dirty after',
]);
});
});
describe('Atom Effects', () => {
testRecoil(
'Atom effects are run when first get from a transaction',
async () => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect first get transaction',
default: 'DEFAULT',
effects: [
({trigger}) => {
expect(trigger).toEqual('get');
numTimesEffectInit++;
},
],
});
let getAtomWithTransaction;
let ranTransaction = false;
const Component = () => {
getAtomWithTransaction = useRecoilTransaction(({get}) => () => {
expect(get(atomWithEffect)).toEqual('DEFAULT');
ranTransaction = true;
});
return null;
};
renderElements(<Component />);
act(() => getAtomWithTransaction());
expect(ranTransaction).toBe(true);
expect(numTimesEffectInit).toBe(1);
},
);
testRecoil(
'Atom effects are run when first set with a transaction',
async ({strictMode, concurrentMode}) => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect first set transaction',
default: 'DEFAULT',
effects: [
({trigger}) => {
expect(trigger).toEqual('set');
numTimesEffectInit++;
},
],
});
let setAtomWithTransaction;
const Component = () => {
setAtomWithTransaction = useRecoilTransaction(({set}) => () => {
set(atomWithEffect, 'SET');
});
useEffect(() => {
act(setAtomWithTransaction);
});
return null;
};
renderElements(<Component />);
expect(numTimesEffectInit).toBe(strictMode && concurrentMode ? 2 : 1);
},
);
testRecoil('Atom effects can initialize for a transaction', async () => {
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect init transaction',
default: 'DEFAULT',
effects: [
({setSelf}) => {
setSelf('INIT');
numTimesEffectInit++;
},
],
});
let initAtomWithTransaction;
let ranTransaction = false;
const Component = () => {
initAtomWithTransaction = useRecoilTransaction(({get}) => () => {
expect(get(atomWithEffect)).toEqual('INIT');
ranTransaction = true;
});
return null;
};
renderElements(<Component />);
act(() => initAtomWithTransaction());
expect(ranTransaction).toBe(true);
expect(numTimesEffectInit).toBe(1);
});
testRecoil(
'Atom effects are initialized once if first seen on transaction and then on root store',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'useRecoilTransaction effect first get transaction',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
const Component = () => {
const readAtomFromSnapshot = useRecoilTransaction(({get}) => () => {
get(atomWithEffect);
});
readAtomFromSnapshot(); // first initialization
expect(numTimesEffectInit).toBeGreaterThanOrEqual(1);
const effectsRan = numTimesEffectInit;
/**
* Transactions do not use a snapshot under the hood, so any initialized
* effects from a transaction will be reflected in root store
*/
useRecoilValue(atomWithEffect);
expect(numTimesEffectInit).toBe(effectsRan);
return 'RENDERED';
};
const c = renderElements(<Component />);
expect(c.textContent).toBe('RENDERED');
expect(numTimesEffectInit).toBe(1 * sm);
},
);
testRecoil(
'Atom effects are initialized once if first seen on root store and then on snapshot',
({strictMode, concurrentMode}) => {
const sm = strictMode && concurrentMode ? 2 : 1;
let numTimesEffectInit = 0;
const atomWithEffect = atom({
key: 'atom effect first get root',
default: 0,
effects: [
() => {
numTimesEffectInit++;
},
],
});
const Component = () => {
const readAtomFromSnapshot = useRecoilTransaction(({get}) => () => {
get(atomWithEffect);
});
useRecoilValue(atomWithEffect); // first initialization
expect(numTimesEffectInit).toBeGreaterThanOrEqual(1);
const effectsRan = numTimesEffectInit;
/**
* Transactions do not use a snapshot under the hood, so any initialized
* effects from a transaction will be reflected in root store
*/
readAtomFromSnapshot();
expect(numTimesEffectInit).toBe(effectsRan);
return 'RENDERED';
};
const c = renderElements(<Component />);
expect(c.textContent).toBe('RENDERED');
expect(numTimesEffectInit).toBe(1 * sm);
},
);
});

View File

@@ -0,0 +1,302 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall perf_viz
*/
'use strict';
import type {Snapshot} from 'Recoil_Snapshot';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
freshSnapshot,
atom,
atomFamily,
selector,
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
useRecoilTransactionObserver;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
({freshSnapshot} = require('../../core/Recoil_Snapshot'));
atom = require('../../recoil_values/Recoil_atom');
atomFamily = require('../../recoil_values/Recoil_atomFamily');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
asyncSelector,
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({useRecoilTransactionObserver} = require('../Recoil_SnapshotHooks'));
});
function TransactionObserver({
callback,
}: $TEMPORARY$object<{
callback: ({previousSnapshot: Snapshot, snapshot: Snapshot}) => void,
}>) {
useRecoilTransactionObserver(callback);
return null;
}
// Run test first since it deals with all registered atoms
testRecoil('getNodes', () => {
let snapshot = freshSnapshot();
function UseRecoilTransactionObserver() {
useRecoilTransactionObserver(p => {
p.snapshot.retain();
snapshot = p.snapshot;
});
return null;
}
const atoms = atomFamily<string, string>({
key: 'useRecoilTransactionObserver getNodes atom',
default: x => x,
});
const [ReadsAtomA, setAtomA, resetAtomA] = componentThatReadsAndWritesAtom(
atoms('A'),
);
const [ReadsAtomB, setAtomB] = componentThatReadsAndWritesAtom(atoms('B'));
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'useRecoilTransactionObserver getNodes selector',
get: ({get}) => get(atoms('A')) + '-SELECTOR',
});
const c = renderElements(
<>
<ReadsAtomA />
<ReadsAtomB />
<ReadsAtom atom={selectorA} />
<UseRecoilTransactionObserver />
</>,
);
expect(c.textContent).toEqual('"A""B""A-SELECTOR"');
expect(
Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true})).length,
).toEqual(0);
act(() => setAtomA('A'));
// >= 3 because we expect at least nodes for atom's A and B from
// the family and selectorA. In reality we could get more due to internal
// helper selectors and default fallback atoms.
expect(
Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true})).length,
).toBeGreaterThanOrEqual(3);
const nodes = Array.from(snapshot.getNodes_UNSTABLE({isInitialized: true}));
expect(nodes).toEqual(
expect.arrayContaining([atoms('A'), atoms('B'), selectorA]),
);
// Test atom A is set
const aDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true}));
expect(aDirty.length).toEqual(1);
expect(snapshot.getLoadable(aDirty[0]).contents).toEqual('A');
// Test atom B is set
act(() => setAtomB('B'));
const bDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true}));
expect(bDirty.length).toEqual(1);
expect(snapshot.getLoadable(bDirty[0]).contents).toEqual('B');
// Test atoms
const atomNodes = Array.from(
snapshot.getNodes_UNSTABLE({isInitialized: true}),
);
expect(atomNodes.map(atom => snapshot.getLoadable(atom).contents)).toEqual(
expect.arrayContaining(['A', 'B']),
);
// Test selector
const selectorNodes = Array.from(
snapshot.getNodes_UNSTABLE({isInitialized: true}),
);
expect(
selectorNodes.map(atom => snapshot.getLoadable(atom).contents),
).toEqual(expect.arrayContaining(['A-SELECTOR']));
// Test Reset
act(resetAtomA);
const resetDirty = Array.from(snapshot.getNodes_UNSTABLE({isModified: true}));
expect(resetDirty.length).toEqual(1);
expect(resetDirty[0]).toBe(aDirty[0]);
// TODO Test dirty selectors
});
testRecoil('Can observe atom value', async () => {
const atomA = atom({
key: 'Observe Atom A',
default: 'DEFAULT A',
});
const atomB = atom({
key: 'Observe Atom B',
default: 'DEFAULT B',
});
const [WriteAtomA, setAtomA, resetA] = componentThatReadsAndWritesAtom(atomA);
const [WriteAtomB, setAtomB] = componentThatReadsAndWritesAtom(atomB);
const transactions = [];
renderElements(
<>
<TransactionObserver
callback={({snapshot, previousSnapshot}) => {
snapshot.retain();
previousSnapshot.retain();
transactions.push({snapshot, previousSnapshot});
}}
/>
<WriteAtomA />
<WriteAtomB />
</>,
);
act(() => setAtomB('SET B'));
expect(transactions.length).toEqual(1);
await expect(transactions[0].snapshot.getPromise(atomA)).resolves.toEqual(
'DEFAULT A',
);
await expect(
transactions[0].previousSnapshot.getPromise(atomA),
).resolves.toEqual('DEFAULT A');
await expect(transactions[0].snapshot.getPromise(atomB)).resolves.toEqual(
'SET B',
);
await expect(
transactions[0].previousSnapshot.getPromise(atomB),
).resolves.toEqual('DEFAULT B');
act(() => setAtomA('SET A'));
expect(transactions.length).toEqual(2);
await expect(transactions[1].snapshot.getPromise(atomA)).resolves.toEqual(
'SET A',
);
await expect(
transactions[1].previousSnapshot.getPromise(atomA),
).resolves.toEqual('DEFAULT A');
await expect(transactions[1].snapshot.getPromise(atomB)).resolves.toEqual(
'SET B',
);
await expect(
transactions[1].previousSnapshot.getPromise(atomB),
).resolves.toEqual('SET B');
act(() => resetA());
expect(transactions.length).toEqual(3);
await expect(transactions[2].snapshot.getPromise(atomA)).resolves.toEqual(
'DEFAULT A',
);
await expect(
transactions[2].previousSnapshot.getPromise(atomA),
).resolves.toEqual('SET A');
await expect(transactions[2].snapshot.getPromise(atomB)).resolves.toEqual(
'SET B',
);
await expect(
transactions[2].previousSnapshot.getPromise(atomB),
).resolves.toEqual('SET B');
});
testRecoil('Can observe selector value', async () => {
const atomA = atom({
key: 'Observe Atom for Selector',
default: 'DEFAULT',
});
// $FlowFixMe[incompatible-call]
const selectorA = selector({
key: 'Observer Selector As',
get: ({get}) => `SELECTOR ${get(atomA)}`,
});
const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(atomA);
const transactions = [];
renderElements(
<>
<TransactionObserver
callback={({snapshot, previousSnapshot}) => {
snapshot.retain();
previousSnapshot.retain();
transactions.push({snapshot, previousSnapshot});
}}
/>
<WriteAtom />
</>,
);
act(() => setAtom('SET'));
expect(transactions.length).toEqual(1);
await expect(transactions[0].snapshot.getPromise(atomA)).resolves.toEqual(
'SET',
);
await expect(
transactions[0].previousSnapshot.getPromise(atomA),
).resolves.toEqual('DEFAULT');
await expect(transactions[0].snapshot.getPromise(selectorA)).resolves.toEqual(
'SELECTOR SET',
);
await expect(
transactions[0].previousSnapshot.getPromise(selectorA),
).resolves.toEqual('SELECTOR DEFAULT');
});
testRecoil('Can observe async selector value', async () => {
const atomA = atom({
key: 'Observe Atom for Async Selector',
default: 'DEFAULT',
});
const [WriteAtom, setAtom] = componentThatReadsAndWritesAtom(atomA);
const [selectorA, resolveA] = asyncSelector<string, _>();
const transactions = [];
renderElements(
<>
<TransactionObserver
callback={({snapshot, previousSnapshot}) => {
snapshot.retain();
previousSnapshot.retain();
transactions.push({snapshot, previousSnapshot});
}}
/>
<WriteAtom />
</>,
);
act(() => setAtom('SET'));
expect(transactions.length).toEqual(1);
expect(transactions[0].snapshot.getLoadable(selectorA).state).toEqual(
'loading',
);
act(() => resolveA('RESOLVE'));
expect(transactions.length).toEqual(1);
await expect(transactions[0].snapshot.getPromise(selectorA)).resolves.toEqual(
'RESOLVE',
);
});

View File

@@ -0,0 +1,215 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
act,
selector,
constSelector,
errorSelector,
asyncSelector,
renderElements,
useRecoilValueLoadable,
flushPromisesAndTimers;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({act} = require('ReactTestUtils'));
constSelector = require('../../recoil_values/Recoil_constSelector');
errorSelector = require('../../recoil_values/Recoil_errorSelector');
selector = require('../../recoil_values/Recoil_selector');
({
asyncSelector,
renderElements,
flushPromisesAndTimers,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({useRecoilValueLoadable} = require('../Recoil_Hooks'));
});
// These tests should cover the Loadable interface returned by useRecoilValueLoadable.
// It is also used by useRecoilStateNoThrow, waitForNone, and waitForAny
testRecoil('useRecoilValueLoadable - loadable with value', async () => {
const valueSel = constSelector('VALUE');
let promise;
function ReadLoadable() {
const loadable = useRecoilValueLoadable(valueSel);
expect(loadable.state).toBe('hasValue');
expect(loadable.contents).toBe('VALUE');
expect(loadable.getValue()).toBe('VALUE');
// eslint-disable-next-line jest/valid-expect
promise = expect(loadable.toPromise()).resolves.toBe('VALUE');
expect(loadable.valueMaybe()).toBe('VALUE');
expect(loadable.valueOrThrow()).toBe('VALUE');
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow(Error);
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow(Error);
return loadable.valueOrThrow();
}
const c = renderElements(<ReadLoadable />);
expect(c.textContent).toEqual('VALUE');
await promise;
});
testRecoil('useRecoilValueLoadable - loadable with error', async () => {
const valueSel = errorSelector<$FlowFixMe>('ERROR');
let promise;
function ReadLoadable() {
const loadable = useRecoilValueLoadable(valueSel);
expect(loadable.state).toBe('hasError');
expect(loadable.contents).toBeInstanceOf(Error);
expect(() => loadable.getValue()).toThrow('ERROR');
// eslint-disable-next-line jest/valid-expect
promise = expect(loadable.toPromise()).rejects.toBeInstanceOf(Error);
expect(loadable.valueMaybe()).toBe(undefined);
expect(() => loadable.valueOrThrow()).toThrow(Error);
expect(String(loadable.errorMaybe() ?? {})).toContain('ERROR');
expect(loadable.errorOrThrow()).toBeInstanceOf(Error);
expect(String(loadable.errorOrThrow())).toContain('ERROR');
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow(Error);
return 'VALUE';
}
const c = renderElements(<ReadLoadable />);
expect(c.textContent).toEqual('VALUE');
await promise;
});
testRecoil('useRecoilValueLoadable - loading loadable', async () => {
const [valueSel, resolve] = asyncSelector<string, _>();
let resolved = false;
const promises = [];
function ReadLoadable() {
const loadable = useRecoilValueLoadable(valueSel);
if (!resolved) {
expect(loadable.state).toBe('loading');
expect(loadable.contents).toBeInstanceOf(Promise);
expect(() => loadable.getValue()).toThrow();
try {
loadable.getValue();
} catch (promise) {
promises.push(promise);
}
promises.push(loadable.toPromise());
expect(loadable.valueMaybe()).toBe(undefined);
expect(() => loadable.valueOrThrow()).toThrow(Error);
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow(Error);
expect(loadable.promiseMaybe()).toBeInstanceOf(Promise);
promises.push(loadable.promiseMaybe());
return 'LOADING';
} else {
expect(loadable.state).toBe('hasValue');
expect(loadable.contents).toBe('VALUE');
expect(loadable.getValue()).toBe('VALUE');
promises.push(loadable.toPromise());
expect(loadable.valueMaybe()).toBe('VALUE');
expect(loadable.valueOrThrow()).toBe('VALUE');
expect(loadable.errorMaybe()).toBe(undefined);
expect(() => loadable.errorOrThrow()).toThrow(Error);
expect(loadable.promiseMaybe()).toBe(undefined);
expect(() => loadable.promiseOrThrow()).toThrow(Error);
return loadable.valueOrThrow();
}
}
const c = renderElements(<ReadLoadable />);
expect(c.textContent).toEqual('LOADING');
resolve('VALUE');
resolved = true;
act(() => jest.runAllTimers());
expect(c.textContent).toEqual('VALUE');
await Promise.all(
promises.map(async promise => {
if (!(promise instanceof Promise)) {
// for flow
throw new Error('Expected a promise');
}
const res = await promise;
const val = typeof res === 'string' ? res : res.__value;
expect(val).toBe('VALUE');
}),
);
});
testRecoil(
'useRecoilValueLoadable() with an async throwing selector results in a loadable in error state',
async () => {
const asyncError = selector<mixed>({
key: 'asyncError',
get: async () => {
throw new Error('Test Error');
},
});
const Test = () => {
const loadable = useRecoilValueLoadable(asyncError);
return (
<h1>{loadable?.state === 'hasError' ? 'Has error' : 'No error'}</h1>
);
};
const c = renderElements(<Test />);
await act(() => flushPromisesAndTimers());
expect(c.textContent).toEqual('Has error');
},
);
// Test that an async selector can depend on an async selector dependency
// and include async post-processing.
testRecoil('two level async', async () => {
const level2 = selector({
key: 'useRecoilValueLoadable async level2',
// $FlowFixMe[incompatible-call]
get: () => new Promise(resolve => setTimeout(() => resolve('level2'))),
});
// $FlowFixMe[incompatible-call]
const level1 = selector({
key: 'useRecoilValueLoadable async level1',
get: async ({get}) => {
const level2Value = get(level2);
return await new Promise(resolve =>
// $FlowFixMe[incompatible-call]
// $FlowFixMe[incompatible-type]
setTimeout(() => resolve(`level1 + ${level2Value}`)),
);
},
});
const promises = [];
function ReadPromise() {
const loadable = useRecoilValueLoadable(level1);
promises.push(loadable.toPromise());
return loadable.getValue();
}
const c = renderElements(<ReadPromise />);
expect(c.textContent).toEqual('loading');
await flushPromisesAndTimers();
await flushPromisesAndTimers();
await flushPromisesAndTimers();
await flushPromisesAndTimers();
expect(c.textContent).toEqual('level1 + level2');
await Promise.all(
promises.map(promise => expect(promise).resolves.toBe('level1 + level2')),
);
});

View File

@@ -0,0 +1,270 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {
RecoilState,
RecoilValue,
RecoilValueReadOnly,
} from '../../core/Recoil_RecoilValue';
import type {PersistenceSettings} from '../../recoil_values/Recoil_atom';
import type {NodeKey} from 'Recoil_Keys';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
atom,
selector,
ReadsAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
useTransactionObservation_DEPRECATED;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
atom = require('../../recoil_values/Recoil_atom');
selector = require('../../recoil_values/Recoil_selector');
({
ReadsAtom,
flushPromisesAndTimers,
renderElements,
renderUnwrappedElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
} = require('../Recoil_Hooks'));
({useTransactionObservation_DEPRECATED} = require('../Recoil_SnapshotHooks'));
});
let nextID = 0;
function counterAtom(persistence?: PersistenceSettings<number>) {
return atom({
key: `atom${nextID++}`,
default: 0,
persistence_UNSTABLE: persistence,
});
}
function plusOneSelector(dep: RecoilValue<number>) {
const fn = jest.fn(x => x + 1);
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [sel, fn];
}
function plusOneAsyncSelector(
dep: RecoilValue<number>,
): [RecoilValueReadOnly<number>, (number) => void] {
let nextTimeoutAmount = 100;
const fn = jest.fn(x => {
return new Promise(resolve => {
setTimeout(() => {
resolve(x + 1);
}, nextTimeoutAmount);
});
});
const sel = selector({
key: `selector${nextID++}`,
get: ({get}) => fn(get(dep)),
});
return [
// $FlowFixMe[incompatible-return]
sel,
x => {
nextTimeoutAmount = x;
},
];
}
function componentThatWritesAtom<T>(
recoilState: RecoilState<T>,
// flowlint-next-line unclear-type:off
): [any, ((T => T) | T) => void] {
let updateValue;
const Component = jest.fn(() => {
updateValue = useSetRecoilState(recoilState);
return null;
});
// flowlint-next-line unclear-type:off
return [(Component: any), x => updateValue(x)];
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function ObservesTransactions({fn}) {
useTransactionObservation_DEPRECATED(fn);
return null;
}
testRecoil(
'useTransactionObservation_DEPRECATED: Transaction dirty atoms are set',
async () => {
const anAtom = counterAtom({
type: 'url',
validator: x => (x: any), // flowlint-line unclear-type:off
});
const [aSelector, _] = plusOneSelector(anAtom);
// $FlowFixMe[incompatible-call]
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;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{getAtom: () => null | RecoilState<number>}>) {
const [value] = useRecoilState((getAtom(): any)); // flowlint-line unclear-type:off
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtom', 123),
);
});
const validator = jest.fn(() => 789);
// $FlowFixMe[incompatible-call]
theAtom = atom({
key: 'notDefinedYetAtom',
default: 456,
persistence_UNSTABLE: {
type: 'url',
validator,
},
});
act(() => {
setVisible(true);
});
// $FlowFixMe[invalid-tuple-index]
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), // flowlint-line unclear-type:off
});
const [Component, updateValue] = componentThatWritesAtom(anAtom);
let setUnvalidatedAtomValues;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let values: Map<NodeKey, mixed> = new Map();
renderElements(
<>
<Component />
<SetsUnvalidatedAtomValues />
<ObservesTransactions
fn={({atomValues}) => {
values = atomValues;
}}
/>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('someNonvalidatedAtom', 123),
);
});
values = new Map();
act(() => updateValue(1));
expect(values.size).toBe(2);
expect(values.get('someNonvalidatedAtom')).toBe(123);
},
);
testRecoil('Hooks cannot be used outside of RecoilRoot', () => {
const myAtom = atom({key: 'hook outside RecoilRoot', default: 'INVALID'});
function Test() {
useRecoilValue(myAtom);
return 'TEST';
}
// Make sure there is a friendly error message mentioning <RecoilRoot>
expect(() => renderUnwrappedElements(<Test />)).toThrow('<RecoilRoot>');
});

View File

@@ -0,0 +1,407 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
useTransition,
act,
useRecoilValue,
useRecoilState,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
atom,
selectorFamily,
renderElements,
reactMode,
flushPromisesAndTimers;
const testRecoil = getRecoilTestFn(() => {
React = require('react');
({useState, useTransition} = React);
({act} = require('ReactTestUtils'));
({
useRecoilValue,
useRecoilState,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
atom,
selectorFamily,
} = require('../../Recoil_index'));
({
renderElements,
flushPromisesAndTimers,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
});
let nextID = 0;
testRecoil('Works with useTransition', async ({concurrentMode}) => {
if (!reactMode().concurrent || !concurrentMode) {
return;
}
const indexAtom = atom({
key: `index${nextID++}`,
default: 0,
});
// Basic implementation of a cache that suspends:
const cache = new Map<
number,
| {promise: null, state: string, value: string}
| {promise: Promise<void>, state: string, value: null},
>();
const resolvers = [];
function getItem(index: number) {
if (cache.has(index) && cache.get(index)?.state === 'ready') {
return cache.get(index)?.value;
} else if (cache.has(index)) {
throw cache.get(index)?.promise;
} else {
const promise = new Promise(resolve => {
const onComplete = () => {
cache.set(index, {
state: 'ready',
value: `v${index}`,
promise: null,
});
resolve();
};
resolvers.push(onComplete);
});
const newEntry = {
state: 'loading',
value: null,
promise,
};
// $FlowFixMe[incompatible-call]
cache.set(index, newEntry);
throw promise;
}
}
function ItemContents({index}: $TEMPORARY$object<{index: number}>) {
const item = getItem(index);
return (
<div>
Item {index} = {item}
</div>
);
}
function Item({index}: $TEMPORARY$object<{index: number}>) {
return (
<React.Suspense fallback="Suspended">
<ItemContents index={index} />
</React.Suspense>
);
}
let incrementIndex;
function Main() {
const [index, setIndex] = useRecoilState(indexAtom);
const [isPending, startTransition] = useTransition();
incrementIndex = () => {
startTransition(() => {
setIndex(x => x + 1);
});
};
return (
<div>
Index: {index} - {isPending && 'In transition - '}
<Item index={index} />
</div>
);
}
const c = renderElements(<Main />);
// Initial load:
expect(c.textContent).toEqual('Index: 0 - Suspended');
act(() => resolvers[0]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 0 - Item 0 = v0');
// Increment index a single time; see previous item in transition, then once
// the new item resolves, see the new item:
act(() => incrementIndex());
expect(c.textContent).toEqual('Index: 0 - In transition - Item 0 = v0');
act(() => resolvers[1]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 1 - Item 1 = v1');
// Increment index a second time during transition; see previous item in
// transition, then once the new _second_ item resolves, see that new item:
act(() => incrementIndex());
expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1');
act(() => incrementIndex());
expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1');
act(() => resolvers[2]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 1 - In transition - Item 1 = v1');
act(() => resolvers[3]());
await flushPromisesAndTimers();
expect(c.textContent).toEqual('Index: 3 - Item 3 = v3');
});
testRecoil('useRecoilValue()', async ({concurrentMode}) => {
if (useTransition == null) {
return;
}
const myAtom = atom({key: 'useRecoilValue atom', default: 0});
let resolvers: Array<(result: Promise<string> | string) => void> = [];
function resolveSelectors() {
resolvers.forEach(resolve => resolve('RESOLVED'));
resolvers = [];
}
const query = selectorFamily({
key: 'useRecoilValue selector',
get:
(
// $FlowFixMe[missing-local-annot]
param,
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
const value = get(myAtom);
return new Promise(resolve => {
resolvers.push(resolve);
// $FlowFixMe[incompatible-type]
}).then(str => `${param} ${value} ${str}`);
},
});
function Component({index}: $TEMPORARY$object<{index: number}>) {
const value = useRecoilValue(query(index));
return (
<>
{index} {value}
</>
);
}
let startReactTransition, startRecoilTransition, startBothTransition;
function Main() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] = useRecoilState(myAtom);
const [inTransition, startTransition] = useTransition();
startReactTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
});
};
startRecoilTransition = () => {
startTransition(() => {
setRecoilState(x => x + 1);
});
};
startBothTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
setRecoilState(x => x + 1);
});
};
return (
<>
React:{reactState} Recoil:{recoilState}{' '}
{inTransition ? '[IN TRANSITION] ' : ''}|{' '}
<React.Suspense fallback="LOADING">
<Component index={reactState} />
</React.Suspense>
</>
);
}
const c = renderElements(<Main />);
expect(c.textContent).toBe('React:0 Recoil:0 | LOADING');
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:0 Recoil:0 | 0 0 0 RESOLVED');
// Transition changing React State
act(startReactTransition);
expect(c.textContent).toBe(
concurrentMode
? 'React:0 Recoil:0 [IN TRANSITION] | 0 0 0 RESOLVED'
: 'React:1 Recoil:0 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:0 | 1 1 0 RESOLVED');
// Transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().concurrent
? 'React:1 Recoil:0 [IN TRANSITION] | 1 1 0 RESOLVED'
: 'React:1 Recoil:1 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:1 | 1 1 1 RESOLVED');
// Second transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().concurrent
? 'React:1 Recoil:1 [IN TRANSITION] | 1 1 1 RESOLVED'
: 'React:1 Recoil:2 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:2 | 1 1 2 RESOLVED');
// Transition with both React and Recoil state
act(startBothTransition);
expect(c.textContent).toBe(
concurrentMode &&
(reactMode().concurrent || reactMode().mode === 'MUTABLE_SOURCE')
? 'React:1 Recoil:2 [IN TRANSITION] | 1 1 2 RESOLVED'
: 'React:2 Recoil:3 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:2 Recoil:3 | 2 2 3 RESOLVED');
});
testRecoil(
'useRecoilValue_TRANSITION_SUPPORT_UNSTABLE()',
async ({concurrentMode}) => {
if (useTransition == null) {
return;
}
const myAtom = atom({key: 'TRANSITION_SUPPORT_UNSTABLE atom', default: 0});
let resolvers: Array<(result: Promise<string> | string) => void> = [];
function resolveSelectors() {
resolvers.forEach(resolve => resolve('RESOLVED'));
resolvers = [];
}
const query = selectorFamily({
key: 'TRANSITION_SUPPORT_UNSTABLE selector',
get:
(
// $FlowFixMe[missing-local-annot]
param,
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
const value = get(myAtom);
return new Promise(resolve => {
resolvers.push(resolve);
// $FlowFixMe[incompatible-type]
}).then(str => `${param} ${value} ${str}`);
},
});
function Component({index}: $TEMPORARY$object<{index: number}>) {
const value = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(query(index));
return (
<>
{index} {value}
</>
);
}
let startReactTransition, startRecoilTransition, startBothTransition;
function Main() {
const [reactState, setReactState] = useState(0);
const [recoilState, setRecoilState] =
useRecoilState_TRANSITION_SUPPORT_UNSTABLE(myAtom);
const [inTransition, startTransition] = useTransition();
startReactTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
});
};
startRecoilTransition = () => {
startTransition(() => {
setRecoilState(x => x + 1);
});
};
startBothTransition = () => {
startTransition(() => {
setReactState(x => x + 1);
setRecoilState(x => x + 1);
});
};
return (
<>
React:{reactState} Recoil:{recoilState}{' '}
{inTransition ? '[IN TRANSITION] ' : ''}|{' '}
<React.Suspense fallback="LOADING">
<Component index={reactState} />
</React.Suspense>
</>
);
}
const c = renderElements(<Main />);
expect(c.textContent).toBe('React:0 Recoil:0 | LOADING');
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:0 Recoil:0 | 0 0 0 RESOLVED');
// Transition changing React State
act(startReactTransition);
expect(c.textContent).toBe(
concurrentMode
? 'React:0 Recoil:0 [IN TRANSITION] | 0 0 0 RESOLVED'
: 'React:1 Recoil:0 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:0 | 1 1 0 RESOLVED');
// Transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().early
? 'React:1 Recoil:0 [IN TRANSITION] | 1 1 0 RESOLVED'
: 'React:1 Recoil:1 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:1 | 1 1 1 RESOLVED');
// Second transition changing Recoil State
act(startRecoilTransition);
expect(c.textContent).toBe(
concurrentMode && reactMode().early
? 'React:1 Recoil:1 [IN TRANSITION] | 1 1 1 RESOLVED'
: 'React:1 Recoil:2 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:1 Recoil:2 | 1 1 2 RESOLVED');
// Transition with both React and Recoil State
act(startBothTransition);
expect(c.textContent).toBe(
concurrentMode
? 'React:1 Recoil:2 [IN TRANSITION] | 1 1 2 RESOLVED'
: 'React:2 Recoil:3 | LOADING',
);
act(resolveSelectors);
await flushPromisesAndTimers();
act(resolveSelectors);
await flushPromisesAndTimers();
expect(c.textContent).toBe('React:2 Recoil:3 | 2 2 3 RESOLVED');
},
);

9234
node_modules/recoil/cjs/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

147
node_modules/recoil/cjs/index.js.flow generated vendored Normal file
View File

@@ -0,0 +1,147 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
export type {StoreID} from './core/Recoil_Keys';
export type {PersistenceType} from './core/Recoil_Node';
export type {
RecoilValue,
RecoilState,
RecoilValueReadOnly,
} from './core/Recoil_RecoilValue';
export type {
MutableSnapshot,
Snapshot,
SnapshotID,
} from './core/Recoil_Snapshot';
export type {SetterOrUpdater} from './hooks/Recoil_Hooks';
export type {RecoilCallbackInterface} from './hooks/Recoil_useRecoilCallback';
export type {RecoilBridge} from './hooks/Recoil_useRecoilBridgeAcrossReactRoots';
export type {Loadable} from './adt/Recoil_Loadable';
export type {
AtomEffect,
PersistenceSettings,
} from './recoil_values/Recoil_atom';
export type {TransactionInterface} from './core/Recoil_AtomicUpdates';
export type {
GetRecoilValue,
SetRecoilState,
ResetRecoilState,
} from './recoil_values/Recoil_callbackTypes';
export type {
Parameter,
SelectorFamilyOptions,
} from './recoil_values/Recoil_selectorFamily';
const {RecoilLoadable} = require('./adt/Recoil_Loadable');
const {DefaultValue} = require('./core/Recoil_Node');
const {RecoilRoot, useRecoilStoreID} = require('./core/Recoil_RecoilRoot');
const {isRecoilValue} = require('./core/Recoil_RecoilValue');
const {retentionZone} = require('./core/Recoil_RetentionZone');
const {freshSnapshot} = require('./core/Recoil_Snapshot');
const {
useRecoilState,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
useRecoilStateLoadable,
useRecoilValue,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValueLoadable,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useResetRecoilState,
useSetRecoilState,
} = require('./hooks/Recoil_Hooks');
const {
useGotoRecoilSnapshot,
useRecoilSnapshot,
useRecoilTransactionObserver,
} = require('./hooks/Recoil_SnapshotHooks');
const useGetRecoilValueInfo = require('./hooks/Recoil_useGetRecoilValueInfo');
const useRecoilBridgeAcrossReactRoots = require('./hooks/Recoil_useRecoilBridgeAcrossReactRoots');
const {useRecoilCallback} = require('./hooks/Recoil_useRecoilCallback');
const useRecoilRefresher = require('./hooks/Recoil_useRecoilRefresher');
const useRecoilTransaction = require('./hooks/Recoil_useRecoilTransaction');
const useRetain = require('./hooks/Recoil_useRetain');
const atom = require('./recoil_values/Recoil_atom');
const atomFamily = require('./recoil_values/Recoil_atomFamily');
const constSelector = require('./recoil_values/Recoil_constSelector');
const errorSelector = require('./recoil_values/Recoil_errorSelector');
const readOnlySelector = require('./recoil_values/Recoil_readOnlySelector');
const selector = require('./recoil_values/Recoil_selector');
const selectorFamily = require('./recoil_values/Recoil_selectorFamily');
const {
noWait,
waitForAll,
waitForAllSettled,
waitForAny,
waitForNone,
} = require('./recoil_values/Recoil_WaitFor');
const RecoilEnv = require('recoil-shared/util/Recoil_RecoilEnv');
module.exports = {
// Types
DefaultValue,
isRecoilValue,
RecoilLoadable,
// Global Recoil environment settiongs
RecoilEnv,
// Recoil Root
RecoilRoot,
useRecoilStoreID,
useRecoilBridgeAcrossReactRoots_UNSTABLE: useRecoilBridgeAcrossReactRoots,
// Atoms/Selectors
atom,
selector,
// Convenience Atoms/Selectors
atomFamily,
selectorFamily,
constSelector,
errorSelector,
readOnlySelector,
// Concurrency Helpers for Atoms/Selectors
noWait,
waitForNone,
waitForAny,
waitForAll,
waitForAllSettled,
// Hooks for Atoms/Selectors
useRecoilValue,
useRecoilValueLoadable,
useRecoilState,
useRecoilStateLoadable,
useSetRecoilState,
useResetRecoilState,
useGetRecoilValueInfo_UNSTABLE: useGetRecoilValueInfo,
useRecoilRefresher_UNSTABLE: useRecoilRefresher,
useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE,
useRecoilValue_TRANSITION_SUPPORT_UNSTABLE,
useRecoilState_TRANSITION_SUPPORT_UNSTABLE,
// Hooks for complex operations
useRecoilCallback,
useRecoilTransaction_UNSTABLE: useRecoilTransaction,
// Snapshots
useGotoRecoilSnapshot,
useRecoilSnapshot,
useRecoilTransactionObserver_UNSTABLE: useRecoilTransactionObserver,
snapshot_UNSTABLE: freshSnapshot,
// Memory Management
useRetain,
retentionZone,
};

View File

@@ -0,0 +1,328 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {
RecoilValue,
RecoilValueReadOnly,
} from '../core/Recoil_RecoilValue';
import type {GetRecoilValue} from './Recoil_callbackTypes';
const {
loadableWithError,
loadableWithPromise,
loadableWithValue,
} = require('../adt/Recoil_Loadable');
const selector = require('./Recoil_selector');
const selectorFamily = require('./Recoil_selectorFamily');
const isPromise = require('recoil-shared/util/Recoil_isPromise');
/////////////////
// TRUTH TABLE
/////////////////
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [value, loading] [value, Promise] [value, Promise] Promise Promise
// [value, value] [value, value] [value, value] [value, value] [value, value]
//
// [error, loading] [Error, Promise] [Error, Promise] Error Promise
// [error, error] [Error, Error] [Error, Error] Error [error, error]
// [value, error] [value, Error] [value, Error] Error [value, error]
// Issue parallel requests for all dependencies and return the current
// status if they have results, have some error, or are still pending.
function concurrentRequests(
getRecoilValue: GetRecoilValue,
deps: $ReadOnlyArray<RecoilValue<mixed>>,
) {
const results = Array(deps.length).fill(undefined);
const exceptions = Array(deps.length).fill(undefined);
for (const [i, dep] of deps.entries()) {
try {
results[i] = getRecoilValue(dep);
} catch (e) {
// exceptions can either be Promises of pending results or real errors
exceptions[i] = e;
}
}
return [results, exceptions];
}
function isError(exp: $FlowFixMe) {
return exp != null && !isPromise(exp);
}
function unwrapDependencies(
dependencies:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| {+[string]: RecoilValueReadOnly<mixed>},
): $ReadOnlyArray<RecoilValue<mixed>> {
return Array.isArray(dependencies)
? dependencies
: Object.getOwnPropertyNames(dependencies).map(key => dependencies[key]);
}
function wrapResults(
dependencies:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| {+[string]: RecoilValueReadOnly<mixed>},
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
results,
) {
return Array.isArray(dependencies)
? results
: // Object.getOwnPropertyNames() has consistent key ordering with ES6
Object.getOwnPropertyNames(dependencies).reduce(
(out, key, idx) => ({...out, [(key: string)]: results[idx]}),
{},
);
}
function wrapLoadables(
dependencies:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| {+[string]: RecoilValueReadOnly<mixed>},
results: Array<$FlowFixMe>,
exceptions: Array<$FlowFixMe>,
) {
const output = exceptions.map((exception, idx) =>
exception == null
? loadableWithValue(results[idx])
: isPromise(exception)
? loadableWithPromise(exception)
: loadableWithError<mixed>(exception),
);
return wrapResults(dependencies, output);
}
function combineAsyncResultsWithSyncResults<T>(
syncResults: Array<T>,
asyncResults: Array<T>,
): Array<T> {
return asyncResults.map((result, idx) =>
/**
* it's important we use === undefined as opposed to == null, because the
* resolved value of the async promise could be `null`, in which case we
* don't want to use syncResults[idx], which would be undefined. If async
* promise resolves to `undefined`, that's ok because `syncResults[idx]`
* will also be `undefined`. That's a little hacky, but it works.
*/
result === undefined ? syncResults[idx] : result,
);
}
// Selector that requests all dependencies in parallel and immediately returns
// current results without waiting.
const waitForNone: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<Loadable<mixed>> | $ReadOnly<{[string]: Loadable<mixed>, ...}>,
> = selectorFamily({
key: '__waitForNone',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// Always return the current status of the results; never block.
return wrapLoadables(dependencies, results, exceptions);
},
dangerouslyAllowMutability: true,
});
// Selector that requests all dependencies in parallel and waits for at least
// one to be available before returning results. It will only error if all
// dependencies have errors.
const waitForAny: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<mixed> | $ReadOnly<{[string]: mixed, ...}>,
> = selectorFamily({
key: '__waitForAny',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
// Exceptions can either be Promises of pending results or real errors
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// If any results are available, value or error, return the current status
if (exceptions.some(exp => !isPromise(exp))) {
return wrapLoadables(dependencies, results, exceptions);
}
// Otherwise, return a promise that will resolve when the next result is
// available, whichever one happens to be next. But, if all pending
// dependencies end up with errors, then reject the promise.
return new Promise(resolve => {
for (const [i, exp] of exceptions.entries()) {
if (isPromise(exp)) {
exp
.then(result => {
results[i] = result;
exceptions[i] = undefined;
resolve(wrapLoadables(dependencies, results, exceptions));
})
.catch(error => {
exceptions[i] = error;
resolve(wrapLoadables(dependencies, results, exceptions));
});
}
}
});
},
dangerouslyAllowMutability: true,
});
// Selector that requests all dependencies in parallel and waits for all to be
// available before returning a value. It will error if any dependencies error.
const waitForAll: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<mixed> | $ReadOnly<{[string]: mixed, ...}>,
> = selectorFamily({
key: '__waitForAll',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
// Exceptions can either be Promises of pending results or real errors
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// If all results are available, return the results
if (exceptions.every(exp => exp == null)) {
return wrapResults(dependencies, results);
}
// If we have any errors, throw the first error
const error = exceptions.find(isError);
if (error != null) {
throw error;
}
// Otherwise, return a promise that will resolve when all results are available
return Promise.all(exceptions).then(exceptionResults =>
wrapResults(
dependencies,
combineAsyncResultsWithSyncResults(results, exceptionResults),
),
);
},
dangerouslyAllowMutability: true,
});
const waitForAllSettled: <
RecoilValues:
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>, ...}>,
>(
RecoilValues,
) => RecoilValueReadOnly<
$ReadOnlyArray<mixed> | $ReadOnly<{[string]: mixed, ...}>,
> = selectorFamily({
key: '__waitForAllSettled',
get:
(
dependencies:
| $ReadOnly<{[string]: RecoilValueReadOnly<mixed>}>
| $ReadOnlyArray<RecoilValueReadOnly<mixed>>,
) =>
({get}) => {
// Issue requests for all dependencies in parallel.
// Exceptions can either be Promises of pending results or real errors
const deps = unwrapDependencies(dependencies);
const [results, exceptions] = concurrentRequests(get, deps);
// If all results are available, return the results
if (exceptions.every(exp => !isPromise(exp))) {
return wrapLoadables(dependencies, results, exceptions);
}
// Wait for all results to settle
return (
Promise.all(
exceptions.map((exp, i) =>
isPromise(exp)
? exp
.then(result => {
results[i] = result;
exceptions[i] = undefined;
})
.catch(error => {
results[i] = undefined;
exceptions[i] = error;
})
: null,
),
)
// Then wrap them as loadables
.then(() => wrapLoadables(dependencies, results, exceptions))
);
},
dangerouslyAllowMutability: true,
});
const noWait: (RecoilValue<mixed>) => RecoilValueReadOnly<Loadable<mixed>> =
selectorFamily({
key: '__noWait',
get:
dependency =>
({get}) => {
try {
return selector.value(loadableWithValue(get(dependency)));
} catch (exception) {
return selector.value(
isPromise(exception)
? loadableWithPromise(exception)
: loadableWithError(exception),
);
}
},
dangerouslyAllowMutability: true,
});
module.exports = {
waitForNone,
waitForAny,
waitForAll,
waitForAllSettled,
noWait,
};

View File

@@ -0,0 +1,697 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Returns an atom, the basic unit of state in Recoil. An atom is a reference to
* value that can be read, written, and subscribed to. It has a `key` that is
* stable across time so that it can be persisted.
*
* There are two required options for creating an atom:
*
* key. This is a string that uniquely identifies the atom. It should be
* stable across time so that persisted states remain valid.
*
* default
* If `default` is provided, the atom is initialized to that value.
* Or, it may be set to another RecoilValue to use as a fallback.
* In that case, the value of the atom will be equal to that of the
* fallback, and will remain so until the first time the atom is written
* to, at which point it will stop tracking the fallback RecoilValue.
*
* The `persistence` option specifies that the atom should be saved to storage.
* It is an object with two properties: `type` specifies where the atom should
* be persisted; its only allowed value is "url"; `backButton` specifies whether
* changes to the atom should result in pushes to the browser history stack; if
* true, changing the atom and then hitting the Back button will cause the atom's
* previous value to be restored. Applications are responsible for implementing
* persistence by using the `useTransactionObservation` hook.
*
* Scoped atoms (DEPRECATED):
* ===================================================================================
*
* The scopeRules_APPEND_ONLY_READ_THE_DOCS option causes the atom be be "scoped".
* A scoped atom's value depends on other parts of the application state.
* A separate value of the atom is stored for every value of the state that it
* depends on. The dependencies may be changed without breaking existing URLs --
* it uses whatever rule was current when the URL was written. Values written
* under the newer rules are overlaid atop the previously-written values just for
* those states in which the write occurred, with reads falling back to the older
* values in other states, and eventually to the default or fallback.
*
* The scopedRules_APPEND_ONLY_READ_THE_DOCS parameter is a list of rules;
* it should start with a single entry. This list must only be appended to:
* existing entries must never be deleted or modified. Each rule is an atom
* or selector whose value is some arbitrary key. A different value of the
* scoped atom will be stored for each key. To change the scope rule, simply add
* a new function to the list. Each rule is either an array of atoms of primitives,
* or an atom of an array of primitives.
*
* Ordinary atoms may be upgraded to scoped atoms. To un-scope an atom, add a new
* scope rule consisting of a constant.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable, LoadingLoadableType} from '../adt/Recoil_Loadable';
import type {RecoilValueInfo} from '../core/Recoil_FunctionalCore';
import type {StoreID} from '../core/Recoil_Keys';
import type {
PersistenceInfo,
ReadWriteNodeOptions,
Trigger,
} from '../core/Recoil_Node';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
import type {RetainedBy} from '../core/Recoil_RetainedBy';
import type {AtomWrites, NodeKey, Store, TreeState} from '../core/Recoil_State';
// @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom';
// @fb-only: const {scopedAtom} = require('Recoil_ScopedAtom');
const {
isLoadable,
loadableWithError,
loadableWithPromise,
loadableWithValue,
} = require('../adt/Recoil_Loadable');
const {WrappedValue} = require('../adt/Recoil_Wrapper');
const {peekNodeInfo} = require('../core/Recoil_FunctionalCore');
const {
DEFAULT_VALUE,
DefaultValue,
getConfigDeletionHandler,
registerNode,
setConfigDeletionHandler,
} = require('../core/Recoil_Node');
const {isRecoilValue} = require('../core/Recoil_RecoilValue');
const {
getRecoilValueAsLoadable,
markRecoilValueModified,
setRecoilValue,
setRecoilValueLoadable,
} = require('../core/Recoil_RecoilValueInterface');
const {retainedByOptionWithDefault} = require('../core/Recoil_Retention');
const selector = require('./Recoil_selector');
const deepFreezeValue = require('recoil-shared/util/Recoil_deepFreezeValue');
const err = require('recoil-shared/util/Recoil_err');
const expectationViolation = require('recoil-shared/util/Recoil_expectationViolation');
const isPromise = require('recoil-shared/util/Recoil_isPromise');
const nullthrows = require('recoil-shared/util/Recoil_nullthrows');
const recoverableViolation = require('recoil-shared/util/Recoil_recoverableViolation');
export type PersistenceSettings<Stored> = $ReadOnly<{
...PersistenceInfo,
validator: (mixed, DefaultValue) => Stored | DefaultValue,
}>;
// TODO Support Loadable<T>
type NewValue<T> =
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>;
type NewValueOrUpdater<T> =
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>
| ((T | DefaultValue) => T | DefaultValue | WrappedValue<T>);
// Effect is called the first time a node is used with a <RecoilRoot>
export type AtomEffect<T> = ({
node: RecoilState<T>,
storeID: StoreID,
parentStoreID_UNSTABLE?: StoreID,
trigger: Trigger,
// Call synchronously to initialize value or async to change it later
setSelf: (
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>
| ((T | DefaultValue) => T | DefaultValue | WrappedValue<T>),
) => void,
resetSelf: () => void,
// Subscribe callbacks to events.
// Atom effect observers are called before global transaction observers
onSet: (
(newValue: T, oldValue: T | DefaultValue, isReset: boolean) => void,
) => void,
// Accessors to read other atoms/selectors
getPromise: <S>(RecoilValue<S>) => Promise<S>,
getLoadable: <S>(RecoilValue<S>) => Loadable<S>,
getInfo_UNSTABLE: <S>(RecoilValue<S>) => RecoilValueInfo<S>,
}) => void | (() => void);
export type AtomOptionsWithoutDefault<T> = $ReadOnly<{
key: NodeKey,
effects?: $ReadOnlyArray<AtomEffect<T>>,
effects_UNSTABLE?: $ReadOnlyArray<AtomEffect<T>>,
persistence_UNSTABLE?: PersistenceSettings<T>,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS?: ScopeRules,
dangerouslyAllowMutability?: boolean,
retainedBy_UNSTABLE?: RetainedBy,
}>;
type AtomOptionsWithDefault<T> = $ReadOnly<{
...AtomOptionsWithoutDefault<T>,
default: RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> | T,
}>;
export type AtomOptions<T> =
| AtomOptionsWithDefault<T>
| AtomOptionsWithoutDefault<T>;
type BaseAtomOptions<T> = $ReadOnly<{
...AtomOptions<T>,
default: Promise<T> | Loadable<T> | WrappedValue<T> | T,
}>;
const unwrap = <T, S = T>(x: T | S | WrappedValue<T>): T | S =>
x instanceof WrappedValue ? x.value : x;
function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
const {key, persistence_UNSTABLE: persistence} = options;
const retainedBy = retainedByOptionWithDefault(options.retainedBy_UNSTABLE);
let liveStoresCount = 0;
function unwrapPromise(promise: Promise<T>): Loadable<T> {
return loadableWithPromise(
promise
.then(value => {
defaultLoadable = loadableWithValue(value);
return value;
})
.catch(error => {
defaultLoadable = loadableWithError(error);
throw error;
}),
);
}
let defaultLoadable: Loadable<T> = isPromise(options.default)
? unwrapPromise(options.default)
: isLoadable(options.default)
? options.default.state === 'loading'
? unwrapPromise((options.default: LoadingLoadableType<T>).contents)
: options.default
: // $FlowFixMe[incompatible-call]
loadableWithValue(unwrap(options.default));
maybeFreezeValueOrPromise(defaultLoadable.contents);
let cachedAnswerForUnvalidatedValue: void | Loadable<T> = undefined;
// Cleanup handlers for this atom
// Rely on stable reference equality of the store to use it as a key per <RecoilRoot>
const cleanupEffectsByStore: Map<Store, Array<() => void>> = new Map();
function maybeFreezeValueOrPromise(valueOrPromise: mixed) {
if (__DEV__) {
if (options.dangerouslyAllowMutability !== true) {
if (isPromise(valueOrPromise)) {
return valueOrPromise.then(value => {
deepFreezeValue(value);
return value;
});
} else {
deepFreezeValue(valueOrPromise);
return valueOrPromise;
}
}
}
return valueOrPromise;
}
function wrapPendingPromise(
store: Store,
promise: Promise<T | DefaultValue>,
): Promise<T | DefaultValue> {
const wrappedPromise: Promise<T | DefaultValue> = promise
.then(value => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (state.atomValues.get(key)?.contents === wrappedPromise) {
setRecoilValue(store, node, value);
}
return value;
})
.catch(error => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (state.atomValues.get(key)?.contents === wrappedPromise) {
setRecoilValueLoadable(store, node, loadableWithError(error));
}
throw error;
});
return wrappedPromise;
}
function initAtom(
store: Store,
initState: TreeState,
trigger: Trigger,
): () => void {
liveStoresCount++;
const cleanupAtom = () => {
liveStoresCount--;
cleanupEffectsByStore.get(store)?.forEach(cleanup => cleanup());
cleanupEffectsByStore.delete(store);
};
store.getState().knownAtoms.add(key);
// Setup async defaults to notify subscribers when they resolve
if (defaultLoadable.state === 'loading') {
const notifyDefaultSubscribers = () => {
const state = store.getState().nextTree ?? store.getState().currentTree;
if (!state.atomValues.has(key)) {
markRecoilValueModified(store, node);
}
};
defaultLoadable.contents.finally(notifyDefaultSubscribers);
}
///////////////////
// Run Atom Effects
///////////////////
const effects = options.effects ?? options.effects_UNSTABLE;
if (effects != null) {
// This state is scoped by Store, since this is in the initAtom() closure
let initValue: NewValue<T> = DEFAULT_VALUE;
let isDuringInit = true;
let isInitError: boolean = false;
let pendingSetSelf: ?{
effect: AtomEffect<T>,
value: T | DefaultValue,
} = null;
function getLoadable<S>(recoilValue: RecoilValue<S>): Loadable<S> {
// Normally we can just get the current value of another atom.
// But for our own value we need to check if there is a pending
// initialized value or get the fallback default value.
if (isDuringInit && recoilValue.key === key) {
// Cast T to S
const retValue: NewValue<S> = (initValue: any); // flowlint-line unclear-type:off
return retValue instanceof DefaultValue
? (peekAtom(store, initState): any) // flowlint-line unclear-type:off
: isPromise(retValue)
? loadableWithPromise(
retValue.then((v: S | DefaultValue): S | Promise<S> =>
v instanceof DefaultValue
? // Cast T to S
(defaultLoadable: any).toPromise() // flowlint-line unclear-type:off
: v,
),
)
: // $FlowFixMe[incompatible-call]
loadableWithValue(retValue);
}
return getRecoilValueAsLoadable(store, recoilValue);
}
function getPromise<S>(recoilValue: RecoilValue<S>): Promise<S> {
return getLoadable(recoilValue).toPromise();
}
function getInfo_UNSTABLE<S>(
recoilValue: RecoilValue<S>,
): RecoilValueInfo<S> {
const info = peekNodeInfo<S>(
store,
store.getState().nextTree ?? store.getState().currentTree,
recoilValue.key,
);
return isDuringInit &&
recoilValue.key === key &&
!(initValue instanceof DefaultValue)
? {...info, isSet: true, loadable: getLoadable(recoilValue)}
: info;
}
const setSelf =
(effect: AtomEffect<T>) => (valueOrUpdater: NewValueOrUpdater<T>) => {
if (isDuringInit) {
const currentLoadable = getLoadable(node);
const currentValue: T | DefaultValue =
currentLoadable.state === 'hasValue'
? currentLoadable.contents
: DEFAULT_VALUE;
initValue =
typeof valueOrUpdater === 'function'
? // cast to any because we can't restrict T from being a function without losing support for opaque types
(valueOrUpdater: any)(currentValue) // flowlint-line unclear-type:off
: valueOrUpdater;
if (isPromise(initValue)) {
initValue = initValue.then(value => {
// Avoid calling onSet() when setSelf() initializes with a Promise
pendingSetSelf = {effect, value};
return value;
});
}
} else {
if (isPromise(valueOrUpdater)) {
throw err('Setting atoms to async values is not implemented.');
}
if (typeof valueOrUpdater !== 'function') {
pendingSetSelf = {
effect,
value: unwrap<T, DefaultValue>(valueOrUpdater),
};
}
setRecoilValue(
store,
node,
typeof valueOrUpdater === 'function'
? (currentValue: $FlowFixMe) => {
const newValue = unwrap(
// cast to any because we can't restrict T from being a function without losing support for opaque types
(valueOrUpdater: any)(currentValue), // flowlint-line unclear-type:off
);
// $FlowFixMe[incompatible-type]
pendingSetSelf = {effect, value: newValue};
return newValue;
}
: unwrap(valueOrUpdater),
);
}
};
const resetSelf = (effect: AtomEffect<T>) => () =>
setSelf(effect)(DEFAULT_VALUE);
const onSet =
(effect: AtomEffect<T>) =>
(handler: (T, T | DefaultValue, boolean) => void) => {
const {release} = store.subscribeToTransactions(currentStore => {
// eslint-disable-next-line prefer-const
let {currentTree, previousTree} = currentStore.getState();
if (!previousTree) {
recoverableViolation(
'Transaction subscribers notified without a next tree being present -- this is a bug in Recoil',
'recoil',
);
previousTree = currentTree; // attempt to trundle on
}
const newLoadable =
currentTree.atomValues.get(key) ?? defaultLoadable;
if (newLoadable.state === 'hasValue') {
const newValue: T = newLoadable.contents;
const oldLoadable =
previousTree.atomValues.get(key) ?? defaultLoadable;
const oldValue: T | DefaultValue =
oldLoadable.state === 'hasValue'
? oldLoadable.contents
: DEFAULT_VALUE; // TODO This isn't actually valid, use as a placeholder for now.
// Ignore atom value changes that were set via setSelf() in the same effect.
// We will still properly call the handler if there was a subsequent
// set from something other than an atom effect which was batched
// with the `setSelf()` call. However, we may incorrectly ignore
// the handler if the subsequent batched call happens to set the
// atom to the exact same value as the `setSelf()`. But, in that
// case, it was kind of a noop, so the semantics are debatable..
if (
pendingSetSelf?.effect !== effect ||
pendingSetSelf?.value !== newValue
) {
handler(newValue, oldValue, !currentTree.atomValues.has(key));
} else if (pendingSetSelf?.effect === effect) {
pendingSetSelf = null;
}
}
}, key);
cleanupEffectsByStore.set(store, [
...(cleanupEffectsByStore.get(store) ?? []),
release,
]);
};
for (const effect of effects) {
try {
const cleanup = effect({
node,
storeID: store.storeID,
parentStoreID_UNSTABLE: store.parentStoreID,
trigger,
setSelf: setSelf(effect),
resetSelf: resetSelf(effect),
onSet: onSet(effect),
getPromise,
getLoadable,
getInfo_UNSTABLE,
});
if (cleanup != null) {
cleanupEffectsByStore.set(store, [
...(cleanupEffectsByStore.get(store) ?? []),
cleanup,
]);
}
} catch (error) {
initValue = error;
isInitError = true;
}
}
isDuringInit = false;
// Mutate initial state in place since we know there are no other subscribers
// since we are the ones initializing on first use.
if (!(initValue instanceof DefaultValue)) {
const initLoadable = isInitError
? loadableWithError<$FlowFixMe>(initValue)
: isPromise(initValue)
? loadableWithPromise(wrapPendingPromise(store, initValue))
: loadableWithValue(unwrap(initValue));
maybeFreezeValueOrPromise(initLoadable.contents);
initState.atomValues.set(key, initLoadable);
// If there is a pending transaction, then also mutate the next state tree.
// This could happen if the atom was first initialized in an action that
// also updated some other atom's state.
store.getState().nextTree?.atomValues.set(key, initLoadable);
}
}
return cleanupAtom;
}
function peekAtom(_store: Store, state: TreeState): Loadable<T> {
return (
state.atomValues.get(key) ??
cachedAnswerForUnvalidatedValue ??
defaultLoadable
);
}
function getAtom(_store: Store, state: TreeState): Loadable<T> {
if (state.atomValues.has(key)) {
// Atom value is stored in state:
return nullthrows(state.atomValues.get(key));
} else if (state.nonvalidatedAtoms.has(key)) {
// Atom value is stored but needs validation before use.
// We might have already validated it and have a cached validated value:
if (cachedAnswerForUnvalidatedValue != null) {
return cachedAnswerForUnvalidatedValue;
}
if (persistence == null) {
expectationViolation(
`Tried to restore a persisted value for atom ${key} but it has no persistence settings.`,
);
return defaultLoadable;
}
const nonvalidatedValue = state.nonvalidatedAtoms.get(key);
const validatorResult: T | DefaultValue = persistence.validator(
nonvalidatedValue,
DEFAULT_VALUE,
);
const validatedValueLoadable =
validatorResult instanceof DefaultValue
? defaultLoadable
: loadableWithValue(validatorResult);
cachedAnswerForUnvalidatedValue = validatedValueLoadable;
return cachedAnswerForUnvalidatedValue;
} else {
return defaultLoadable;
}
}
function invalidateAtom() {
cachedAnswerForUnvalidatedValue = undefined;
}
function setAtom(
_store: Store,
state: TreeState,
newValue: T | DefaultValue,
): AtomWrites {
// Bail out if we're being set to the existing value, or if we're being
// reset but have no stored value (validated or unvalidated) to reset from:
if (state.atomValues.has(key)) {
const existing = nullthrows(state.atomValues.get(key));
if (existing.state === 'hasValue' && newValue === existing.contents) {
return new Map();
}
} else if (
!state.nonvalidatedAtoms.has(key) &&
newValue instanceof DefaultValue
) {
return new Map();
}
maybeFreezeValueOrPromise(newValue);
cachedAnswerForUnvalidatedValue = undefined; // can be released now if it was previously in use
return new Map<NodeKey, Loadable<$FlowFixMe>>().set(
key,
loadableWithValue(newValue),
);
}
function shouldDeleteConfigOnReleaseAtom() {
return getConfigDeletionHandler(key) !== undefined && liveStoresCount <= 0;
}
const node = registerNode(
({
key,
nodeType: 'atom',
peek: peekAtom,
get: getAtom,
set: setAtom,
init: initAtom,
invalidate: invalidateAtom,
shouldDeleteConfigOnRelease: shouldDeleteConfigOnReleaseAtom,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
persistence_UNSTABLE: options.persistence_UNSTABLE
? {
type: options.persistence_UNSTABLE.type,
backButton: options.persistence_UNSTABLE.backButton,
}
: undefined,
shouldRestoreFromSnapshots: true,
retainedBy,
}: ReadWriteNodeOptions<T>),
);
return node;
}
// prettier-ignore
function atom<T>(options: AtomOptions<T>): RecoilState<T> {
if (__DEV__) {
if (typeof options.key !== 'string') {
throw err(
'A key option with a unique string value must be provided when creating an atom.',
);
}
}
const {
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
...restOptions
} = options;
const optionsDefault: RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> | T =
'default' in options
?
// $FlowIssue[incompatible-type] No way to refine in Flow that property is not defined
options.default
: new Promise(() => {});
if (isRecoilValue(optionsDefault)
// Continue to use atomWithFallback for promise defaults for scoped atoms
// for now, since scoped atoms don't support async defaults
// @fb-only: || (isPromise(optionsDefault) && scopeRules_APPEND_ONLY_READ_THE_DOCS)
// @fb-only: || (isLoadable(optionsDefault) && scopeRules_APPEND_ONLY_READ_THE_DOCS)
) {
return atomWithFallback<T>({
...restOptions,
default: optionsDefault,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
});
// @fb-only: } else if (scopeRules_APPEND_ONLY_READ_THE_DOCS
// @fb-only: && !isPromise(optionsDefault)
// @fb-only: && !isLoadable(optionsDefault)
// @fb-only: ) {
// @fb-only: return scopedAtom<T>({
// @fb-only: ...restOptions,
// @fb-only: default: unwrap<T>(optionsDefault),
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
// @fb-only: });
} else {
return baseAtom<T>({...restOptions, default: optionsDefault});
}
}
type AtomWithFallbackOptions<T> = $ReadOnly<{
...AtomOptions<T>,
default: RecoilValue<T> | Promise<T> | Loadable<T>,
}>;
function atomWithFallback<T>(
options: AtomWithFallbackOptions<T>,
): RecoilState<T> {
const base = atom<T | DefaultValue>({
...options,
default: DEFAULT_VALUE,
persistence_UNSTABLE:
options.persistence_UNSTABLE === undefined
? undefined
: {
...options.persistence_UNSTABLE,
validator: (storedValue: mixed) =>
storedValue instanceof DefaultValue
? storedValue
: nullthrows(options.persistence_UNSTABLE).validator(
storedValue,
DEFAULT_VALUE,
),
},
// TODO Hack for now.
effects: (options.effects: any), // flowlint-line unclear-type: off
effects_UNSTABLE: (options.effects_UNSTABLE: any), // flowlint-line unclear-type: off
});
// $FlowFixMe[incompatible-call]
const sel = selector<T>({
key: `${options.key}__withFallback`,
get: ({get}) => {
const baseValue = get(base);
return baseValue instanceof DefaultValue ? options.default : baseValue;
},
// $FlowFixMe[incompatible-call]
set: ({set}, newValue) => set(base, newValue),
// This selector does not need to cache as it is a wrapper selector
// and the selector within the wrapper selector will have a cache
// option by default
cachePolicy_UNSTABLE: {
eviction: 'most-recent',
},
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
});
setConfigDeletionHandler(sel.key, getConfigDeletionHandler(options.key));
return sel;
}
// $FlowFixMe[missing-local-annot]
atom.value = value => new WrappedValue(value);
module.exports = (atom: {
<T>(AtomOptions<T>): RecoilState<T>,
value: <S>(S) => WrappedValue<S>,
});

View File

@@ -0,0 +1,176 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {WrappedValue} from '../adt/Recoil_Wrapper';
import type {CachePolicyWithoutEviction} from '../caches/Recoil_CachePolicy';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
import type {RetainedBy} from '../core/Recoil_RetainedBy';
import type {AtomEffect, AtomOptionsWithoutDefault} from './Recoil_atom';
// @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom';
const cacheFromPolicy = require('../caches/Recoil_cacheFromPolicy');
const {setConfigDeletionHandler} = require('../core/Recoil_Node');
const atom = require('./Recoil_atom');
const stableStringify = require('recoil-shared/util/Recoil_stableStringify');
type Primitive = void | null | boolean | number | string;
interface HasToJSON {
toJSON(): Parameter;
}
export type Parameter =
| Primitive
| HasToJSON
| $ReadOnlyArray<Parameter>
| $ReadOnly<{[string]: Parameter}>
| $ReadOnlySet<Parameter>
| $ReadOnlyMap<Parameter, Parameter>;
// flowlint unclear-type:off
export type ParameterizedScopeRules<P> = $ReadOnlyArray<
| RecoilValue<$ReadOnlyArray<any>>
| $ReadOnlyArray<RecoilValue<any> | (P => RecoilValue<any>)>,
>;
// flowlint unclear-type:error
export type AtomFamilyOptionsWithoutDefault<T, P: Parameter> = $ReadOnly<{
...AtomOptionsWithoutDefault<T>,
effects?:
| $ReadOnlyArray<AtomEffect<T>>
| (P => $ReadOnlyArray<AtomEffect<T>>),
// effects_UNSTABLE?:
// | $ReadOnlyArray<AtomEffect<T>>
// | (P => $ReadOnlyArray<AtomEffect<T>>),
retainedBy_UNSTABLE?: RetainedBy | (P => RetainedBy),
cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS?: ParameterizedScopeRules<P>,
}>;
export type AtomFamilyOptions<T, P: Parameter> =
| $ReadOnly<{
...AtomFamilyOptionsWithoutDefault<T, P>,
default:
| RecoilValue<T>
| Promise<T>
| Loadable<T>
| WrappedValue<T>
| T
| (P =>
| T
| RecoilValue<T>
| Promise<T>
| Loadable<T>
| WrappedValue<T>),
}>
| AtomFamilyOptionsWithoutDefault<T, P>;
// Process scopeRules to handle any entries which are functions taking parameters
// prettier-ignore
// @fb-only: function mapScopeRules<P>(
// @fb-only: scopeRules?: ParameterizedScopeRules<P>,
// @fb-only: param: P,
// @fb-only: ): ScopeRules | void {
// @fb-only: return scopeRules?.map(rule =>
// @fb-only: Array.isArray(rule)
// @fb-only: ? rule.map(entry => (typeof entry === 'function' ? entry(param) : entry))
// @fb-only: : rule,
// @fb-only: );
// @fb-only: }
/*
A function which returns an atom based on the input parameter.
Each unique parameter returns a unique atom. E.g.,
const f = atomFamily(...);
f({a: 1}) => an atom
f({a: 2}) => a different atom
This allows components to persist local, private state using atoms. Each
instance of the component may have a different key, which it uses as the
parameter for a family of atoms; in this way, each component will have
its own atom not shared by other instances. These state keys may be composed
into children's state keys as well.
*/
function atomFamily<T, P: Parameter>(
options: AtomFamilyOptions<T, P>,
): P => RecoilState<T> {
const atomCache = cacheFromPolicy<P, RecoilState<T>>({
equality: options.cachePolicyForParams_UNSTABLE?.equality ?? 'value',
eviction: 'keep-all',
});
// Simple atomFamily implementation to cache individual atoms based
// on the parameter value equality.
return (params: P) => {
const cachedAtom = atomCache.get(params);
if (cachedAtom != null) {
return cachedAtom;
}
const {cachePolicyForParams_UNSTABLE, ...atomOptions} = options;
const optionsDefault:
| RecoilValue<T>
| Promise<T>
| Loadable<T>
| WrappedValue<T>
| T
| (P => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>) =
'default' in options
? // $FlowIssue[incompatible-type] No way to refine in Flow that property is not defined
options.default
: new Promise(() => {});
const newAtom = atom<T>({
...atomOptions,
key: `${options.key}__${stableStringify(params) ?? 'void'}`,
default:
typeof optionsDefault === 'function'
? // The default was parameterized
// Flow doesn't know that T isn't a function, so we need to case to any
// $FlowIssue[incompatible-use]
optionsDefault(params)
: // Default may be a static value, promise, or RecoilValue
optionsDefault,
retainedBy_UNSTABLE:
typeof options.retainedBy_UNSTABLE === 'function'
? options.retainedBy_UNSTABLE(params)
: options.retainedBy_UNSTABLE,
effects:
typeof options.effects === 'function'
? options.effects(params)
: typeof options.effects_UNSTABLE === 'function'
? options.effects_UNSTABLE(params)
: options.effects ?? options.effects_UNSTABLE,
// prettier-ignore
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS: mapScopeRules(
// @fb-only: options.scopeRules_APPEND_ONLY_READ_THE_DOCS,
// @fb-only: params,
// @fb-only: ),
});
atomCache.set(params, newAtom);
setConfigDeletionHandler(newAtom.key, () => {
atomCache.delete(params);
});
return newAtom;
};
}
module.exports = atomFamily;

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {DefaultValue} from '../core/Recoil_Node';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
export type ValueOrUpdater<T> =
| T
| DefaultValue
| ((prevValue: T) => T | DefaultValue);
export type GetRecoilValue = <T>(RecoilValue<T>) => T;
export type SetRecoilState = <T>(RecoilState<T>, ValueOrUpdater<T>) => void;
export type ResetRecoilState = <T>(RecoilState<T>) => void;
module.exports = ({}: {...});

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValueReadOnly} from '../core/Recoil_RecoilValue';
import type {Parameter} from './Recoil_selectorFamily';
const selectorFamily = require('./Recoil_selectorFamily');
// flowlint-next-line unclear-type:off
const constantSelector = selectorFamily<any, any>({
key: '__constant',
get: constant => () => constant,
cachePolicyForParams_UNSTABLE: {
equality: 'reference',
},
});
// Function that returns a selector which always produces the
// same constant value. It may be called multiple times with the
// same value, based on reference equality, and will provide the
// same selector.
function constSelector<T: Parameter>(constant: T): RecoilValueReadOnly<T> {
return constantSelector(constant);
}
module.exports = constSelector;

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValueReadOnly} from '../core/Recoil_RecoilValue';
const selectorFamily = require('./Recoil_selectorFamily');
const err = require('recoil-shared/util/Recoil_err');
// flowlint-next-line unclear-type:off
const throwingSelector = selectorFamily<any, any>({
key: '__error',
get: message => () => {
throw err(message);
},
// TODO Why?
cachePolicyForParams_UNSTABLE: {
equality: 'reference',
},
});
// Function that returns a selector which always throws an error
// with the provided message.
function errorSelector<T>(message: string): RecoilValueReadOnly<T> {
return throwingSelector(message);
}
module.exports = errorSelector;

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Wraps another recoil value and prevents writing to it.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {
RecoilValue,
RecoilValueReadOnly,
} from '../core/Recoil_RecoilValue';
function readOnlySelector<T>(atom: RecoilValue<T>): RecoilValueReadOnly<T> {
// flowlint-next-line unclear-type: off
return (atom: any);
}
module.exports = readOnlySelector;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,194 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Loadable} from '../adt/Recoil_Loadable';
import type {WrappedValue} from '../adt/Recoil_Wrapper';
import type {
CachePolicy,
CachePolicyWithoutEviction,
} from '../caches/Recoil_CachePolicy';
import type {DefaultValue} from '../core/Recoil_Node';
import type {
RecoilState,
RecoilValue,
RecoilValueReadOnly,
} from '../core/Recoil_RecoilValue';
import type {RetainedBy} from '../core/Recoil_RetainedBy';
import type {GetCallback} from '../recoil_values/Recoil_selector';
import type {
GetRecoilValue,
ResetRecoilState,
SetRecoilState,
} from './Recoil_callbackTypes';
const cacheFromPolicy = require('../caches/Recoil_cacheFromPolicy');
const {setConfigDeletionHandler} = require('../core/Recoil_Node');
const selector = require('./Recoil_selector');
const err = require('recoil-shared/util/Recoil_err');
const stableStringify = require('recoil-shared/util/Recoil_stableStringify');
// Keep in mind the parameter needs to be serializable as a cahche key
// using Recoil_stableStringify
type Primitive = void | null | boolean | number | string;
interface HasToJSON {
toJSON(): Parameter;
}
export type Parameter =
| Primitive
| HasToJSON
| $ReadOnlySet<Parameter>
| $ReadOnlyMap<Parameter, Parameter>
| $ReadOnlyArray<Parameter>
| $ReadOnly<{...}>;
// | $ReadOnly<{[string]: Parameter}>; // TODO Better enforce object is serializable
type BaseSelectorFamilyOptions<P: Parameter> = $ReadOnly<{
key: string,
cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction,
cachePolicy_UNSTABLE?: CachePolicy,
dangerouslyAllowMutability?: boolean,
retainedBy_UNSTABLE?: RetainedBy | (P => RetainedBy),
}>;
export type ReadOnlySelectorFamilyOptions<T, P: Parameter> = $ReadOnly<{
...BaseSelectorFamilyOptions<P>,
get: P => ({
get: GetRecoilValue,
getCallback: GetCallback<T>,
}) => Promise<T> | Loadable<T> | WrappedValue<T> | RecoilValue<T> | T,
}>;
export type ReadWriteSelectorFamilyOptions<T, P: Parameter> = $ReadOnly<{
...ReadOnlySelectorFamilyOptions<T, P>,
set: P => (
{set: SetRecoilState, get: GetRecoilValue, reset: ResetRecoilState},
newValue: T | DefaultValue,
) => void,
}>;
export type SelectorFamilyOptions<T, P> =
| ReadOnlySelectorFamilyOptions<T, P>
| ReadWriteSelectorFamilyOptions<T, P>;
// Add a unique index to each selector in case the cache implementation allows
// duplicate keys based on equivalent stringified parameters
let nextIndex = 0;
/* eslint-disable no-redeclare */
declare function selectorFamily<T, Params: Parameter>(
options: ReadOnlySelectorFamilyOptions<T, Params>,
): Params => RecoilValueReadOnly<T>;
declare function selectorFamily<T, Params: Parameter>(
options: ReadWriteSelectorFamilyOptions<T, Params>,
): Params => RecoilState<T>;
// Return a function that returns members of a family of selectors of the same type
// E.g.,
//
// const s = selectorFamily(...);
// s({a: 1}) => a selector
// s({a: 2}) => a different selector
//
// By default, the selectors are distinguished by distinct values of the
// parameter based on value equality, not reference equality. This allows using
// object literals or other equivalent objects at callsites to not create
// duplicate cache entries. This behavior may be overridden with the
// cacheImplementationForParams option.
function selectorFamily<T, Params: Parameter>(
options:
| ReadOnlySelectorFamilyOptions<T, Params>
| ReadWriteSelectorFamilyOptions<T, Params>,
): Params => RecoilValue<T> {
const selectorCache = cacheFromPolicy<
Params,
RecoilState<T> | RecoilValueReadOnly<T>,
>({
equality: options.cachePolicyForParams_UNSTABLE?.equality ?? 'value',
eviction: 'keep-all',
});
return (params: Params) => {
// Throw an error with selector key so that it is clear which
// selector is causing an error
let cachedSelector;
try {
cachedSelector = selectorCache.get(params);
} catch (error) {
throw err(
`Problem with cache lookup for selector ${options.key}: ${error.message}`,
);
}
if (cachedSelector != null) {
return cachedSelector;
}
const myKey = `${options.key}__selectorFamily/${
stableStringify(params, {
// It is possible to use functions in parameters if the user uses
// a cache with reference equality thanks to the incrementing index.
allowFunctions: true,
}) ?? 'void'
}/${nextIndex++}`; // Append index in case values serialize to the same key string
const myGet = (callbacks: {
get: GetRecoilValue,
getCallback: GetCallback<T>,
}) => options.get(params)(callbacks);
const myCachePolicy = options.cachePolicy_UNSTABLE;
const retainedBy =
typeof options.retainedBy_UNSTABLE === 'function'
? options.retainedBy_UNSTABLE(params)
: options.retainedBy_UNSTABLE;
let newSelector;
if (options.set != null) {
const set = options.set;
const mySet = (
callbacks: {
get: GetRecoilValue,
reset: ResetRecoilState,
set: SetRecoilState,
},
newValue: T | DefaultValue,
) => set(params)(callbacks, newValue);
newSelector = selector<T>({
key: myKey,
get: myGet,
set: mySet,
cachePolicy_UNSTABLE: myCachePolicy,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
retainedBy_UNSTABLE: retainedBy,
});
} else {
newSelector = selector<T>({
key: myKey,
get: myGet,
cachePolicy_UNSTABLE: myCachePolicy,
dangerouslyAllowMutability: options.dangerouslyAllowMutability,
retainedBy_UNSTABLE: retainedBy,
});
}
selectorCache.set(params, newSelector);
setConfigDeletionHandler(newSelector.key, () => {
selectorCache.delete(params);
});
return newSelector;
};
}
/* eslint-enable no-redeclare */
module.exports = selectorFamily;

View File

@@ -0,0 +1,135 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilState} from '../../core/Recoil_RecoilValue';
const {useRecoilValue} = require('../../hooks/Recoil_Hooks');
const atom = require('../Recoil_atom');
const readOnlySelector = require('../Recoil_readOnlySelector');
const {
noWait,
waitForAll,
waitForAllSettled,
waitForNone,
} = require('../Recoil_WaitFor');
const numberAtom: RecoilState<number> = atom({key: 'number', default: 0});
const stringAtom: RecoilState<string> = atom({key: 'string', default: ''});
let num: number;
let str: string;
//////////////
// waitForAll
//////////////
// Test tuple unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const arrayResults = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
waitForAll([readOnlySelector(numberAtom), readOnlySelector(stringAtom)]),
);
num = arrayResults[0];
str = arrayResults[1];
// $FlowExpectedError
num = arrayResults[1];
// $FlowExpectedError
str = arrayResults[0];
// Test object unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const objResults = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
// $FlowIssue[incompatible-call]
waitForAll({num: numberAtom, str: stringAtom}),
);
num = objResults.num;
str = objResults.str;
// $FlowExpectedError
num = objResults.str;
// $FlowExpectedError
str = objResults.num;
//////////////
// waitForNone
//////////////
// Test tuple unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const arrayResultsNone = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
waitForNone([readOnlySelector(numberAtom), readOnlySelector(stringAtom)]),
);
num = arrayResultsNone[0].valueOrThrow();
str = arrayResultsNone[1].valueOrThrow();
// $FlowExpectedError
num = arrayResultsNone[1].valueOrThrow();
// $FlowExpectedError
str = arrayResultsNone[0].valueOrThrow();
// Test object unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const objResultsNone = useRecoilValue(
// $FlowIssue[incompatible-call]
waitForNone({num: numberAtom, str: stringAtom}),
);
num = objResultsNone.num.valueOrThrow();
str = objResultsNone.str.valueOrThrow();
// $FlowExpectedError
num = objResultsNone.str.valueOrThrow();
// $FlowExpectedError
str = objResultsNone.num.valueOrThrow();
//////////////
// waitForAllSettled
//////////////
// Test tuple unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const arrayResultsAllSettled = useRecoilValue(
waitForAllSettled([
// $FlowIssue[invalid-tuple-map]
readOnlySelector(numberAtom),
// $FlowIssue[invalid-tuple-map]
readOnlySelector(stringAtom),
]),
);
num = arrayResultsAllSettled[0].valueOrThrow();
str = arrayResultsAllSettled[1].valueOrThrow();
// $FlowExpectedError
num = arrayResultsAllSettled[1].valueOrThrow();
// $FlowExpectedError
str = arrayResultsAllSettled[0].valueOrThrow();
// Test object unwrapping of types
// eslint-disable-next-line fb-www/react-hooks
const objResultsAllSettled = useRecoilValue(
// $FlowIssue[invalid-tuple-map]
// $FlowIssue[incompatible-call]
waitForAllSettled({num: numberAtom, str: stringAtom}),
);
num = objResultsAllSettled.num.valueOrThrow();
str = objResultsAllSettled.str.valueOrThrow();
// $FlowExpectedError
num = objResultsAllSettled.str.valueOrThrow();
// $FlowExpectedError
str = objResultsAllSettled.num.valueOrThrow();
//////////////
// noWait
//////////////
num = useRecoilValue(noWait(numberAtom)).valueOrThrow(); // eslint-disable-line fb-www/react-hooks
// $FlowExpectedError
str = useRecoilValue(noWait(numberAtom)).valueOrThrow(); // eslint-disable-line fb-www/react-hooks

View File

@@ -0,0 +1,639 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValue} from '../../core/Recoil_RecoilValue';
const {
flushPromisesAndTimers,
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let getRecoilValueAsLoadable,
noWait,
waitForAll,
waitForAllSettled,
waitForAny,
waitForNone,
store,
selector,
invariant;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
invariant = require('recoil-shared/util/Recoil_invariant');
({
getRecoilValueAsLoadable,
} = require('../../core/Recoil_RecoilValueInterface'));
selector = require('../Recoil_selector');
({
noWait,
waitForAll,
waitForAllSettled,
waitForAny,
waitForNone,
} = require('../Recoil_WaitFor'));
store = makeStore();
});
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function getLoadable(atom) {
return getRecoilValueAsLoadable(store, atom).contents;
}
function getState<T>(
recoilValue: RecoilValue<T>,
): 'loading' | 'hasValue' | 'hasError' {
return getRecoilValueAsLoadable(store, recoilValue).state;
}
function getValue<T>(recoilValue: RecoilValue<T>): T {
const loadable = getRecoilValueAsLoadable(store, recoilValue);
if (loadable.state !== 'hasValue') {
throw new Error(`expected atom "${recoilValue.key}" to have a value`);
}
return loadable.contents;
}
function getPromise<T>(recoilValue: RecoilValue<T>): Promise<T> {
const loadable = getRecoilValueAsLoadable(store, recoilValue);
if (loadable.state !== 'loading') {
throw new Error(`expected atom "${recoilValue.key}" to be a promise`);
}
return loadable.toPromise();
}
let id = 0;
function asyncSelector<T, S>(
dep?: RecoilValue<S>,
): [RecoilValue<T>, (T) => void, (Error) => void, () => boolean] {
let resolve: T => void = () => invariant(false, 'bug in test code'); // make flow happy with initialization
let reject: mixed => void = () => invariant(false, 'bug in test code');
let evaluated = false;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// $FlowFixMe[incompatible-call]
const sel = selector({
key: `AsyncSelector${id++}`,
get: ({get}) => {
evaluated = true;
if (dep != null) {
get(dep);
}
return promise;
},
});
return [sel, resolve, reject, () => evaluated];
}
/* eslint-disable jest/valid-expect */
testRecoil('noWait - resolve', async () => {
const [dep, resolve] = asyncSelector<number, _>();
const pTest = expect(getValue(noWait(dep)).toPromise()).resolves.toBe(42);
expect(getValue(noWait(dep)).contents).toBeInstanceOf(Promise);
resolve(42);
await flushPromisesAndTimers();
expect(getValue(noWait(dep)).contents).toBe(42);
await pTest;
});
testRecoil('noWait - reject', async () => {
const [dep, _resolve, reject] = asyncSelector<$FlowFixMe, _>();
class MyError extends Error {}
const pTest = expect(
getValue(noWait(dep)).toPromise(),
).rejects.toBeInstanceOf(MyError);
expect(getValue(noWait(dep)).contents).toBeInstanceOf(Promise);
reject(new MyError());
await flushPromisesAndTimers();
expect(getValue(noWait(dep)).contents).toBeInstanceOf(MyError);
await pTest;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [value, loading] [value, Promise] [value, Promise] Promise Promise
// [value, value] [value, value] [value, value] [value, value] [value, value]
testRecoil('waitFor - resolve to values', async () => {
const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, resolveB] = asyncSelector<$FlowFixMe | number, _>();
const deps = [depA, depB];
// Test for initial values
// watiForNone returns loadables with promises that resolve to their values
expect(getValue(waitForNone(deps)).every(r => r.state === 'loading')).toBe(
true,
);
const depTest0 = expect(
getValue(waitForNone(deps))[0].promiseMaybe(),
).resolves.toBe(0);
const depTest1 = expect(
getValue(waitForNone(deps))[1].promiseMaybe(),
).resolves.toBe(1);
// waitForAny returns a promise that resolves to the state with the next
// resolved value. So, that includes the first value and a promise for the second.
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value[0].valueMaybe()).toEqual(0);
return value[0].valueMaybe();
}),
).resolves.toEqual(0);
const anyTest1 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value[1].promiseMaybe()).toBeInstanceOf(Promise);
return value[1].promiseMaybe();
}),
).resolves.toBe(1);
// waitForAll returns a promise that resolves to the actual values
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(getPromise(waitForAll(deps))).resolves.toEqual([
0, 1,
]);
// Resolve the first dep
resolveA(0);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps))[0].contents).toBe(0);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getValue(waitForAny(deps))[0].contents).toBe(0);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest1 = expect(getPromise(waitForAll(deps))).resolves.toEqual([
0, 1,
]);
// Resolve the second dep
resolveB(1);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps))[0].contents).toBe(0);
expect(getValue(waitForNone(deps))[1].contents).toBe(1);
expect(getValue(waitForAny(deps))[0].contents).toBe(0);
expect(getValue(waitForAny(deps))[1].contents).toBe(1);
expect(getValue(waitForAll(deps))[0]).toBe(0);
expect(getValue(waitForAll(deps))[1]).toBe(1);
await depTest0;
await depTest1;
await anyTest0;
await anyTest1;
await allTest0;
await allTest1;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [error, loading] [Error, Promise] [Error, Promise] Error Promise
// [error, error] [Error, Error] [Error, Error] Error [Error, Error]
testRecoil('waitFor - rejected', async () => {
const [depA, _resolveA, rejectA] = asyncSelector<$FlowFixMe, _>();
const [depB, _resolveB, rejectB] = asyncSelector<$FlowFixMe, _>();
const deps = [depA, depB];
class Error1 extends Error {}
class Error2 extends Error {}
// All deps Loading Tests
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('loading');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Promise);
return 'success';
}),
).resolves.toEqual('success');
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(
getPromise(waitForAll(deps)).catch(err => {
expect(err).toBeInstanceOf(Error1);
return 'failure';
}),
).resolves.toEqual('failure');
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest0 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depA Rejected tests
rejectA(new Error1());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest1 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depB Rejected tests
rejectB(new Error2());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('hasValue');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAllSettled(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAllSettled(deps))[1].contents).toBeInstanceOf(Error2);
await anyTest0;
await allTest0;
await allSettledTest0;
await allSettledTest1;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [value, loading] [value, Promise] [value, Promise] Promise Promise
// [value, error] [value, Error] [value, Error] Error [value, Error]
testRecoil('waitFor - resolve then reject', async () => {
const [depA, resolveA, _rejectA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, _resolveB, rejectB] = asyncSelector<$FlowFixMe | number, _>();
const deps = [depA, depB];
class Error2 extends Error {}
// All deps Loading Tests
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('loading');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(res => {
expect(res[0].contents).toEqual(1);
expect(res[1].contents).toBeInstanceOf(Promise);
return 'success';
}),
).resolves.toEqual('success');
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(
getPromise(waitForAll(deps)).catch(err => {
expect(err).toBeInstanceOf(Error2);
return 'failure';
}),
).resolves.toEqual('failure');
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest0 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toEqual(1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depA Resolves tests
resolveA(1);
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toEqual(1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toEqual(1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest1 = expect(getPromise(waitForAll(deps))).rejects.toBeInstanceOf(
Error2,
);
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest1 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toEqual(1);
expect(res[1].contents).toBeInstanceOf(Error2);
return 'success';
}),
).resolves.toEqual('success');
// depB Rejected tests
rejectB(new Error2());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toEqual(1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toEqual(1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Error2);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error2);
expect(getState(waitForAllSettled(deps))).toEqual('hasValue');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAllSettled(deps))[0].contents).toEqual(1);
expect(getValue(waitForAllSettled(deps))[1].contents).toBeInstanceOf(Error2);
await anyTest0;
await allTest0;
await allTest1;
await allSettledTest0;
await allSettledTest1;
});
// TRUTH TABLE
// Dependencies waitForNone waitForAny waitForAll waitForAllSettled
// [loading, loading] [Promise, Promise] Promise Promise Promise
// [error, loading] [Error, Promise] [Error, Promsie] Error Promise
// [error, value] [Error, value] [Error, value] Error [Error, value]
testRecoil('waitFor - reject then resolve', async () => {
const [depA, _resolveA, rejectA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, resolveB, _rejectB] = asyncSelector<$FlowFixMe | number, _>();
const deps = [depA, depB];
class Error1 extends Error {}
// All deps Loading Tests
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('loading');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toBeInstanceOf(Promise);
return 'success';
}),
).resolves.toEqual('success');
expect(getState(waitForAll(deps))).toEqual('loading');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(
getPromise(waitForAll(deps)).catch(err => {
expect(err).toBeInstanceOf(Error1);
return 'failure';
}),
).resolves.toEqual('failure');
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest0 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toEqual(1);
return 'success';
}),
).resolves.toEqual('success');
// depA Rejects tests
rejectA(new Error1());
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toBeInstanceOf(Promise);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('loading');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Promise);
const allSettledTest1 = expect(
getPromise(waitForAllSettled(deps)).then(res => {
expect(res[0].contents).toBeInstanceOf(Error1);
expect(res[1].contents).toEqual(1);
return 'success';
}),
).resolves.toEqual('success');
// depB Resolves tests
resolveB(1);
await flushPromisesAndTimers();
expect(getState(waitForNone(deps))).toEqual('hasValue');
expect(getLoadable(waitForNone(deps))).toBeInstanceOf(Array);
expect(getValue(waitForNone(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForNone(deps))[1].contents).toEqual(1);
expect(getState(waitForAny(deps))).toEqual('hasValue');
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAny(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAny(deps))[1].contents).toEqual(1);
expect(getState(waitForAll(deps))).toEqual('hasError');
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Error1);
expect(getState(waitForAllSettled(deps))).toEqual('hasValue');
expect(getLoadable(waitForAllSettled(deps))).toBeInstanceOf(Array);
expect(getValue(waitForAllSettled(deps))[0].contents).toBeInstanceOf(Error1);
expect(getValue(waitForAllSettled(deps))[1].contents).toEqual(1);
await anyTest0;
await allTest0;
await allSettledTest0;
await allSettledTest1;
});
// Similar as the first test that resolves both dependencies, but with named dependencies.
testRecoil('waitFor - named dependency version', async () => {
const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>();
const [depB, resolveB] = asyncSelector<$FlowFixMe | number, _>();
const deps = {a: depA, b: depB};
expect(getValue(waitForNone(deps)).a.promiseMaybe()).toBeInstanceOf(Promise);
expect(getValue(waitForNone(deps)).b.promiseMaybe()).toBeInstanceOf(Promise);
const depTest0 = expect(
getValue(waitForNone(deps)).a.promiseMaybe(),
).resolves.toBe(0);
const depTest1 = expect(
getValue(waitForNone(deps)).b.promiseMaybe(),
).resolves.toBe(1);
expect(getLoadable(waitForAny(deps))).toBeInstanceOf(Promise);
const anyTest0 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value.a.valueMaybe()).toEqual(0);
return value.a.valueMaybe();
}),
).resolves.toEqual(0);
const anyTest1 = expect(
getPromise(waitForAny(deps)).then(value => {
expect(value.b.promiseMaybe()).toBeInstanceOf(Promise);
return value.b.promiseMaybe();
}),
).resolves.toBe(1);
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest0 = expect(getPromise(waitForAll(deps))).resolves.toEqual({
a: 0,
b: 1,
});
resolveA(0);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps)).a.contents).toBe(0);
expect(getValue(waitForNone(deps)).b.contents).toBeInstanceOf(Promise);
expect(getValue(waitForAny(deps)).a.contents).toBe(0);
expect(getValue(waitForAny(deps)).b.contents).toBeInstanceOf(Promise);
expect(getLoadable(waitForAll(deps))).toBeInstanceOf(Promise);
const allTest1 = expect(getPromise(waitForAll(deps))).resolves.toEqual({
a: 0,
b: 1,
});
resolveB(1);
await flushPromisesAndTimers();
expect(getValue(waitForNone(deps)).a.contents).toBe(0);
expect(getValue(waitForNone(deps)).b.contents).toBe(1);
expect(getValue(waitForAny(deps)).a.contents).toBe(0);
expect(getValue(waitForAny(deps)).b.contents).toBe(1);
expect(getValue(waitForAll(deps)).a).toBe(0);
expect(getValue(waitForAll(deps)).b).toBe(1);
await depTest0;
await depTest1;
await anyTest0;
await anyTest1;
await allTest0;
await allTest1;
});
testRecoil('waitForAll - Evaluated concurrently', async () => {
const [depA, resolveA, _rejectA, evaluatedA] = asyncSelector<
$FlowFixMe | number,
_,
>();
const [depB, _resolveB, _rejectB, evaluatedB] = asyncSelector<
$FlowFixMe,
_,
>();
const deps = [depA, depB];
expect(evaluatedA()).toBe(false);
expect(evaluatedB()).toBe(false);
getPromise(waitForAll(deps));
await flushPromisesAndTimers();
// Confirm dependencies were evaluated in parallel
expect(evaluatedA()).toBe(true);
expect(evaluatedB()).toBe(true);
resolveA(0);
getPromise(waitForAll(deps));
await flushPromisesAndTimers();
expect(evaluatedA()).toBe(true);
expect(evaluatedB()).toBe(true);
});
testRecoil('waitForAll - mixed sync and async deps', async () => {
const [depA, resolveA] = asyncSelector<$FlowFixMe | number, _>();
const depB = selector({
key: 'mydepkeyB',
get: () => 1,
});
const deps = [depA, depB];
const allTest = expect(getPromise(waitForAll(deps))).resolves.toEqual([0, 1]);
resolveA(0);
await flushPromisesAndTimers();
expect(getValue(waitForAll(deps))).toEqual([0, 1]);
await allTest;
});
/* eslint-enable jest/valid-expect */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,762 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {Store} from '../../core/Recoil_State';
import type {Parameter} from 'Recoil_atomFamily';
import type {NodeKey, StoreID as StoreIDType} from 'Recoil_Keys';
import type {RecoilState} from 'Recoil_RecoilValue';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let store: Store,
React,
Profiler,
useState,
act,
RecoilRoot,
getRecoilValueAsLoadable,
setRecoilValue,
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
useRecoilStoreID,
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
reactMode,
stableStringify,
atom,
atomFamily,
selectorFamily,
RecoilLoadable,
pAtom;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
React = require('react');
({Profiler, useState} = require('react'));
({act} = require('ReactTestUtils'));
({RecoilRoot, useRecoilStoreID} = require('../../core/Recoil_RecoilRoot'));
({
getRecoilValueAsLoadable,
setRecoilValue,
} = require('../../core/Recoil_RecoilValueInterface'));
({
useRecoilState,
useRecoilValue,
useSetRecoilState,
useSetUnvalidatedAtomValues,
} = require('../../hooks/Recoil_Hooks'));
({
ReadsAtom,
componentThatReadsAndWritesAtom,
flushPromisesAndTimers,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
({reactMode} = require('../../core/Recoil_ReactMode'));
stableStringify = require('recoil-shared/util/Recoil_stableStringify');
atom = require('../Recoil_atom');
atomFamily = require('../Recoil_atomFamily');
selectorFamily = require('../Recoil_selectorFamily');
({RecoilLoadable} = require('../../adt/Recoil_Loadable'));
store = makeStore();
pAtom = atomFamily<_, {k: string} | {x: string} | {y: string}>({
key: 'pAtom',
default: 'fallback',
});
});
let fbOnlyTest = test.skip;
// $FlowFixMe[prop-missing]
// $FlowFixMe[incompatible-type]
// @fb-only: fbOnlyTest = testRecoil;
let id = 0;
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function get(recoilValue) {
return getRecoilValueAsLoadable(store, recoilValue).contents;
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
function getLoadable(recoilValue) {
return getRecoilValueAsLoadable(store, recoilValue);
}
function set(
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
recoilValue,
value:
| void
| number
| $TEMPORARY$string<'VALUE'>
| $TEMPORARY$string<'bar'>
| $TEMPORARY$string<'eggs'>
| $TEMPORARY$string<'spam'>
| $TEMPORARY$string<'xValue'>
| $TEMPORARY$string<'xValue1'>
| $TEMPORARY$string<'xValue2'>
| $TEMPORARY$string<'xValue3'>
| $TEMPORARY$string<'xValue4'>
| $TEMPORARY$string<'yValue'>
| $TEMPORARY$string<'yValue1'>
| $TEMPORARY$string<'yValue2'>
| $TEMPORARY$string<'yValue3'>
| $TEMPORARY$string<'yValue4'>,
) {
setRecoilValue(store, recoilValue, value);
}
testRecoil('Read fallback by default', () => {
expect(get(pAtom({k: 'x'}))).toBe('fallback');
});
testRecoil('Uses value for parameter', () => {
set(pAtom({k: 'x'}), 'xValue');
set(pAtom({k: 'y'}), 'yValue');
expect(get(pAtom({k: 'x'}))).toBe('xValue');
expect(get(pAtom({k: 'y'}))).toBe('yValue');
expect(get(pAtom({k: 'z'}))).toBe('fallback');
});
testRecoil('Works with non-overlapping sets', () => {
set(pAtom({x: 'x'}), 'xValue');
set(pAtom({y: 'y'}), 'yValue');
expect(get(pAtom({x: 'x'}))).toBe('xValue');
expect(get(pAtom({y: 'y'}))).toBe('yValue');
});
describe('Default', () => {
testRecoil('default is optional', () => {
const myAtom = atom<$FlowFixMe>({key: 'atom without default'});
expect(getLoadable(myAtom).state).toBe('loading');
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
});
testRecoil('Works with atom default', () => {
const fallbackAtom = atom({key: 'fallback', default: 0});
const hasFallback = atomFamily<_, {k: string}>({
key: 'hasFallback',
default: fallbackAtom,
});
expect(get(hasFallback({k: 'x'}))).toBe(0);
set(fallbackAtom, 1);
expect(get(hasFallback({k: 'x'}))).toBe(1);
set(hasFallback({k: 'x'}), 2);
expect(get(hasFallback({k: 'x'}))).toBe(2);
expect(get(hasFallback({k: 'y'}))).toBe(1);
});
testRecoil('Works with parameterized default', () => {
const paramDefaultAtom = atomFamily<_, {num: number}>({
key: 'parameterized default',
// $FlowFixMe[missing-local-annot]
default: ({num}) => num,
});
expect(get(paramDefaultAtom({num: 1}))).toBe(1);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(get(paramDefaultAtom({num: 1}))).toBe(3);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
});
testRecoil('Parameterized async default', async () => {
const paramDefaultAtom = atomFamily<_, {num: number}>({
key: 'parameterized async default',
// $FlowFixMe[missing-local-annot]
default: ({num}) =>
num === 1 ? Promise.reject(num) : Promise.resolve(num),
});
await expect(get(paramDefaultAtom({num: 1}))).rejects.toBe(1);
await expect(get(paramDefaultAtom({num: 2}))).resolves.toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(get(paramDefaultAtom({num: 1}))).toBe(3);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
});
testRecoil('Parameterized loadable default', async () => {
const paramDefaultAtom = atomFamily<_, {num: number}>({
key: 'parameterized loadable default',
// $FlowFixMe[missing-local-annot]
default: ({num}) =>
// $FlowFixMe[underconstrained-implicit-instantiation]
num === 1 ? RecoilLoadable.error(num) : RecoilLoadable.of(num),
});
expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasError');
expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(1);
expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue');
expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(getLoadable(paramDefaultAtom({num: 1})).state).toBe('hasValue');
expect(getLoadable(paramDefaultAtom({num: 1})).contents).toBe(3);
expect(getLoadable(paramDefaultAtom({num: 2})).state).toBe('hasValue');
expect(getLoadable(paramDefaultAtom({num: 2})).contents).toBe(2);
});
});
testRecoil('Works with date as parameter', () => {
const dateAtomFamily = atomFamily<_, Date>({
key: 'dateFamily',
// $FlowFixMe[missing-local-annot]
default: _date => 0,
});
expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(0);
expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0);
set(dateAtomFamily(new Date(2021, 2, 25)), 1);
expect(get(dateAtomFamily(new Date(2021, 2, 25)))).toBe(1);
expect(get(dateAtomFamily(new Date(2021, 2, 26)))).toBe(0);
});
testRecoil('Works with parameterized fallback', () => {
const fallbackAtom = atomFamily<_, $FlowFixMe | {num: number}>({
key: 'parameterized fallback default',
// $FlowFixMe[missing-local-annot]
default: ({num}) => num * 10,
});
const paramFallbackAtom = atomFamily<_, {num: number}>({
key: 'parameterized fallback',
default: fallbackAtom,
});
expect(get(paramFallbackAtom({num: 1}))).toBe(10);
expect(get(paramFallbackAtom({num: 2}))).toBe(20);
set(paramFallbackAtom({num: 1}), 3);
expect(get(paramFallbackAtom({num: 1}))).toBe(3);
expect(get(paramFallbackAtom({num: 2}))).toBe(20);
set(fallbackAtom({num: 2}), 200);
expect(get(paramFallbackAtom({num: 2}))).toBe(200);
set(fallbackAtom({num: 1}), 100);
expect(get(paramFallbackAtom({num: 1}))).toBe(3);
expect(get(paramFallbackAtom({num: 2}))).toBe(200);
});
testRecoil('atomFamily async fallback', async () => {
const paramFallback = atomFamily<_, {}>({
key: 'paramaterizedAtom async Fallback',
default: Promise.resolve(42),
});
const container = renderElements(<ReadsAtom atom={paramFallback({})} />);
expect(container.textContent).toEqual('loading');
act(() => jest.runAllTimers());
await flushPromisesAndTimers();
expect(container.textContent).toEqual('42');
});
testRecoil('Parameterized fallback with atom and async', async () => {
const paramFallback = atomFamily<_, {param: string}>({
key: 'parameterized async Fallback',
// $FlowFixMe[missing-local-annot]
default: ({param}) =>
({
value: 'value',
atom: atom({key: `param async fallback atom ${id++}`, default: 'atom'}),
async: Promise.resolve('async'),
}[param]),
});
const valueCont = renderElements(
<ReadsAtom atom={paramFallback({param: 'value'})} />,
);
expect(valueCont.textContent).toEqual('"value"');
const atomCont = renderElements(
<ReadsAtom atom={paramFallback({param: 'atom'})} />,
);
expect(atomCont.textContent).toEqual('"atom"');
const asyncCont = renderElements(
<ReadsAtom atom={paramFallback({param: 'async'})} />,
);
expect(asyncCont.textContent).toEqual('loading');
act(() => jest.runAllTimers());
await flushPromisesAndTimers();
expect(asyncCont.textContent).toEqual('"async"');
});
fbOnlyTest('atomFamily with scope', () => {
const scopeForParamAtom = atom<string>({
key: 'scope atom for atomFamily',
default: 'foo',
});
const paramAtomWithScope = atomFamily<string, {k: string}>({
key: 'parameterized atom with scope',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [[scopeForParamAtom]],
});
expect(get(paramAtomWithScope({k: 'x'}))).toBe('default');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'x'}), 'xValue1');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'y'}), 'yValue1');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue1');
set(scopeForParamAtom, 'bar');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('default');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'x'}), 'xValue2');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('default');
set(paramAtomWithScope({k: 'y'}), 'yValue2');
expect(get(paramAtomWithScope({k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithScope({k: 'y'}))).toBe('yValue2');
});
fbOnlyTest('atomFamily with parameterized scope', () => {
const paramScopeForParamAtom = atomFamily<string, {namespace: string}>({
key: 'scope atom for atomFamily with parameterized scope',
default: ({namespace}) => namespace,
});
const paramAtomWithParamScope = atomFamily<string, {k: string, n: string}>({
key: 'parameterized atom with parameterized scope',
default: 'default',
scopeRules_APPEND_ONLY_READ_THE_DOCS: [
[({n}) => paramScopeForParamAtom({namespace: n})],
],
});
expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('foo');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue1');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue1');
set(paramScopeForParamAtom({namespace: 'foo'}), 'eggs');
expect(get(paramScopeForParamAtom({namespace: 'foo'}))).toBe('eggs');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'x'}), 'xValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'foo', k: 'y'}), 'yValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'x'}))).toBe('xValue2');
expect(get(paramAtomWithParamScope({n: 'foo', k: 'y'}))).toBe('yValue2');
expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('bar');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue3');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue3');
set(paramScopeForParamAtom({namespace: 'bar'}), 'spam');
expect(get(paramScopeForParamAtom({namespace: 'bar'}))).toBe('spam');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('default');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'x'}), 'xValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('default');
set(paramAtomWithParamScope({n: 'bar', k: 'y'}), 'yValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'x'}))).toBe('xValue4');
expect(get(paramAtomWithParamScope({n: 'bar', k: 'y'}))).toBe('yValue4');
});
testRecoil('Returns the fallback for parameterized atoms', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
let setAtomParam;
let setAtomValue;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{
getAtom: () => null | (Parameter => RecoilState<number>),
}>) {
const [param, setParam] = useState({num: 1});
setAtomParam = setParam;
// flowlint-next-line unclear-type:off
const myAtom: any = getAtom();
const [value, setValue] = useRecoilState(myAtom(param));
setAtomValue = setValue;
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtomFamilyWithFallback', 123),
);
});
const fallback = atom<number>({
key: 'fallback for atomFamily',
default: 222,
});
theAtom = atomFamily<_, Parameter>({
key: 'notDefinedYetAtomFamilyWithFallback',
default: fallback,
persistence_UNSTABLE: {
type: 'url',
validator: (_, returnFallback) => returnFallback,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('222');
act(() => {
setAtomValue(111);
});
expect(container.textContent).toBe('111');
act(() => {
setAtomParam({num: 2});
});
expect(container.textContent).toBe('222');
});
testRecoil(
'Returns the fallback for parameterized atoms with a selector as the fallback',
() => {
let theAtom = null;
let setUnvalidatedAtomValues;
let setAtomParam;
let setAtomValue;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by
* Flow's LTI update could not be added via codemod */
function MyReadsAtom({getAtom}) {
const [param, setParam] = useState({num: 10});
setAtomParam = setParam;
// flowlint-next-line unclear-type:off
const myAtom: any = getAtom();
const [value, setValue] = useRecoilState(myAtom(param));
setAtomValue = setValue;
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set(
'notDefinedYetAtomFamilyFallbackSel',
123,
),
);
});
theAtom = atomFamily<_, $FlowFixMe>({
key: 'notDefinedYetAtomFamilyFallbackSel',
default: selectorFamily({
key: 'notDefinedYetAtomFamilyFallbackSelFallback',
get:
(
// $FlowFixMe[missing-local-annot]
{num},
) =>
() =>
num === 1 ? 456 : 789,
}),
persistence_UNSTABLE: {
type: 'url',
validator: (_, notValid) => notValid,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('789');
act(() => {
setAtomValue(111);
});
expect(container.textContent).toBe('111');
act(() => {
setAtomParam({num: 1});
});
expect(container.textContent).toBe('456');
},
);
testRecoil('Independent atom subscriptions', ({gks}) => {
const BASE_CALLS =
reactMode().mode === 'LEGACY' &&
!gks.includes('recoil_suppress_rerender_in_callback')
? 1
: 0;
const myAtom = atomFamily<_, string>({
key: 'atomFamily/independent subscriptions',
default: 'DEFAULT',
});
const TrackingComponent = (
param: $TEMPORARY$string<'A'> | $TEMPORARY$string<'B'>,
) => {
let numUpdates = 0;
let setValue;
const Component = () => {
setValue = useSetRecoilState(myAtom(param));
return (
<Profiler
id="test"
onRender={() => {
numUpdates++;
}}>
{stableStringify(useRecoilValue(myAtom(param)))}
</Profiler>
);
};
// $FlowFixMe[incompatible-call]
return [Component, (value: number) => setValue(value), () => numUpdates];
};
const [ComponentA, setValueA, getNumUpdatesA] = TrackingComponent('A');
const [ComponentB, setValueB, getNumUpdatesB] = TrackingComponent('B');
const container = renderElements(
<>
<ComponentA />
<ComponentB />
</>,
);
// Initial:
expect(container.textContent).toBe('"DEFAULT""DEFAULT"');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 1);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 1);
// After setting at parameter A, component A should update:
act(() => setValueA(1));
expect(container.textContent).toBe('1"DEFAULT"');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 2);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 1);
// After setting at parameter B, component B should update:
act(() => setValueB(2));
expect(container.textContent).toBe('12');
expect(getNumUpdatesA()).toBe(BASE_CALLS + 2);
expect(getNumUpdatesB()).toBe(BASE_CALLS + 2);
});
describe('Effects', () => {
testRecoil('Initialization', () => {
let inited = 0;
const myFamily = atomFamily<string, number>({
key: 'atomFamily effect init',
default: 'DEFAULT',
effects: [
({setSelf}) => {
inited++;
setSelf('INIT');
},
],
});
expect(inited).toEqual(0);
expect(get(myFamily(1))).toEqual('INIT');
expect(inited).toEqual(1);
set(myFamily(2));
expect(inited).toEqual(2);
const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom(
myFamily(1),
);
const c = renderElements(<ReadsWritesAtom />);
expect(c.textContent).toEqual('"INIT"');
act(reset);
expect(c.textContent).toEqual('"DEFAULT"');
});
testRecoil('Parameterized Initialization', () => {
const myFamily = atomFamily({
key: 'atomFamily effect parameterized init',
default: 'DEFAULT',
// $FlowFixMe[missing-local-annot]
effects: param => [({setSelf}) => setSelf(param)],
});
expect(get(myFamily(1))).toEqual(1);
expect(get(myFamily(2))).toEqual(2);
});
testRecoil('Cleanup Handlers - when root unmounted', () => {
const refCounts: {[string]: number} = {A: 0, B: 0};
const atoms = atomFamily({
key: 'atomFamily effect cleanup',
// $FlowFixMe[missing-local-annot]
default: p => p,
// $FlowFixMe[missing-local-annot]
effects: p => [
() => {
refCounts[p]++;
return () => {
refCounts[p]--;
};
},
],
});
let setNumRoots;
function App() {
const [numRoots, _setNumRoots] = useState(0);
setNumRoots = _setNumRoots;
return (
<div>
{Array(numRoots)
.fill(null)
.map((_, idx) => (
<RecoilRoot key={idx}>
<ReadsAtom atom={atoms('A')} />
<ReadsAtom atom={atoms('B')} />
</RecoilRoot>
))}
</div>
);
}
const c = renderElements(<App />);
expect(c.textContent).toBe('');
expect(refCounts).toEqual({A: 0, B: 0});
act(() => setNumRoots(1));
expect(c.textContent).toBe('"A""B"');
expect(refCounts).toEqual({A: 1, B: 1});
act(() => setNumRoots(2));
expect(c.textContent).toBe('"A""B""A""B"');
expect(refCounts).toEqual({A: 2, B: 2});
act(() => setNumRoots(1));
expect(c.textContent).toBe('"A""B"');
expect(refCounts).toEqual({A: 1, B: 1});
act(() => setNumRoots(0));
expect(c.textContent).toBe('');
expect(refCounts).toEqual({A: 0, B: 0});
});
testRecoil('storeID matches <RecoilRoot>', async () => {
const atoms = atomFamily({
key: 'atomFamily effect - storeID',
default: 'DEFAULT',
// $FlowFixMe[missing-local-annot]
effects: rootKey => [
({storeID, setSelf}) => {
expect(storeID).toEqual(storeIDs[rootKey]);
setSelf(rootKey);
},
],
});
const storeIDs: {[string]: StoreIDType} = {};
function StoreID({
rootKey,
}:
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A1'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'A2'>}>
| $TEMPORARY$object<{rootKey: $TEMPORARY$string<'B'>}>) {
const storeID = useRecoilStoreID();
storeIDs[rootKey] = storeID;
return null;
}
function MyApp() {
return (
<div>
<RecoilRoot>
<StoreID rootKey="A" />
<ReadsAtom atom={atoms('A')} />
<RecoilRoot>
<StoreID rootKey="A1" />
<ReadsAtom atom={atoms('A1')} />
</RecoilRoot>
<RecoilRoot override={false}>
<StoreID rootKey="A2" />
<ReadsAtom atom={atoms('A2')} />
</RecoilRoot>
</RecoilRoot>
<RecoilRoot>
<StoreID rootKey="B" />
<ReadsAtom atom={atoms('B')} />
</RecoilRoot>
</div>
);
}
const c = renderElements(<MyApp />);
expect(c.textContent).toEqual('"A""A1""A2""B"');
expect('A' in storeIDs).toEqual(true);
expect('A1' in storeIDs).toEqual(true);
expect('A2' in storeIDs).toEqual(true);
expect('B' in storeIDs).toEqual(true);
expect(storeIDs.A).not.toEqual(storeIDs.B);
expect(storeIDs.A).not.toEqual(storeIDs.A1);
expect(storeIDs.A).toEqual(storeIDs.A2);
expect(storeIDs.B).not.toEqual(storeIDs.A1);
expect(storeIDs.B).not.toEqual(storeIDs.A2);
});
});

View File

@@ -0,0 +1,260 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValue} from '../../core/Recoil_RecoilValue';
import type {Store} from '../../core/Recoil_State';
import type {NodeKey} from 'Recoil_Keys';
import type {RecoilState} from 'Recoil_RecoilValue';
import type {Node} from 'react';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let React,
useState,
act,
getRecoilValueAsLoadable,
setRecoilValue,
subscribeToRecoilValue,
useRecoilState,
useSetUnvalidatedAtomValues,
componentThatReadsAndWritesAtom,
renderElements,
atom,
constSelector,
store: Store;
let fallbackAtom: RecoilValue<number>, hasFallbackAtom: RecoilValue<number>;
let id = 0;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
React = require('react');
({useState} = require('react'));
({act} = require('ReactTestUtils'));
({
getRecoilValueAsLoadable,
setRecoilValue,
subscribeToRecoilValue,
} = require('../../core/Recoil_RecoilValueInterface'));
({
useRecoilState,
useSetUnvalidatedAtomValues,
} = require('../../hooks/Recoil_Hooks'));
({
componentThatReadsAndWritesAtom,
renderElements,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils'));
atom = require('../Recoil_atom');
constSelector = require('../Recoil_constSelector');
store = makeStore();
fallbackAtom = atom<number>({key: `fallback${id}`, default: 1});
hasFallbackAtom = atom<number>({
key: `hasFallback${id++}`,
default: fallbackAtom,
});
subscribeToRecoilValue(store, hasFallbackAtom, () => undefined);
});
function get(
recoilValue: RecoilState<string> | RecoilState<?string> | RecoilValue<number>,
) {
return getRecoilValueAsLoadable(store, recoilValue).contents;
}
function set(
recoilValue: RecoilState<?string> | RecoilValue<number>,
value: ?(number | $TEMPORARY$string<'VALUE'>),
) {
setRecoilValue(store, recoilValue, value);
}
testRecoil('atomWithFallback', () => {
expect(get(hasFallbackAtom)).toBe(1);
set(fallbackAtom, 2);
expect(get(hasFallbackAtom)).toBe(2);
set(hasFallbackAtom, 3);
expect(get(hasFallbackAtom)).toBe(3);
});
describe('ReturnDefaultOrFallback', () => {
testRecoil('Returns the default', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{getAtom: () => null | RecoilState<number>}>) {
// flowlint-next-line unclear-type:off
const [value] = useRecoilState((getAtom(): any));
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtomValidator', 123),
);
});
theAtom = atom({
key: 'notDefinedYetAtomValidator',
default: 456,
persistence_UNSTABLE: {
type: 'url',
validator: (_, returnFallback) => returnFallback,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('456');
});
testRecoil('Returns the fallback', () => {
let theAtom = null;
let setUnvalidatedAtomValues;
function SetsUnvalidatedAtomValues() {
setUnvalidatedAtomValues = useSetUnvalidatedAtomValues();
return null;
}
let setVisible;
function Switch({children}: $TEMPORARY$object<{children: Node}>) {
const [visible, mySetVisible] = useState(false);
setVisible = mySetVisible;
return visible ? children : null;
}
function MyReadsAtom({
getAtom,
}: $TEMPORARY$object<{getAtom: () => null | RecoilState<number>}>) {
// flowlint-next-line unclear-type:off
const [value] = useRecoilState((getAtom(): any));
return value;
}
const container = renderElements(
<>
<SetsUnvalidatedAtomValues />
<Switch>
<MyReadsAtom getAtom={() => theAtom} />
</Switch>
</>,
);
act(() => {
setUnvalidatedAtomValues(
new Map<NodeKey, mixed>().set('notDefinedYetAtomWithFallback', 123),
);
});
const fallback = atom<number>({
key: 'notDefinedYetAtomFallback',
default: 222,
});
theAtom = atom({
key: 'notDefinedYetAtomWithFallback',
default: fallback,
persistence_UNSTABLE: {
type: 'url',
validator: (_, returnFallback) => returnFallback,
},
});
act(() => {
setVisible(true);
});
expect(container.textContent).toBe('222');
});
});
testRecoil('Atom with atom fallback can store null and undefined', () => {
const myFallbackAtom = atom<?string>({
key: 'fallback for null undefined',
default: 'FALLBACK',
});
const myAtom = atom<?string>({
key: 'fallback atom with undefined',
default: myFallbackAtom,
});
expect(get(myAtom)).toBe('FALLBACK');
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
act(() => set(myAtom, null));
expect(get(myAtom)).toBe(null);
act(() => set(myAtom, undefined));
expect(get(myAtom)).toBe(undefined);
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
});
testRecoil('Atom with selector fallback can store null and undefined', () => {
const fallbackSelector = constSelector('FALLBACK');
const myAtom = atom<?string>({
key: 'fallback selector with undefined',
default: fallbackSelector,
});
expect(get(myAtom)).toBe('FALLBACK');
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
act(() => set(myAtom, null));
expect(get(myAtom)).toBe(null);
act(() => set(myAtom, undefined));
expect(get(myAtom)).toBe(undefined);
act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
});
testRecoil('Effects', () => {
let inited = false;
const myFallbackAtom = atom({
key: 'atom with fallback effects init fallback',
default: 'FALLBACK',
});
const myAtom = atom<string>({
key: 'atom with fallback effects init',
default: myFallbackAtom,
effects: [
({setSelf}) => {
inited = true;
setSelf('INIT');
},
],
});
expect(get(myAtom)).toEqual('INIT');
expect(inited).toEqual(true);
const [ReadsWritesAtom, _, reset] = componentThatReadsAndWritesAtom(myAtom);
const c = renderElements(<ReadsWritesAtom />);
expect(c.textContent).toEqual('"INIT"');
act(reset);
expect(c.textContent).toEqual('"FALLBACK"');
});

View File

@@ -0,0 +1,106 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValue} from '../../core/Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let getRecoilValueAsLoadable, store, constSelector;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
({
getRecoilValueAsLoadable,
} = require('../../core/Recoil_RecoilValueInterface'));
constSelector = require('../Recoil_constSelector');
store = makeStore();
});
function get<T>(recoilValue: RecoilValue<T>): T {
return getRecoilValueAsLoadable<T>(store, recoilValue).valueOrThrow();
}
testRecoil('constSelector - string', () => {
const mySelector = constSelector('HELLO');
expect(get(mySelector)).toEqual('HELLO');
expect(get(mySelector)).toBe('HELLO');
});
testRecoil('constSelector - number', () => {
const mySelector = constSelector(42);
expect(get(mySelector)).toEqual(42);
expect(get(mySelector)).toBe(42);
});
testRecoil('constSelector - null', () => {
const mySelector = constSelector(null);
expect(get(mySelector)).toEqual(null);
expect(get(mySelector)).toBe(null);
});
testRecoil('constSelector - boolean', () => {
const mySelector = constSelector(true);
expect(get(mySelector)).toEqual(true);
expect(get(mySelector)).toBe(true);
});
testRecoil('constSelector - array', () => {
const emptyArraySelector = constSelector(([]: Array<$FlowFixMe>));
expect(get(emptyArraySelector)).toEqual([]);
const numberArray = [1, 2, 3];
const numberArraySelector = constSelector(numberArray);
expect(get(numberArraySelector)).toEqual([1, 2, 3]);
expect(get(numberArraySelector)).toBe(numberArray);
});
testRecoil('constSelector - object', () => {
const emptyObjSelector = constSelector({});
expect(get(emptyObjSelector)).toEqual({});
const obj = {foo: 'bar'};
const objSelector = constSelector(obj);
expect(get(objSelector)).toEqual({foo: 'bar'});
expect(get(objSelector)).toBe(obj);
// Calling a second time with same object provides the same selector
const objSelector2 = constSelector(obj);
expect(objSelector2).toBe(objSelector);
expect(get(objSelector2)).toEqual({foo: 'bar'});
expect(get(objSelector2)).toBe(obj);
// Calling a third time with similar but different object provides
// a new selector for the new reference.
const newObj = {foo: 'bar'};
const objSelector3 = constSelector(newObj);
expect(get(objSelector3)).toEqual({foo: 'bar'});
expect(get(objSelector3)).toBe(newObj);
});
testRecoil('constSelector - function', () => {
const foo = () => 'FOO';
const bar = () => 'BAR';
const fooSelector = constSelector(foo);
const barSelector = constSelector(bar);
expect(get(fooSelector)()).toEqual('FOO');
expect(get(barSelector)()).toEqual('BAR');
expect(constSelector(foo)).toEqual(fooSelector);
});

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValueReadOnly} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let store, getRecoilValueAsLoadable, errorSelector;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
({
getRecoilValueAsLoadable,
} = require('../../core/Recoil_RecoilValueInterface'));
errorSelector = require('../Recoil_errorSelector');
store = makeStore();
});
function getError(recoilValue: RecoilValueReadOnly<mixed>): Error {
const error = getRecoilValueAsLoadable(store, recoilValue).errorOrThrow();
if (!(error instanceof Error)) {
throw new Error('Expected error to be an instance of Error');
}
return error;
}
testRecoil('errorSelector - string', () => {
const mySelector = errorSelector<mixed>('My Error');
expect(getError(mySelector).message).toEqual(
expect.stringContaining('My Error'),
);
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,358 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
* @oncall recoil
*/
'use strict';
import type {RecoilValue} from '../../core/Recoil_RecoilValue';
import type {RecoilState} from 'Recoil_RecoilValue';
const {
getRecoilTestFn,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
let atom,
DefaultValue,
selectorFamily,
getRecoilValueAsLoadable,
setRecoilValue,
store,
myAtom;
const testRecoil = getRecoilTestFn(() => {
const {
makeStore,
} = require('recoil-shared/__test_utils__/Recoil_TestingUtils');
atom = require('../Recoil_atom');
({DefaultValue} = require('../../core/Recoil_Node'));
selectorFamily = require('../Recoil_selectorFamily');
({
getRecoilValueAsLoadable,
setRecoilValue,
} = require('../../core/Recoil_RecoilValueInterface'));
store = makeStore();
myAtom = atom({
key: 'atom',
default: 0,
});
});
function getValue<T>(recoilValue: RecoilValue<T>): T {
return getRecoilValueAsLoadable<T>(store, recoilValue).valueOrThrow();
}
function set(recoilValue: RecoilState<number>, value: number) {
setRecoilValue(store, recoilValue, value);
}
testRecoil('selectorFamily - number parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/number',
get:
(
// $FlowFixMe[missing-local-annot]
multiplier,
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
});
set(myAtom, 1);
expect(getValue(mySelector(10))).toBe(10);
expect(getValue(mySelector(100))).toBe(100);
set(myAtom, 2);
expect(getValue(mySelector(10))).toBe(20);
expect(getValue(mySelector(100))).toBe(200);
});
testRecoil('selectorFamily - array parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/array',
// $FlowFixMe[missing-local-annot]
get: numbers => () => numbers.reduce((x, y) => x + y, 0),
});
expect(getValue(mySelector([]))).toBe(0);
expect(getValue(mySelector([1, 2, 3]))).toBe(6);
expect(getValue(mySelector([0, 1, 1, 2, 3, 5]))).toBe(12);
});
testRecoil('selectorFamily - object parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/object',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
});
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
set(myAtom, 2);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
});
testRecoil('selectorFamily - date parameter', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/date',
get:
(
// $FlowFixMe[missing-local-annot]
date,
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
const daysToAdd = get(myAtom);
const returnDate = new Date(date);
returnDate.setDate(returnDate.getDate() + daysToAdd);
return returnDate;
},
});
set(myAtom, 1);
expect(getValue(mySelector(new Date(2021, 2, 25))).getDate()).toBe(26);
set(myAtom, 2);
expect(getValue(mySelector(new Date(2021, 2, 25))).getDate()).toBe(27);
});
testRecoil('Works with supersets', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/supersets',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
});
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(getValue(mySelector({multiplier: 100, extra: 'foo'}))).toBe(100);
});
testRecoil('selectorFamily - writable', () => {
const mySelector = selectorFamily({
key: 'selectorFamily/writable',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) =>
get(myAtom) * multiplier,
set:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({set}, num) =>
set(myAtom, num instanceof DefaultValue ? num : num / multiplier),
});
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
set(mySelector({multiplier: 10}), 20);
expect(getValue(myAtom)).toBe(2);
set(mySelector({multiplier: 10}), 30);
expect(getValue(myAtom)).toBe(3);
set(mySelector({multiplier: 100}), 400);
expect(getValue(myAtom)).toBe(4);
});
testRecoil('selectorFamily - value caching', () => {
let evals = 0;
const mySelector = selectorFamily({
key: 'selectorFamily/value caching',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
evals++;
return get(myAtom) * multiplier;
},
});
expect(evals).toBe(0);
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(1);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(2);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(2);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(2);
set(myAtom, 2);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(3);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(3);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(4);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(4);
});
testRecoil('selectorFamily - reference caching', () => {
let evals = 0;
const mySelector = selectorFamily({
key: 'selectorFamily/reference caching',
get:
(
// $FlowFixMe[missing-local-annot]
{multiplier},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => {
evals++;
return get(myAtom) * multiplier;
},
cachePolicyForParams_UNSTABLE: {
equality: 'reference',
},
});
expect(evals).toBe(0);
set(myAtom, 1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(1);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(2);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(3);
expect(getValue(mySelector({multiplier: 100}))).toBe(100);
expect(evals).toBe(4);
expect(getValue(mySelector({multiplier: 10}))).toBe(10);
expect(evals).toBe(5);
set(myAtom, 2);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(6);
expect(getValue(mySelector({multiplier: 10}))).toBe(20);
expect(evals).toBe(7);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(8);
expect(getValue(mySelector({multiplier: 100}))).toBe(200);
expect(evals).toBe(9);
const multiply10 = {multiplier: 10};
const multiply100 = {multiplier: 100};
set(myAtom, 1);
expect(getValue(mySelector(multiply10))).toBe(10);
expect(evals).toBe(10);
expect(getValue(mySelector(multiply10))).toBe(10);
expect(evals).toBe(10);
expect(getValue(mySelector(multiply100))).toBe(100);
expect(evals).toBe(11);
expect(getValue(mySelector(multiply100))).toBe(100);
expect(evals).toBe(11);
expect(getValue(mySelector(multiply10))).toBe(10);
expect(evals).toBe(11);
set(myAtom, 2);
expect(getValue(mySelector(multiply10))).toBe(20);
expect(evals).toBe(12);
expect(getValue(mySelector(multiply10))).toBe(20);
expect(evals).toBe(12);
expect(getValue(mySelector(multiply100))).toBe(200);
expect(evals).toBe(13);
expect(getValue(mySelector(multiply100))).toBe(200);
expect(evals).toBe(13);
});
// Parameterized selector results should be frozen unless
// dangerouslyAllowMutability is set
testRecoil('selectorFamily - mutability', () => {
const myImmutableSelector = selectorFamily({
key: 'selectorFamily/immutable',
get:
(
// $FlowFixMe[missing-local-annot]
{key},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => ({[key]: get(myAtom)}),
});
set(myAtom, 42);
const immutableResult: {[string]: number, ...} = getValue(
myImmutableSelector({key: 'foo'}),
);
expect(immutableResult).toEqual({foo: 42});
expect(() => {
immutableResult.foo = 2600;
}).toThrow();
const myMutableSelector = selectorFamily({
key: 'selectorFamily/mutable',
get:
(
// $FlowFixMe[missing-local-annot]
{key},
) =>
// $FlowFixMe[missing-local-annot]
({get}) => ({[key]: get(myAtom)}),
dangerouslyAllowMutability: true,
});
set(myAtom, 42);
const mutableResult: {[string]: number, ...} = getValue(
myMutableSelector({key: 'foo'}),
);
expect(mutableResult).toEqual({foo: 42});
mutableResult.foo = 2600;
expect(mutableResult).toEqual({foo: 2600});
});
testRecoil('selectorFamily - evaluate to RecoilValue', () => {
const atomA = atom({key: 'selectorFamily/const atom A', default: 'A'});
const atomB = atom({key: 'selectorFamily/const atom B', default: 'B'});
const mySelector = selectorFamily<string, string>({
key: 'selectorFamily/',
get: param => () => param === 'a' ? atomA : atomB,
});
expect(getValue(mySelector('a'))).toEqual('A');
expect(getValue(mySelector('b'))).toEqual('B');
});
testRecoil('selectorFamily - invalid parameter error message', () => {
const mySelector = selectorFamily<_, {foo: () => void}>({
key: 'function in parameter',
get: () => () => {},
});
expect(() => getValue(mySelector({foo: () => {}}))).toThrow(
'function in parameter',
);
});

File diff suppressed because it is too large Load Diff

9191
node_modules/recoil/es/index.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1
node_modules/recoil/es/index.mjs generated vendored Normal file

File diff suppressed because one or more lines are too long

593
node_modules/recoil/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,593 @@
// Minimum TypeScript Version: 3.9
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @oncall recoil
*/
/**
* This file is a manual translation of the flow types, which are the source of truth, so we should not introduce new terminology or behavior in this file.
*/
export { };
import * as React from 'react';
// state.d.ts
type NodeKey = string;
// node.d.ts
export class DefaultValue {
private __tag: 'DefaultValue';
}
// recoilRoot.d.ts
export type RecoilRootProps = {
initializeState?: (mutableSnapshot: MutableSnapshot) => void,
override?: true,
children: React.ReactNode,
} | {
override: false,
children: React.ReactNode,
};
// recoilEnv.d.ts
export interface RecoilEnv {
RECOIL_DUPLICATE_ATOM_KEY_CHECKING_ENABLED: boolean;
RECOIL_GKS_ENABLED: Set<string>;
}
export const RecoilEnv: RecoilEnv;
/**
* Root component for managing Recoil state. Most Recoil hooks should be
* called from a component nested in a <RecoilRoot>
*/
export const RecoilRoot: React.FC<RecoilRootProps>;
// Snapshot.d.ts
declare const SnapshotID_OPAQUE: unique symbol;
export interface SnapshotID {
readonly [SnapshotID_OPAQUE]: true;
}
interface ComponentInfo {
name: string;
}
interface RecoilStateInfo<T> {
loadable?: Loadable<T>;
isActive: boolean;
isSet: boolean;
isModified: boolean; // TODO report modified selectors
type: 'atom' | 'selector';
deps: Iterable<RecoilValue<T>>;
subscribers: {
nodes: Iterable<RecoilValue<T>>,
components: Iterable<ComponentInfo>,
};
}
export class Snapshot {
getID(): SnapshotID;
getLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T>;
getPromise<T>(recoilValue: RecoilValue<T>): Promise<T>;
getNodes_UNSTABLE(opts?: { isModified?: boolean, isInitialized?: boolean }): Iterable<RecoilValue<unknown>>;
getInfo_UNSTABLE<T>(recoilValue: RecoilValue<T>): RecoilStateInfo<T>;
map(cb: (mutableSnapshot: MutableSnapshot) => void): Snapshot;
asyncMap(cb: (mutableSnapshot: MutableSnapshot) => Promise<void>): Promise<Snapshot>;
retain(): () => void;
isRetained(): boolean;
}
export class MutableSnapshot extends Snapshot {
set: SetRecoilState;
reset: ResetRecoilState;
}
declare const WrappedValue_OPAQUE: unique symbol;
export interface WrappedValue<T> {
readonly [WrappedValue_OPAQUE]: true;
}
// Effect is called the first time a node is used with a <RecoilRoot>
export type AtomEffect<T> = (param: {
node: RecoilState<T>,
storeID: StoreID,
parentStoreID_UNSTABLE?: StoreID,
trigger: 'set' | 'get',
// Call synchronously to initialize value or async to change it later
setSelf: (param:
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>
| ((param: T | DefaultValue) => T | DefaultValue | WrappedValue<T>),
) => void,
resetSelf: () => void,
// Subscribe callbacks to events.
// Atom effect observers are called before global transaction observers
onSet: (
param: (newValue: T, oldValue: T | DefaultValue, isReset: boolean) => void,
) => void,
// Accessors to read other atoms/selectors
getPromise: <S>(recoilValue: RecoilValue<S>) => Promise<S>,
getLoadable: <S>(recoilValue: RecoilValue<S>) => Loadable<S>,
getInfo_UNSTABLE: <S>(recoilValue: RecoilValue<S>) => RecoilStateInfo<S>,
}) => void | (() => void);
// atom.d.ts
interface AtomOptionsWithoutDefault<T> {
key: NodeKey;
effects?: ReadonlyArray<AtomEffect<T>>;
effects_UNSTABLE?: ReadonlyArray<AtomEffect<T>>;
dangerouslyAllowMutability?: boolean;
}
interface AtomOptionsWithDefault<T> extends AtomOptionsWithoutDefault<T> {
default: RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T> | T;
}
export type AtomOptions<T> = AtomOptionsWithoutDefault<T> | AtomOptionsWithDefault<T>;
/**
* Creates an atom, which represents a piece of writeable state
*/
export function atom<T>(options: AtomOptions<T>): RecoilState<T>;
export namespace atom {
function value<T>(value: T): WrappedValue<T>;
}
export type GetRecoilValue = <T>(recoilVal: RecoilValue<T>) => T;
export type SetterOrUpdater<T> = (valOrUpdater: ((currVal: T) => T) | T) => void;
export type Resetter = () => void;
export interface TransactionInterface_UNSTABLE {
get<T>(a: RecoilValue<T>): T;
set<T>(s: RecoilState<T>, u: ((currVal: T) => T) | T): void;
reset(s: RecoilState<any>): void;
}
export interface CallbackInterface {
set: <T>(recoilVal: RecoilState<T>, valOrUpdater: ((currVal: T) => T) | T) => void;
reset: (recoilVal: RecoilState<any>) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
refresh: (recoilValue: RecoilValue<any>) => void;
snapshot: Snapshot;
gotoSnapshot: (snapshot: Snapshot) => void;
transact_UNSTABLE: (cb: (i: TransactionInterface_UNSTABLE) => void) => void;
}
// selector.d.ts
export interface SelectorCallbackInterface extends CallbackInterface {
node: RecoilState<unknown>; // TODO This isn't properly typed
}
export type GetCallback = <Args extends ReadonlyArray<unknown>, Return>(
fn: (interface: SelectorCallbackInterface) => (...args: Args) => Return,
) => (...args: Args) => Return;
export type SetRecoilState = <T>(
recoilVal: RecoilState<T>,
newVal: T | DefaultValue | ((prevValue: T) => T | DefaultValue),
) => void;
export type ResetRecoilState = (recoilVal: RecoilState<any>) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
// export type EqualityPolicy = 'reference' | 'value'; TODO: removing while we discuss long term API
export type EvictionPolicy = 'lru' | 'keep-all' | 'most-recent';
// TODO: removing while we discuss long term API
// export type CachePolicy =
// | {eviction: 'lru', maxSize: number, equality?: EqualityPolicy}
// | {eviction: 'none', equality?: EqualityPolicy}
// | {eviction?: undefined, equality: EqualityPolicy};
// TODO: removing while we discuss long term API
// export interface CachePolicyWithoutEviction {
// equality: EqualityPolicy;
// }
export type CachePolicyWithoutEquality = {eviction: 'lru', maxSize: number} | {eviction: 'keep-all'} | {eviction: 'most-recent'};
export interface ReadOnlySelectorOptions<T> {
key: string;
get: (opts: {
get: GetRecoilValue,
getCallback: GetCallback,
}) => Promise<T> | RecoilValue<T> | Loadable<T> | WrappedValue<T> | T;
dangerouslyAllowMutability?: boolean;
cachePolicy_UNSTABLE?: CachePolicyWithoutEquality; // TODO: using the more restrictive CachePolicyWithoutEquality while we discuss long term API
}
export interface ReadWriteSelectorOptions<T> extends ReadOnlySelectorOptions<T> {
set: (
opts: {
set: SetRecoilState;
get: GetRecoilValue;
reset: ResetRecoilState;
},
newValue: T | DefaultValue,
) => void;
}
/**
* Creates a selector which represents derived state.
*/
export function selector<T>(options: ReadWriteSelectorOptions<T>): RecoilState<T>;
export function selector<T>(options: ReadOnlySelectorOptions<T>): RecoilValueReadOnly<T>;
export namespace selector {
function value<T>(value: T): WrappedValue<T>;
}
// hooks.d.ts
/**
* Returns the value of an atom or selector (readonly or writeable) and
* subscribes the components to future updates of that state.
*/
export function useRecoilValue<T>(recoilValue: RecoilValue<T>): T;
/**
* Returns a Loadable representing the status of the given Recoil state
* and subscribes the component to future updates of that state. Useful
* for working with async selectors.
*/
export function useRecoilValueLoadable<T>(recoilValue: RecoilValue<T>): Loadable<T>;
/**
* Returns a tuple where the first element is the value of the recoil state
* and the second is a setter to update that state. Subscribes component
* to updates of the given state.
*/
export function useRecoilState<T>(recoilState: RecoilState<T>): [T, SetterOrUpdater<T>];
/**
* Returns a tuple where the first element is a Loadable and the second
* element is a setter function to update the given state. Subscribes
* component to updates of the given state.
*/
export function useRecoilStateLoadable<T>(recoilState: RecoilState<T>): [Loadable<T>, SetterOrUpdater<T>];
/**
* Returns a setter function for updating Recoil state. Does not subscribe
* the component to the given state.
*/
export function useSetRecoilState<T>(recoilState: RecoilState<T>): SetterOrUpdater<T>;
/**
* Returns a function that will reset the given state to its default value.
*/
export function useResetRecoilState(recoilState: RecoilState<any>): Resetter; // eslint-disable-line @typescript-eslint/no-explicit-any
/**
* Returns current info about an atom
*/
export function useGetRecoilValueInfo_UNSTABLE(): <T>(recoilValue: RecoilValue<T>) => RecoilStateInfo<T>;
/**
* Experimental version of hooks for useTransition() support
*/
export function useRecoilValue_TRANSITION_SUPPORT_UNSTABLE<T>(recoilValue: RecoilValue<T>): T;
export function useRecoilValueLoadable_TRANSITION_SUPPORT_UNSTABLE<T>(recoilValue: RecoilValue<T>): Loadable<T>;
export function useRecoilState_TRANSITION_SUPPORT_UNSTABLE<T>(recoilState: RecoilState<T>): [T, SetterOrUpdater<T>];
/**
* Returns a function that will run the callback that was passed when
* calling this hook. Useful for accessing Recoil state in response to
* events.
*/
export function useRecoilCallback<Args extends ReadonlyArray<unknown>, Return>(
fn: (interface: CallbackInterface) => (...args: Args) => Return,
deps?: ReadonlyArray<unknown>,
): (...args: Args) => Return;
/**
* Returns a function that executes an atomic transaction for updating Recoil state.
*/
export function useRecoilTransaction_UNSTABLE<Args extends ReadonlyArray<unknown>>(
fn: (interface: TransactionInterface_UNSTABLE) => (...args: Args) => void,
deps?: ReadonlyArray<unknown>,
): (...args: Args) => void;
export function useRecoilTransactionObserver_UNSTABLE(
callback: (opts: {
snapshot: Snapshot,
previousSnapshot: Snapshot,
}) => void,
): void;
/**
* Updates Recoil state to match the provided snapshot.
*/
export function useGotoRecoilSnapshot(): (snapshot: Snapshot) => void;
/**
* Returns a snapshot of the current Recoil state and subscribes the component
* to re-render when any state is updated.
*/
export function useRecoilSnapshot(): Snapshot;
// useRecoilRefresher.d.ts
/**
* Clears the cache for a selector causing it to be reevaluated.
*/
export function useRecoilRefresher_UNSTABLE(recoilValue: RecoilValue<any>): () => void;
// useRecoilBridgeAcrossReactRoots.d.ts
export const RecoilBridge: React.FC<{children: React.ReactNode}>;
/**
* Returns a component that acts like a <RecoilRoot> but shares the same store
* as the current <RecoilRoot>.
*/
export function useRecoilBridgeAcrossReactRoots_UNSTABLE(): typeof RecoilBridge;
// useRecoilStoreID
declare const StoreID_OPAQUE: unique symbol;
export interface StoreID {
readonly [StoreID_OPAQUE]: true;
}
/**
* Returns an ID for the currently active state store of the host <RecoilRoot>
*/
export function useRecoilStoreID(): StoreID;
// loadable.d.ts
interface BaseLoadable<T> {
getValue: () => T;
toPromise: () => Promise<T>;
valueOrThrow: () => T;
errorOrThrow: () => any;
promiseOrThrow: () => Promise<T>;
is: (other: Loadable<any>) => boolean;
map: <S>(map: (from: T) => Loadable<S> | Promise<S> | S) => Loadable<S>;
}
interface ValueLoadable<T> extends BaseLoadable<T> {
state: 'hasValue';
contents: T;
valueMaybe: () => T;
errorMaybe: () => undefined;
promiseMaybe: () => undefined;
}
interface LoadingLoadable<T> extends BaseLoadable<T> {
state: 'loading';
contents: Promise<T>;
valueMaybe: () => undefined;
errorMaybe: () => undefined;
promiseMaybe: () => Promise<T>;
}
interface ErrorLoadable<T> extends BaseLoadable<T> {
state: 'hasError';
contents: any;
valueMaybe: () => undefined;
errorMaybe: () => any;
promiseMaybe: () => undefined;
}
export type Loadable<T> =
| ValueLoadable<T>
| LoadingLoadable<T>
| ErrorLoadable<T>;
// recoilValue.d.ts
declare class AbstractRecoilValue<T> {
__tag: [T];
__cTag: (t: T) => void; // for contravariance
key: NodeKey;
constructor(newKey: NodeKey);
toJSON(): {key: string};
}
declare class AbstractRecoilValueReadonly<T> {
__tag: [T];
key: NodeKey;
constructor(newKey: NodeKey);
toJSON(): {key: string};
}
export class RecoilState<T> extends AbstractRecoilValue<T> {}
export class RecoilValueReadOnly<T> extends AbstractRecoilValueReadonly<T> {}
export type RecoilValue<T> = RecoilValueReadOnly<T> | RecoilState<T>;
/**
* Returns true if the parameter is a Recoil atom or selector.
*/
export function isRecoilValue(val: unknown): val is RecoilValue<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
/** Utilities */
// bigint not supported yet
type Primitive = undefined | null | boolean | number | symbol | string;
interface HasToJSON { toJSON(): SerializableParam; }
export type SerializableParam =
| Primitive
| HasToJSON
| ReadonlyArray<SerializableParam>
| ReadonlySet<SerializableParam>
| ReadonlyMap<SerializableParam, SerializableParam>
| Readonly<{[key: string]: SerializableParam}>;
interface AtomFamilyOptionsWithoutDefault<T, P extends SerializableParam> {
key: NodeKey;
dangerouslyAllowMutability?: boolean;
effects?: | ReadonlyArray<AtomEffect<T>> | ((param: P) => ReadonlyArray<AtomEffect<T>>);
effects_UNSTABLE?: | ReadonlyArray<AtomEffect<T>> | ((param: P) => ReadonlyArray<AtomEffect<T>>);
// cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction; TODO: removing while we discuss long term API
}
interface AtomFamilyOptionsWithDefault<T, P extends SerializableParam> extends AtomFamilyOptionsWithoutDefault<T, P> {
default:
| RecoilValue<T>
| Promise<T>
| Loadable<T>
| WrappedValue<T>
| T
| ((param: P) => T | RecoilValue<T> | Promise<T> | Loadable<T> | WrappedValue<T>);
}
export type AtomFamilyOptions<T, P extends SerializableParam> =
| AtomFamilyOptionsWithDefault<T, P>
| AtomFamilyOptionsWithoutDefault<T, P>;
/**
* Returns a function which returns a memoized atom for each unique parameter value.
*/
export function atomFamily<T, P extends SerializableParam>(
options: AtomFamilyOptions<T, P>,
): (param: P) => RecoilState<T>;
export interface ReadOnlySelectorFamilyOptions<T, P extends SerializableParam> {
key: string;
get: (param: P) => (opts: {
get: GetRecoilValue,
getCallback: GetCallback,
}) => Promise<T> | RecoilValue<T> | Loadable<T> | WrappedValue<T> | T;
// cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction; TODO: removing while we discuss long term API
cachePolicy_UNSTABLE?: CachePolicyWithoutEquality; // TODO: using the more restrictive CachePolicyWithoutEquality while we discuss long term API
dangerouslyAllowMutability?: boolean;
}
export interface ReadWriteSelectorFamilyOptions<T, P extends SerializableParam> {
key: string;
get: (param: P) => (opts: {
get: GetRecoilValue,
getCallback: GetCallback,
}) => Promise<T> | Loadable<T> | WrappedValue<T> | RecoilValue<T> | T;
set: (
param: P,
) => (
opts: { set: SetRecoilState; get: GetRecoilValue; reset: ResetRecoilState },
newValue: T | DefaultValue,
) => void;
// cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction; TODO: removing while we discuss long term API
cachePolicy_UNSTABLE?: CachePolicyWithoutEquality; // TODO: using the more restrictive CachePolicyWithoutEquality while we discuss long term API
dangerouslyAllowMutability?: boolean;
}
/**
* Returns a function which returns a memoized atom for each unique parameter value.
*/
export function selectorFamily<T, P extends SerializableParam>(
options: ReadWriteSelectorFamilyOptions<T, P>,
): (param: P) => RecoilState<T>;
/**
* Returns a function which returns a memoized atom for each unique parameter value.
*/
export function selectorFamily<T, P extends SerializableParam>(
options: ReadOnlySelectorFamilyOptions<T, P>,
): (param: P) => RecoilValueReadOnly<T>;
/**
* Returns a selector that always has a constant value.
*/
export function constSelector<T extends SerializableParam>(constant: T): RecoilValueReadOnly<T>;
/**
* Returns a selector which is always in the provided error state.
*/
export function errorSelector(message: string): RecoilValueReadOnly<never>;
/**
* Casts a selector to be a read-only selector
*/
export function readOnlySelector<T>(atom: RecoilValue<T>): RecoilValueReadOnly<T>;
/**
* Returns a selector that has the value of the provided atom or selector as a Loadable.
* This means you can use noWait() to avoid entering an error or suspense state in
* order to manually handle those cases.
*/
export function noWait<T>(state: RecoilValue<T>): RecoilValueReadOnly<Loadable<T>>;
/* eslint-disable @typescript-eslint/no-explicit-any */
export type UnwrapRecoilValue<T> = T extends RecoilValue<infer R> ? R : never;
export type UnwrapRecoilValues<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {
[P in keyof T]: UnwrapRecoilValue<T[P]>;
};
export type UnwrapRecoilValueLoadables<T extends Array<RecoilValue<any>> | { [key: string]: RecoilValue<any> }> = {
[P in keyof T]: Loadable<UnwrapRecoilValue<T[P]>>;
};
export function waitForNone<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
export function waitForNone<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
export function waitForAny<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
export function waitForAny<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
export function waitForAll<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;
export function waitForAll<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValues<RecoilValues>>;
export function waitForAllSettled<RecoilValues extends Array<RecoilValue<any>> | [RecoilValue<any>]>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
export function waitForAllSettled<RecoilValues extends { [key: string]: RecoilValue<any> }>(
param: RecoilValues,
): RecoilValueReadOnly<UnwrapRecoilValueLoadables<RecoilValues>>;
export type UnwrapLoadable<T> = T extends Loadable<infer R> ? R : T extends Promise<infer P> ? P : T;
export type UnwrapLoadables<T extends any[] | { [key: string]: any }> = {
[P in keyof T]: UnwrapLoadable<T[P]>;
};
/* eslint-disable @typescript-eslint/no-unused-vars */
export namespace RecoilLoadable {
/**
* Factory to make a Loadable object. If a Promise is provided the Loadable will
* be in a 'loading' state until the Promise is either resolved or rejected.
*/
function of<T>(x: T | Promise<T> | Loadable<T>): Loadable<T>;
/**
* Factory to make a Loadable object in an error state.
*/
function error(x: any): ErrorLoadable<any>;
/**
* Factory to make a loading Loadable which never resolves.
*/
function loading(): LoadingLoadable<any>;
/**
* Factory to make a Loadable which is resolved when all of the Loadables provided
* to it are resolved or any one has an error. The value is an array of the values
* of all of the provided Loadables. This is comparable to Promise.all() for Loadables.
* Similar to Promise.all(), inputs may be Loadables, Promises, or literal values.
*/
function all<Inputs extends any[] | [Loadable<any>]>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;
function all<Inputs extends {[key: string]: any}>(inputs: Inputs): Loadable<UnwrapLoadables<Inputs>>;
/**
* Returns true if the provided parameter is a Loadable type.
*/
function isLoadable(x: any): x is Loadable<any>;
}
/* eslint-enable @typescript-eslint/no-unused-vars */
/* eslint-enable @typescript-eslint/no-explicit-any */
/**
* Factory to produce a Recoil snapshot object with all atoms in the default state.
*/
export function snapshot_UNSTABLE(initializeState?: (mutableSnapshot: MutableSnapshot) => void): Snapshot;

Some files were not shown because too many files have changed in this diff Show More