recoil
Version:
Recoil - A state management library for React
269 lines (243 loc) • 8.71 kB
Flow
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+recoil
* @flow strict-local
* @format
*/
;
import type {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 => {
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 => {
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();
const releaseRef = useRef();
useTransactionSubscription(
useCallback(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 release;
}, [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();
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,
};