Skip to content

Commit

Permalink
Provide new value to onSet() handler when triggered by async default
Browse files Browse the repository at this point in the history
Summary:
When an atom effect's `onSet()` handler is triggered due to having a default which is an async `Promise` that resolves the handler should be able to get the proper new value instead of a `DefaultValue` placeholder.

This addresses customer-reported issue facebookexperimental#1050

Differential Revision: D28850148

fbshipit-source-id: 0f19d2179e7f3d522887550af32b87247f979f26
  • Loading branch information
drarmstr authored and facebook-github-bot committed Jun 2, 2021
1 parent a4461f9 commit 57a008c
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 15 deletions.
16 changes: 6 additions & 10 deletions src/recoil_values/Recoil_atom.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,7 @@ export type AtomEffect<T> = ({

// Subscribe callbacks to events.
// Atom effect observers are called before global transaction observers
onSet: (
(newValue: T | DefaultValue, oldValue: T | DefaultValue) => void,
) => void,
onSet: ((newValue: T, oldValue: T | DefaultValue) => void) => void,
}) => void | (() => void);

export type AtomOptions<T> = $ReadOnly<{
Expand Down Expand Up @@ -293,9 +291,7 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
};
const resetSelf = effect => () => setSelf(effect)(DEFAULT_VALUE);

const onSet = effect => (
handler: (T | DefaultValue, T | DefaultValue) => void,
) => {
const onSet = effect => (handler: (T, T | DefaultValue) => void) => {
store.subscribeToTransactions(currentStore => {
// eslint-disable-next-line prefer-const
let {currentTree, previousTree} = currentStore.getState();
Expand All @@ -306,10 +302,10 @@ function baseAtom<T>(options: BaseAtomOptions<T>): RecoilState<T> {
);
previousTree = currentTree; // attempt to trundle on
}
const newLoadable = currentTree.atomValues.get(key);
if (newLoadable == null || newLoadable.state === 'hasValue') {
const newValue: T | DefaultValue =
newLoadable != null ? newLoadable.contents : DEFAULT_VALUE;
const newLoadable =
currentTree.atomValues.get(key) ?? defaultLoadable;
if (newLoadable.state === 'hasValue') {
const newValue: T = newLoadable.contents;
const oldLoadable =
previousTree.atomValues.get(key) ?? defaultLoadable;
const oldValue: T | DefaultValue =
Expand Down
45 changes: 43 additions & 2 deletions src/recoil_values/__tests__/Recoil_atom-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('Effects', () => {
setSelf('INIT');
// This only fires on the reset action, not the default promise resolving
onSet(newValue => {
expect(newValue).toBeInstanceOf(DefaultValue);
expect(newValue).toBe('RESOLVE');
});
},
],
Expand Down Expand Up @@ -443,6 +443,47 @@ describe('Effects', () => {
expect(onSetForSameEffect).toHaveBeenCalledTimes(0);
});

testRecoil('set default promise', async () => {
let setValue = 'RESOLVE_DEFAULT';
const onSetHandler = jest.fn(newValue => {
expect(newValue).toBe(setValue);
});

let resolveDefault;
const myAtom = atom({
key: 'atom effect default promise',
default: new Promise(resolve => {
resolveDefault = resolve;
}),
effects_UNSTABLE: [
({onSet}) => {
onSet(onSetHandler);
},
],
});

const [ReadsWritesAtom, set, reset] = componentThatReadsAndWritesAtom(
myAtom,
);
const c = renderElements(<ReadsWritesAtom />);
expect(c.textContent).toEqual('loading');

act(() => resolveDefault?.('RESOLVE_DEFAULT'));
await flushPromisesAndTimers();
expect(c.textContent).toEqual('"RESOLVE_DEFAULT"');
expect(onSetHandler).toHaveBeenCalledTimes(1);

setValue = 'SET';
act(() => set('SET'));
expect(c.textContent).toEqual('"SET"');
expect(onSetHandler).toHaveBeenCalledTimes(2);

setValue = 'RESOLVE_DEFAULT';
act(reset);
expect(c.textContent).toEqual('"RESOLVE_DEFAULT"');
expect(onSetHandler).toHaveBeenCalledTimes(3);
});

testRecoil(
'when setSelf is called in onSet, then onSet is not triggered again',
() => {
Expand Down Expand Up @@ -556,7 +597,7 @@ describe('Effects', () => {
}),
);
onSet(value => {
expect(value).toBeInstanceOf(DefaultValue);
expect(value).toBe('DEFAULT');
validated = true;
});
},
Expand Down
2 changes: 1 addition & 1 deletion typescript/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export type AtomEffect<T> = (param: {
// Subscribe callbacks to events.
// Atom effect observers are called before global transaction observers
onSet: (
param: (newValue: T | DefaultValue, oldValue: T | DefaultValue) => void,
param: (newValue: T, oldValue: T | DefaultValue) => void,
) => void,
}) => void | (() => void);

Expand Down
4 changes: 2 additions & 2 deletions typescript/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ isRecoilValue(mySelector1);
setSelf('a'); // $ExpectError

onSet(val => {
val; // $ExpectType number | DefaultValue
val; // $ExpectType number
});
onSet('a'); // $ExpectError

Expand All @@ -496,7 +496,7 @@ isRecoilValue(mySelector1);
setSelf('a'); // $ExpectError

onSet(val => {
val; // $ExpectType number | DefaultValue
val; // $ExpectType number
});
onSet('a'); // $ExpectError

Expand Down

0 comments on commit 57a008c

Please sign in to comment.