Skip to content
This repository has been archived by the owner on Jan 1, 2025. It is now read-only.

Commit

Permalink
Make default optional for atoms (#1639)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #1639

Make `default` an optional property for `atom()` and `atomFamily()`.  If not provided, the atom will initialize in a pending state until set.

This can be convenient to avoid having to make an atom with some nullable type or trying to come up with a default placeholder or using something cryptic like `default: new Promise(() => {}),`

Reviewed By: habond

Differential Revision: D34488397

fbshipit-source-id: 4615b258146d2d3bed902008179868b179028ad3
  • Loading branch information
drarmstr authored and facebook-github-bot committed Feb 26, 2022
1 parent 5ec24c8 commit 2d1a5c2
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 74 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## UPCOMING
**_Add new changes here as they land_**

- The `default` value is now optional for `atom()` and `atomFamily()`. If not provided the atom will initialize to a pending state. (#1639)
- `shouldNotBeFrozen` now works in JS environment without `Window` interface. (#1571)
- Avoid spurious console errors from effects when calling `setSelf()` from `onSet()` handlers. (#1589)
- Better error reporting when selectors provide inconsistent results (#1696)
Expand Down
21 changes: 15 additions & 6 deletions packages/recoil/recoil_values/Recoil_atom.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,8 @@ export type AtomEffect<T> = ({
getInfo_UNSTABLE: <S>(RecoilValue<S>) => RecoilValueInfo<S>,
}) => void | (() => void);

export type AtomOptions<T> = $ReadOnly<{
export type AtomOptionsWithoutDefault<T> = $ReadOnly<{
key: NodeKey,
default: RecoilValue<T> | Promise<T> | T,
effects?: $ReadOnlyArray<AtomEffect<T>>,
effects_UNSTABLE?: $ReadOnlyArray<AtomEffect<T>>,
persistence_UNSTABLE?: PersistenceSettings<T>,
Expand All @@ -151,6 +150,15 @@ export type AtomOptions<T> = $ReadOnly<{
retainedBy_UNSTABLE?: RetainedBy,
}>;

type AtomOptionsWithDefault<T> = $ReadOnly<{
...AtomOptionsWithoutDefault<T>,
default: RecoilValue<T> | Promise<T> | T,
}>;

export type AtomOptions<T> =
| AtomOptionsWithDefault<T>
| AtomOptionsWithoutDefault<T>;

type BaseAtomOptions<T> = $ReadOnly<{
...AtomOptions<T>,
default: T | Promise<T>,
Expand Down Expand Up @@ -557,16 +565,17 @@ function atom<T>(options: AtomOptions<T>): RecoilState<T> {
'A key option with a unique string value must be provided when creating an atom.',
);
}
if (!('default' in options)) {
throw err('A default value must be specified when creating an atom.');
}
}

const {
default: optionsDefault,
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS,
...restOptions
} = options;
const optionsDefault: RecoilValue<T> | Promise<T> | T = 'default' in options
? // $FlowIssue[prop-missing] No way to refine in Flow that property is not defined
options.default
: new Promise(() => {});

if (isRecoilValue(optionsDefault)
// Continue to use atomWithFallback for promise defaults for scoped atoms
// for now, since scoped atoms don't support async defaults
Expand Down
40 changes: 28 additions & 12 deletions packages/recoil/recoil_values/Recoil_atomFamily.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
*/
'use strict';

// @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom';
import type {CachePolicyWithoutEviction} from '../caches/Recoil_CachePolicy';
import type {RecoilState, RecoilValue} from '../core/Recoil_RecoilValue';
import type {RetainedBy} from '../core/Recoil_RetainedBy';
import type {AtomEffect, AtomOptions} from './Recoil_atom';
import type {AtomEffect, AtomOptionsWithoutDefault} from './Recoil_atom';
// @fb-only: import type {ScopeRules} from 'Recoil_ScopedAtom';

const cacheFromPolicy = require('../caches/Recoil_cacheFromPolicy');
const {setConfigDeletionHandler} = require('../core/Recoil_Node');
Expand All @@ -38,13 +38,8 @@ export type ParameterizedScopeRules<P> = $ReadOnlyArray<
>;
// flowlint unclear-type:error

export type AtomFamilyOptions<T, P: Parameter> = $ReadOnly<{
...AtomOptions<T>,
default:
| RecoilValue<T>
| Promise<T>
| T
| (P => T | RecoilValue<T> | Promise<T>),
export type AtomFamilyOptionsWithoutDefault<T, P: Parameter> = $ReadOnly<{
...AtomOptionsWithoutDefault<T>,
effects?:
| $ReadOnlyArray<AtomEffect<T>>
| (P => $ReadOnlyArray<AtomEffect<T>>),
Expand All @@ -57,6 +52,17 @@ export type AtomFamilyOptions<T, P: Parameter> = $ReadOnly<{
// @fb-only: scopeRules_APPEND_ONLY_READ_THE_DOCS?: ParameterizedScopeRules<P>,
}>;

export type AtomFamilyOptions<T, P: Parameter> =
| $ReadOnly<{
...AtomFamilyOptionsWithoutDefault<T, P>,
default:
| RecoilValue<T>
| Promise<T>
| T
| (P => T | RecoilValue<T> | Promise<T>),
}>
| AtomFamilyOptionsWithoutDefault<T, P>;

// Process scopeRules to handle any entries which are functions taking parameters
// prettier-ignore
// @fb-only: function mapScopeRules<P>(
Expand Down Expand Up @@ -102,17 +108,27 @@ function atomFamily<T, P: Parameter>(
}

const {cachePolicyForParams_UNSTABLE, ...atomOptions} = options;
const optionsDefault:
| RecoilValue<T>
| Promise<T>
| T
| (P => T | RecoilValue<T> | Promise<T>) =
'default' in options
? // $FlowIssue[prop-missing] No way to refine in Flow that property is not defined
options.default
: new Promise(() => {});

const newAtom = atom<T>({
...atomOptions,
key: `${options.key}__${stableStringify(params) ?? 'void'}`,
default:
typeof options.default === 'function'
typeof optionsDefault === 'function'
? // The default was parameterized
// Flow doesn't know that T isn't a function, so we need to case to any
(options.default: any)(params) // flowlint-line unclear-type:off
// $FlowIssue[incompatible-use]
optionsDefault(params)
: // Default may be a static value, promise, or RecoilValue
options.default,
optionsDefault,

retainedBy_UNSTABLE:
typeof options.retainedBy_UNSTABLE === 'function'
Expand Down
30 changes: 18 additions & 12 deletions packages/recoil/recoil_values/__tests__/Recoil_atom-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,24 @@ function reset(recoilValue) {
setRecoilValue(store, recoilValue, DEFAULT_VALUE);
}

testRecoil('Key is required when creating atoms', () => {
const devStatus = window.__DEV__;
window.__DEV__ = true;

// $FlowExpectedError[incompatible-call]
expect(() => atom({default: undefined})).toThrow();

window.__DEV__ = devStatus;
});

testRecoil('default is optional', () => {
const myAtom = atom({key: 'atom without default'});
expect(getRecoilStateLoadable(myAtom).state).toBe('loading');

act(() => set(myAtom, 'VALUE'));
expect(getValue(myAtom)).toBe('VALUE');
});

testRecoil('atom can read and write value', () => {
const myAtom = atom<string>({
key: 'atom with default',
Expand Down Expand Up @@ -1484,15 +1502,3 @@ testRecoil('object is frozen when stored in atom', async () => {

window.__DEV__ = devStatus;
});

testRecoil('Required options are provided when creating atoms', () => {
const devStatus = window.__DEV__;
window.__DEV__ = true;

// $FlowExpectedError[prop-missing]
expect(() => atom({default: undefined})).toThrow();
// $FlowExpectedError[prop-missing]
expect(() => atom({key: 'MISSING DEFAULT'})).toThrow();

window.__DEV__ = devStatus;
});
56 changes: 35 additions & 21 deletions packages/recoil/recoil_values/__tests__/Recoil_atomFamily-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ function get(recoilValue) {
return getRecoilValueAsLoadable(store, recoilValue).contents;
}

function getLoadable(recoilValue) {
return getRecoilValueAsLoadable(store, recoilValue);
}

function set(recoilValue, value) {
setRecoilValue(store, recoilValue, value);
}
Expand All @@ -113,30 +117,40 @@ testRecoil('Works with non-overlapping sets', () => {
expect(get(pAtom({y: 'y'}))).toBe('yValue');
});

testRecoil('Works with atom default', () => {
const fallbackAtom = atom({key: 'fallback', default: 0});
const hasFallback = atomFamily({
key: 'hasFallback',
default: fallbackAtom,
describe('Default', () => {
testRecoil('default is optional', () => {
const myAtom = atom({key: 'atom without default'});
expect(getLoadable(myAtom).state).toBe('loading');

act(() => set(myAtom, 'VALUE'));
expect(get(myAtom)).toBe('VALUE');
});
expect(get(hasFallback({k: 'x'}))).toBe(0);
set(fallbackAtom, 1);
expect(get(hasFallback({k: 'x'}))).toBe(1);
set(hasFallback({k: 'x'}), 2);
expect(get(hasFallback({k: 'x'}))).toBe(2);
expect(get(hasFallback({k: 'y'}))).toBe(1);
});

testRecoil('Works with parameterized default', () => {
const paramDefaultAtom = atomFamily({
key: 'parameterized default',
default: ({num}) => num,
testRecoil('Works with atom default', () => {
const fallbackAtom = atom({key: 'fallback', default: 0});
const hasFallback = atomFamily({
key: 'hasFallback',
default: fallbackAtom,
});
expect(get(hasFallback({k: 'x'}))).toBe(0);
set(fallbackAtom, 1);
expect(get(hasFallback({k: 'x'}))).toBe(1);
set(hasFallback({k: 'x'}), 2);
expect(get(hasFallback({k: 'x'}))).toBe(2);
expect(get(hasFallback({k: 'y'}))).toBe(1);
});

testRecoil('Works with parameterized default', () => {
const paramDefaultAtom = atomFamily({
key: 'parameterized default',
default: ({num}) => num,
});
expect(get(paramDefaultAtom({num: 1}))).toBe(1);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(get(paramDefaultAtom({num: 1}))).toBe(3);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
});
expect(get(paramDefaultAtom({num: 1}))).toBe(1);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
set(paramDefaultAtom({num: 1}), 3);
expect(get(paramDefaultAtom({num: 1}))).toBe(3);
expect(get(paramDefaultAtom({num: 2}))).toBe(2);
});

testRecoil('Works with date as parameter', () => {
Expand Down
28 changes: 14 additions & 14 deletions packages/recoil/recoil_values/__tests__/Recoil_selector-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ function resetValue(recoilState) {
store.getState().currentTree.version++;
}

testRecoil('Required options are provided when creating selectors', () => {
const devStatus = window.__DEV__;
window.__DEV__ = true;

// $FlowExpectedError[incompatible-call]
expect(() => selector({get: () => {}})).toThrow();
// $FlowExpectedError[incompatible-call]
expect(() => selector({get: false})).toThrow();
// $FlowExpectedError[incompatible-call]
expect(() => selector({key: 'MISSING GET'})).toThrow();

window.__DEV__ = devStatus;
});

testRecoil('selector get', () => {
const staticSel = constSelector('HELLO');

Expand Down Expand Up @@ -1044,17 +1058,3 @@ testRecoil('Selector values are frozen', async () => {

window.__DEV__ = devStatus;
});

testRecoil('Required options are provided when creating selectors', () => {
const devStatus = window.__DEV__;
window.__DEV__ = true;

// $FlowExpectedError[incompatible-call]
expect(() => selector({get: () => {}})).toThrow();
// $FlowExpectedError[incompatible-call]
expect(() => selector({get: false})).toThrow();
// $FlowExpectedError[incompatible-call]
expect(() => selector({key: 'MISSING GET'})).toThrow();

window.__DEV__ = devStatus;
});
25 changes: 17 additions & 8 deletions typescript/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,16 @@
}) => void | (() => void);

// atom.d.ts
export interface AtomOptions<T> {
key: NodeKey;
default: RecoilValue<T> | Promise<T> | T;
effects?: ReadonlyArray<AtomEffect<T>>;
effects_UNSTABLE?: ReadonlyArray<AtomEffect<T>>;
dangerouslyAllowMutability?: boolean;
interface AtomOptionsWithoutDefault<T> {
key: NodeKey;
effects?: ReadonlyArray<AtomEffect<T>>;
effects_UNSTABLE?: ReadonlyArray<AtomEffect<T>>;
dangerouslyAllowMutability?: boolean;
}
interface AtomOptionsWithDefault<T> extends AtomOptionsWithoutDefault<T> {
default: RecoilValue<T> | Promise<T> | T;
}
export type AtomOptions<T> = AtomOptionsWithoutDefault<T> | AtomOptionsWithDefault<T>;

/**
* Creates an atom, which represents a piece of writeable state
Expand Down Expand Up @@ -383,14 +386,20 @@
| ReadonlyArray<SerializableParam>
| Readonly<{[key: string]: SerializableParam}>;

export interface AtomFamilyOptions<T, P extends SerializableParam> {
interface AtomFamilyOptionsWithoutDefault<T, P extends SerializableParam> {
key: NodeKey;
dangerouslyAllowMutability?: boolean;
default: RecoilValue<T> | Promise<T> | T | ((param: P) => T | RecoilValue<T> | Promise<T>);
effects?: | ReadonlyArray<AtomEffect<T>> | ((param: P) => ReadonlyArray<AtomEffect<T>>);
effects_UNSTABLE?: | ReadonlyArray<AtomEffect<T>> | ((param: P) => ReadonlyArray<AtomEffect<T>>);
// cachePolicyForParams_UNSTABLE?: CachePolicyWithoutEviction; TODO: removing while we discuss long term API
}
interface AtomFamilyOptionsWithDefault<T, P extends SerializableParam>
extends AtomFamilyOptionsWithoutDefault<T, P> {
default: RecoilValue<T> | Promise<T> | T | ((param: P) => T | RecoilValue<T> | Promise<T>);
}
export type AtomFamilyOptions<T, P extends SerializableParam> =
| AtomFamilyOptionsWithDefault<T, P>
| AtomFamilyOptionsWithoutDefault<T, P>;

/**
* Returns a function which returns a memoized atom for each unique parameter value.
Expand Down
11 changes: 10 additions & 1 deletion typescript/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,16 @@
new DefaultValue();

// atom
const myAtom = atom({
const myAtom: RecoilState<number> = atom({
key: 'MyAtom',
default: 5,
});

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const myAtomWithoutDefault: RecoilState<number> = atom<number>({
key: 'MyAtomWithoutDefault',
});

// selector
const mySelector1 = selector({
key: 'MySelector1',
Expand Down Expand Up @@ -394,6 +399,10 @@ isRecoilValue(mySelector1);
useRecoilValue(atm); // $ExpectType number

myAtomFam(''); // $ExpectError

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const myAtomFamilyWithoutDefault: (number: number) => RecoilState<number> =
atomFamily<number, number>({key: 'MyAtomFamilyWithoutDefault'});
}

/**
Expand Down

0 comments on commit 2d1a5c2

Please sign in to comment.