diff --git a/CHANGELOG.md b/CHANGELOG.md index 334820c1fb..848a9c07f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Ability to map Loadables with other Loadables - Allow class instances in family parameters for Flow +- Add `getLoadable()` and `getPromise()` to Atom Effects interface for reading other atoms. ### Pending - Memory management diff --git a/src/recoil_values/Recoil_atom.js b/src/recoil_values/Recoil_atom.js index 6fd753fb00..d490c963fe 100644 --- a/src/recoil_values/Recoil_atom.js +++ b/src/recoil_values/Recoil_atom.js @@ -88,6 +88,7 @@ const { } = require('../core/Recoil_Node'); const {isRecoilValue} = require('../core/Recoil_RecoilValue'); const { + getRecoilValueAsLoadable, markRecoilValueModified, setRecoilValue, setRecoilValueLoadable, @@ -129,6 +130,10 @@ export type AtomEffect = ({ // Subscribe callbacks to events. // Atom effect observers are called before global transaction observers onSet: ((newValue: T, oldValue: T | DefaultValue) => void) => void, + + // Accessors to read other atoms/selectors + getPromise: (RecoilValue) => Promise, + getLoadable: (RecoilValue) => Loadable, }) => void | (() => void); export type AtomOptions = $ReadOnly<{ @@ -241,6 +246,39 @@ function baseAtom(options: BaseAtomOptions): RecoilState { if (options.effects_UNSTABLE != null && !alreadyKnown) { let duringInit = true; + function getLoadable(recoilValue: RecoilValue): Loadable { + // 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) { + // Cast T to S + const retValue: NewValue = (initValue: any); // flowlint-line unclear-type:off + return retValue instanceof DefaultValue + ? (defaultLoadable: any) // flowlint-line unclear-type:off + : isPromise(retValue) + ? loadableWithPromise( + retValue.then( + (v: S | DefaultValue): ResolvedLoadablePromiseInfo => ({ + __key: key, + __value: + v instanceof DefaultValue + ? // TODO It's a little weird that this returns a Promise + // instead of T, but it seems to work. This can be cleaned + // up if we clean up how Loadable's wrap keys and values. + (defaultLoadable: any).toPromise() // flowlint-line unclear-type:off + : v, + }), + ), + ) + : loadableWithValue(retValue); + } + return getRecoilValueAsLoadable(store, recoilValue); + } + + function getPromise(recoilValue: RecoilValue): Promise { + return getLoadable(recoilValue).toPromise(); + } + const setSelf = (effect: AtomEffect) => ( valueOrUpdater: NewValueOrUpdater, ) => { @@ -339,6 +377,8 @@ function baseAtom(options: BaseAtomOptions): RecoilState { setSelf: setSelf(effect), resetSelf: resetSelf(effect), onSet: onSet(effect), + getPromise, + getLoadable, }); if (cleanup != null) { cleanupEffectsByStore.set(store, [ diff --git a/src/recoil_values/__tests__/Recoil_atom-test.js b/src/recoil_values/__tests__/Recoil_atom-test.js index a7f23294be..022c01ef64 100644 --- a/src/recoil_values/__tests__/Recoil_atom-test.js +++ b/src/recoil_values/__tests__/Recoil_atom-test.js @@ -71,6 +71,10 @@ function get(recoilValue) { return getRecoilValueAsLoadable(store, recoilValue).contents; } +function getLoadable(recoilValue) { + return getRecoilValueAsLoadable(store, recoilValue); +} + function set(recoilValue, value: mixed) { setRecoilValue(store, recoilValue, value); } @@ -1008,6 +1012,135 @@ describe('Effects', () => { expect(numTimesEffectInit).toBe(2); }, ); + + describe('Other Atoms', () => { + test('init from other atom', () => { + const myAtom = atom({ + key: 'atom effect - init from other atom', + default: 'DEFAULT', + effects_UNSTABLE: [ + ({setSelf, getLoadable}) => { + const otherValue = getLoadable(otherAtom).contents; + expect(otherValue).toEqual('OTHER'); + setSelf(otherValue); + }, + ], + }); + + const otherAtom = atom({ + key: 'atom effect - other atom', + default: 'OTHER', + }); + + expect(get(myAtom)).toEqual('OTHER'); + }); + + test('init from other atom async', async () => { + const myAtom = atom({ + key: 'atom effect - init from other atom async', + default: 'DEFAULT', + effects_UNSTABLE: [ + ({setSelf, getPromise}) => { + const otherValue = getPromise(otherAtom); + setSelf(otherValue); + }, + ], + }); + + const otherAtom = atom({ + key: 'atom effect - other atom async', + default: Promise.resolve('OTHER'), + }); + + await expect(getLoadable(myAtom).promiseOrThrow()).resolves.toEqual( + 'OTHER', + ); + }); + + test('async get other atoms', async () => { + let initTest1 = new Promise(() => {}); + let initTest2 = new Promise(() => {}); + let initTest3 = new Promise(() => {}); + let setTest = new Promise(() => {}); + + const myAtom = atom({ + key: 'atom effect - async get', + default: 'DEFAULT', + effects_UNSTABLE: [ + // Test we can get default values + ({node, getLoadable, getPromise}) => { + expect(getLoadable(node).contents).toEqual('DEFAULT'); + // eslint-disable-next-line jest/valid-expect + initTest1 = expect(getPromise(asyncAtom)).resolves.toEqual('ASYNC'); + }, + ({setSelf}) => { + setSelf('INIT'); + }, + // Test we can get value from previous initialization + ({node, getLoadable}) => { + expect(getLoadable(node).contents).toEqual('INIT'); + }, + // Test we can asynchronouse get "current" values of both self and other atoms + // This will be executed when myAtom is set, but checks both atoms. + ({onSet, getLoadable, getPromise}) => { + onSet(x => { + expect(x).toEqual('SET_ATOM'); + expect(getLoadable(myAtom).contents).toEqual(x); + // eslint-disable-next-line jest/valid-expect + setTest = expect(getPromise(asyncAtom)).resolves.toEqual( + 'SET_OTHER', + ); + }); + }, + ], + }); + + const asyncAtom = atom({ + key: 'atom effect - other atom async get', + default: Promise.resolve('ASYNC_DEFAULT'), + effects_UNSTABLE: [ + ({setSelf}) => void setSelf(Promise.resolve('ASYNC')), + ({getLoadable, getPromise}) => { + expect(getLoadable(myAtom).contents).toEqual('DEFAULT'); + // eslint-disable-next-line jest/valid-expect + initTest2 = expect(getPromise(asyncAtom)).resolves.toEqual('ASYNC'); + }, + + // Test that we can read default for an aborted initialization + ({setSelf}) => void setSelf(Promise.resolve(new DefaultValue())), + ({getPromise}) => { + // eslint-disable-next-line jest/valid-expect + initTest3 = expect(getPromise(asyncAtom)).resolves.toEqual( + 'ASYNC_DEFAULT', + ); + }, + + ({setSelf}) => void setSelf(Promise.resolve('ASYNC')), + ], + }); + + const [MyAtom, setMyAtom] = componentThatReadsAndWritesAtom(myAtom); + const [AsyncAtom, setAsyncAtom] = componentThatReadsAndWritesAtom( + asyncAtom, + ); + const c = renderElements( + <> + + + , + ); + + await flushPromisesAndTimers(); + expect(c.textContent).toBe('"INIT""ASYNC"'); + await initTest1; + await initTest2; + await initTest3; + + act(() => setAsyncAtom('SET_OTHER')); + act(() => setMyAtom('SET_ATOM')); + await setTest; + }); + }); }); testRecoil('object is frozen when stored in atom', () => {