Skip to content

Commit

Permalink
Enable atom effects to initialize or set atom to a Promise (facebooke…
Browse files Browse the repository at this point in the history
…xperimental#1681)

Summary:
Pull Request resolved: facebookexperimental#1681

Minor completeness for the API to allow atoms to be initialized to `Promise` types by wrapping them, similar to atom defaults.

To properly allow Promises and such as atom values we also need the ability to set atoms to Promises.  The point of this diff is to provide completeness and for future work to support async atoms

Differential Revision: D34975767

fbshipit-source-id: 784443175018133b37894e1daab4e53338a88685
  • Loading branch information
drarmstr authored and facebook-github-bot committed Mar 25, 2022
1 parent d922baa commit 7acb985
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 29 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## UPCOMING
**_Add new changes here as they land_**

- Atom effects can initialize or set atoms to wrapped values (#1681)

## 0.7 (2022-03-25)

### New Features
Expand Down
57 changes: 32 additions & 25 deletions packages/recoil/recoil_values/Recoil_atom.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,18 @@ export type PersistenceSettings<Stored> = $ReadOnly<{
validator: (mixed, DefaultValue) => Stored | DefaultValue,
}>;

// TODO Support Loadable<T> and WrappedValue<T>
type NewValue<T> = T | DefaultValue | Promise<T | DefaultValue>;
// TODO Support Loadable<T>
type NewValue<T> =
| T
| DefaultValue
| Promise<T | DefaultValue>
| WrappedValue<T>;
type NewValueOrUpdater<T> =
| T
| DefaultValue
| Promise<T | DefaultValue>
| ((T | DefaultValue) => T | DefaultValue);
| WrappedValue<T>
| ((T | DefaultValue) => T | DefaultValue | WrappedValue<T>);

// Effect is called the first time a node is used with a <RecoilRoot>
export type AtomEffect<T> = ({
Expand All @@ -127,7 +132,8 @@ export type AtomEffect<T> = ({
| T
| DefaultValue
| Promise<T | DefaultValue>
| ((T | DefaultValue) => T | DefaultValue),
| WrappedValue<T>
| ((T | DefaultValue) => T | DefaultValue | WrappedValue<T>),
) => void,
resetSelf: () => void,

Expand Down Expand Up @@ -167,6 +173,9 @@ type BaseAtomOptions<T> = $ReadOnly<{
default: Promise<T> | Loadable<T> | WrappedValue<T> | T,
}>;

const unwrap = <T, S = T>(x: T | S | WrappedValue<T>): T | S =>
x instanceof WrappedValue ? x.value : x;

function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
const {key, persistence_UNSTABLE: persistence} = options;

Expand All @@ -193,11 +202,7 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
? options.default.state === 'loading'
? unwrapPromise((options.default: LoadingLoadableType<T>).contents)
: options.default
: loadableWithValue(
options.default instanceof WrappedValue
? options.default.value
: options.default,
);
: loadableWithValue(unwrap(options.default));
maybeFreezeValueOrPromise(defaultLoadable.contents);

let cachedAnswerForUnvalidatedValue: void | Loadable<T> = undefined;
Expand Down Expand Up @@ -280,8 +285,8 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
const effects = options.effects ?? options.effects_UNSTABLE;
if (effects != null) {
// This state is scoped by Store, since this is in the initAtom() closure
let duringInit = true;
let initValue: NewValue<T> = DEFAULT_VALUE;
let isDuringInit = true;
let isInitError: boolean = false;
let pendingSetSelf: ?{
effect: AtomEffect<T>,
Expand All @@ -292,7 +297,7 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
// Normally we can just get the current value of another atom.
// But for our own value we need to check if there is a pending
// initialized value or get the fallback default value.
if (duringInit && recoilValue.key === key) {
if (isDuringInit && recoilValue.key === key) {
// Cast T to S
const retValue: NewValue<S> = (initValue: any); // flowlint-line unclear-type:off
return retValue instanceof DefaultValue
Expand Down Expand Up @@ -323,7 +328,7 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
store.getState().nextTree ?? store.getState().currentTree,
recoilValue.key,
);
return duringInit &&
return isDuringInit &&
recoilValue.key === key &&
!(initValue instanceof DefaultValue)
? {...info, isSet: true, loadable: getLoadable(recoilValue)}
Expand All @@ -332,7 +337,7 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {

const setSelf =
(effect: AtomEffect<T>) => (valueOrUpdater: NewValueOrUpdater<T>) => {
if (duringInit) {
if (isDuringInit) {
const currentLoadable = getLoadable(node);
const currentValue: T | DefaultValue =
currentLoadable.state === 'hasValue'
Expand All @@ -356,21 +361,25 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
}

if (typeof valueOrUpdater !== 'function') {
pendingSetSelf = {effect, value: valueOrUpdater};
pendingSetSelf = {
effect,
value: unwrap<T, DefaultValue>(valueOrUpdater),
};
}

setRecoilValue(
store,
node,
typeof valueOrUpdater === 'function'
? currentValue => {
const newValue =
const newValue = unwrap(
// cast to any because we can't restrict T from being a function without losing support for opaque types
(valueOrUpdater: any)(currentValue); // flowlint-line unclear-type:off
(valueOrUpdater: any)(currentValue), // flowlint-line unclear-type:off
);
pendingSetSelf = {effect, value: newValue};
return newValue;
}
: valueOrUpdater,
: unwrap(valueOrUpdater),
);
}
};
Expand Down Expand Up @@ -447,17 +456,17 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
}
}

duringInit = false;
isDuringInit = false;

// Mutate initial state in place since we know there are no other subscribers
// since we are the ones initializing on first use.
if (!(initValue instanceof DefaultValue)) {
const frozenInitValue = maybeFreezeValueOrPromise(initValue);
const initLoadable = isInitError
? loadableWithError(initValue)
: isPromise(frozenInitValue)
? loadableWithPromise(wrapPendingPromise(store, frozenInitValue))
: loadableWithValue(frozenInitValue);
: isPromise(initValue)
? loadableWithPromise(wrapPendingPromise(store, initValue))
: loadableWithValue(unwrap(initValue));
maybeFreezeValueOrPromise(initLoadable.contents);
initState.atomValues.set(key, initLoadable);

// If there is a pending transaction, then also mutate the next state tree.
Expand Down Expand Up @@ -610,9 +619,7 @@ function atom<T>(options: AtomOptions<T>): RecoilState<T> {
// @fb-only: ) {
// @fb-only: return scopedAtom<T>({
// @fb-only: ...restOptions,
// @fb-only: default: optionsDefault instanceof WrappedValue
// @fb-only: ? optionsDefault.value
// @fb-only: : optionsDefault,
// @fb-only: default: unwrap<T>(optionsDefault),
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
// @fb-only: });
} else {
Expand Down
30 changes: 29 additions & 1 deletion packages/recoil/recoil_values/__tests__/Recoil_atom-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ describe('Effects', () => {
testRecoil('initialization', () => {
let inited = false;
const myAtom = atom({
key: 'atom effect',
key: 'atom effect init',
default: 'DEFAULT',
effects: [
({node, trigger, setSelf}) => {
Expand Down Expand Up @@ -421,6 +421,34 @@ describe('Effects', () => {
expect(c.textContent).toEqual('"RESOLVE"');
});

testRecoil('set to Promise', async () => {
let setLater;
const myAtom = atom({
key: 'atom effect set promise',
default: 'DEFAULT',
effects: [
({setSelf}) => {
setSelf(atom.value(Promise.resolve('INIT_PROMISE')));
setLater = setSelf;
},
],
});
expect(getRecoilStateLoadable(myAtom).state).toBe('hasValue');
await expect(getRecoilStateLoadable(myAtom).contents).resolves.toBe(
'INIT_PROMISE',
);
act(() => setLater(atom.value(Promise.resolve('LATER_PROMISE'))));
expect(getRecoilStateLoadable(myAtom).state).toBe('hasValue');
await expect(getRecoilStateLoadable(myAtom).contents).resolves.toBe(
'LATER_PROMISE',
);
act(() => setLater(() => atom.value(Promise.resolve('UPDATER_PROMISE'))));
expect(getRecoilStateLoadable(myAtom).state).toBe('hasValue');
await expect(getRecoilStateLoadable(myAtom).contents).resolves.toBe(
'UPDATER_PROMISE',
);
});

testRecoil('order of effects', () => {
const myAtom = atom({
key: 'atom effect order',
Expand Down
3 changes: 2 additions & 1 deletion typescript/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@
| T
| DefaultValue
| Promise<T | DefaultValue>
| ((param: T | DefaultValue) => T | DefaultValue),
| WrappedValue<T>
| ((param: T | DefaultValue) => T | DefaultValue | WrappedValue<T>),
) => void,
resetSelf: () => void,

Expand Down
4 changes: 2 additions & 2 deletions typescript/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ useRecoilCallback(({ snapshot, set, reset, refresh, gotoSnapshot, transact_UNSTA
gotoSnapshot(3); // $ExpectError
gotoSnapshot(myAtom); // $ExpectError

loadable.state; // $ExpectType "hasValue" | "loading" | "hasError"
loadable.state; // $ExpectType "loading" | "hasValue" | "hasError"
loadable.contents; // $ExpectType any

set(myAtom, 5);
Expand Down Expand Up @@ -341,7 +341,7 @@ const transact: (p: number) => void = useRecoilTransaction_UNSTABLE(({get, set,

for (const node of Array.from(snapshot.getNodes_UNSTABLE({isModified: true}))) {
const loadable = snapshot.getLoadable(node); // $ExpectType Loadable<unknown>
loadable.state; // $ExpectType "hasValue" | "loading" | "hasError"
loadable.state; // $ExpectType "loading" | "hasValue" | "hasError"
}
},
);
Expand Down

0 comments on commit 7acb985

Please sign in to comment.