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

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');
},
);