@uniflowed/state
opaque-type
ReadonlyAtom
export opaque type ReadonlyAtom<T> = AtomRecord<T, empty>;
Any atom, as something to read.
Every other atom type is a subtype of this one, so a function that only reads takes a ReadonlyAtom<T> and accepts all of them.
opaque-type
WritableAtom
export opaque type WritableAtom<T, A>: ReadonlyAtom<T> = AtomRecord<T, A>;
An atom that can be written with an argument of type A.
A is not always the value: an atom holding a list may take an "add this one" argument, and keeping the two apart is what lets the setter a component is handed be typed exactly.
opaque-type
Atom
export opaque type Atom<T>: WritableAtom<T, SetAction<T>> = AtomRecord<T, SetAction<T>>;
A piece of state: readable, and writable the way useState is.
The useState-shaped argument — a value, or a function of the current one — is the reason this is a type of its own rather than WritableAtom<T, T>: setCount((n) => n + 1) has to mean what it does everywhere else in React.
opaque-type
WriteOnlyAtom
export opaque type WriteOnlyAtom<A>: WritableAtom<null, A> = AtomRecord<null, A>;
An atom that is only ever written: an action.
opaque-type
AsyncAtom
export opaque type AsyncAtom<T>: ReadonlyAtom<Loadable<T>> = AtomRecord<Loadable<T>, empty>;
An atom whose value arrives from a promise: what [asyncAtom] returns.
Nominally distinct from ReadonlyAtom<Loadable<T>> even though the two are the same record, and the distinction earns its place at exactly one call site: [refresh] can only mean something for an atom that has a load to run, and a selector that happens to return a Loadable — a cache lookup projected into one, say — has nothing to refresh. Without the name that mistake is a runtime error; with it, Flow says so at the call site.
type
Getter
export type Getter = <V>(target: ReadonlyAtom<V>) => V;
Reading another atom, inside a read or a write.
type
Setter
export type Setter = <V, A>(target: WritableAtom<V, A>, argument: A) => void;
Writing another atom, inside a write.
type
AtomSetter
export type AtomSetter<T> = (next: SetAction<T>) => void;
What useSetAtom hands back for a useState-shaped atom.
type
AtomTuple
export type AtomTuple<T> = [T, AtomSetter<T>];
What useAtom hands back: the shape useState returns.
opaque-type
Store
export opaque type Store = StoreInstance;
Where atom values live.
Opaque, and deliberately small: get, set and sub are everything an application needs, and everything a test needs to assert on a tree's state without rendering one.
function
atom
export function atom<T>(initial: T, options?: PrimitiveOptions<T>): Atom<T> { ... }
A piece of state.
Declaring one allocates nothing and belongs to no store — the value appears the first time a store is asked for it. That is what makes it safe to declare atoms at module scope in a file a server imports.
onMount runs when the first subscriber in a store arrives and its return value runs when the last one leaves, which is where a subscription to anything outside the graph belongs: a socket, an interval, a media query.
function
selector
export function selector<T>(read: (get: Getter) => T, options?: AtomOptions<T>): ReadonlyAtom<T> { ... }
State derived from other atoms, recomputed only when what it read changes.
No dependency array: the read discovers its own dependencies by running, and they are rebuilt every time it runs. A read that branches — get(showAll) ? get(all) : get(some) — depends on the branch it took, so writing to the other one recomputes nothing.
A read that returns an unchanged value does not re-render its readers, and options.equals is how a read that builds a fresh array each time says what "unchanged" means for it.
function
writableSelector
export function writableSelector<T, A>(
read: (get: Getter) => T,
write: (get: Getter, set: Setter, argument: A) => void,
options?: AtomOptions<T>,
): WritableAtom<T, A> { ... }
A selector you can also write to.
The write is given get and set, so it can decide what a change to this atom means in terms of the atoms it is derived from — a "full name" atom whose write splits into first and last, a filter atom that also resets the page number. Everything it sets happens in one batch, so subscribers to three of those atoms are woken once each rather than once per set.
get inside a write does not create a dependency. A write is not a computation, and an atom that recomputed because a handler looked at something would be very hard to explain.
function
action
export function action<A>(
write: (get: Getter, set: Setter, argument: A) => void,
options?: AtomOptions<null>,
): WriteOnlyAtom<A> { ... }
An atom that is only written: a named operation over a store.
A component that dispatches one does not subscribe to anything, so it does not re-render when the state the action changes changes. That is the whole point of having it as an atom rather than a function: it is written where the state is, it can be replaced in a test by providing a different store, and dispatching it costs the caller no subscription.
function
asyncAtom
export function asyncAtom<T>(
load: (get: Getter, context: LoadContext) => Promise<T>,
options?: AtomOptions<Loadable<T>>,
): AsyncAtom<T> { ... }
A derived atom whose read is asynchronous.
Its value is a [Loadable] — loading, hasData or hasError — rather than a promise a component suspends on. Suspense is not available to a useSyncExternalStore reader without throwing a promise from inside a snapshot, which is neither supported nor safe under concurrent rendering, so this package makes the loading state a value the caller renders rather than a control-flow trick. unwrap is there for callers that just want a fallback.
The load is tracked: asyncAtom((get) => fetchUser(get(userId))) reloads when userId changes, and — this is the part that is hard to get right by hand — the load already in flight for the previous id is discarded rather than allowed to win a race and deliver the wrong user.
Discarded, and also stopped. The load's second argument carries an AbortSignal, aborted when a newer load supersedes this one, when [refresh] asks for another, and when the atom loses its last subscriber:
const user = asyncAtom((get, { signal }) =>
fetch(`/users/${get(userId)}`, { signal }).then((response) => response.json()),
);
A load that ignores the signal is still correct — whether a result is adopted is decided by the store either way — but on a search box that reloads per keystroke, ignoring it is one live request per keystroke.
An aborted load's rejection is not the atom's error: it never becomes { state: "hasError" }, because it is the answer to a question the atom stopped asking.
function
refresh
export function refresh<T>(target: AsyncAtom<T>, store?: Store): void { ... }
Load an asynchronous atom again, with the dependencies it already has.
The ordinary case after a mutation, and behind the Retry button on the error state [Loadable] exists to make renderable. An asyncAtom otherwise reloads only when something it read changes, and writing a dependency the value it already holds is correctly dropped by the equality cutoff — so without this there is no way to say "ask again" at all.
A free function rather than a write, which is the choice Jotai makes with atomWithRefresh. Two reasons, and the second is the one that decided it: WritableAtom<T, A> promises that A is the argument type, and an atom that is suddenly writable with no argument muddies that; and a free function works from a route handler, an event handler and a test, none of which have a component to hold a setter.
The atom passes through { state: "loading" } on the way, so a list that shows a spinner while it refetches gets one without asking.
function
atomWithDefault
export function atomWithDefault<T>(
getDefault: (get: Getter) => T,
options?: AtomOptions<T>,
): WritableAtom<T, SetAction<T> | Reset> { ... }
An atom whose value is computed until someone writes one, and again after [RESET].
function
atomWithReset
export function atomWithReset<T>(
initial: T,
options?: AtomOptions<T>,
): WritableAtom<T, SetAction<T> | Reset> { ... }
A piece of state that also answers to [RESET].
atom(0) cannot be reset — its argument type is SetAction<number> and nothing else — so this is what an application reaches for when "back to the default" is a thing the interface offers. It is [atomWithDefault] taking a value instead of a read, which is what a caller who has one already has.
function
atomWithReducer
export function atomWithReducer<State, Action>(
initial: State,
reduce: (state: State, action: Action) => State,
options?: AtomOptions<State>,
): WritableAtom<State, Action> { ... }
State and the actions that change it: useReducer, as an atom.
const count = atomWithReducer<number, "increment" | "reset">(0, (n, action) =>
action === "increment" ? n + 1 : 0,
);
The value and the argument are different types, which is the reason to use this rather than an [atom]: useSetAtom(count) hands a component a dispatch that takes an action, so a state written to it by mistake is a type error rather than a state machine with a hole in it.
function
selectAtom
export function selectAtom<T, Slice>(
source: ReadonlyAtom<T>,
select: (value: T) => Slice,
equals?: (previous: Slice, next: Slice) => boolean,
): ReadonlyAtom<Slice> { ... }
One part of another atom, so that a reader of the part is woken only when the part changes.
const name = selectAtom(user, (current) => current.name);
A component reading that re-renders when the name changes and not when anything else about the user does. equals is for a selection that builds a fresh value each time — an array of ids, a filtered list — which would otherwise be a new value on every recompute and wake every reader.
Jotai's selector also takes the previous slice. This one does not: a read that can see its own output is not a pure function of its dependencies, which is what the equality cutoff underneath rests on, and equals is the direct way to say the thing that parameter was used to say.
function
freezeAtom
export function freezeAtom<T>(source: ReadonlyAtom<T>): ReadonlyAtom<T> { ... }
The same atom, deeply frozen, so a mutation in place fails where it happens.
state.items.push(row) does not replace the value, so the equality cutoff correctly reports no change and nothing re-renders — the most expensive beginner bug in this style of state, because the symptom appears in a component that is not the one at fault. Frozen, the push throws in a module and does nothing outside one, and either way it is at the line that did it.
There is one object, not two: the value handed back is the value that came in, so the atom this derives from is frozen along with it. That is what makes the guard worth anything, and it is worth knowing before wrapping an atom other code writes to.
function
atomWithStorage
export function atomWithStorage<T>(
key: string,
initial: T,
storage?: StorageAdapter<T>,
options?: StorageOptions<T>,
): WritableAtom<T, SetAction<T> | Reset> { ... }
An atom mirrored into a key-value store on every write, and read back from it when a store mounts it.
const theme = atomWithStorage("theme", "light", createJSONStorage(() => localStorage));
The read happens on mount rather than when the atom is declared, and that is the whole difference between a persisted preference that survives hydration and one that does not: a server renders initial, so the first client render has to be initial too, and the stored value arrives immediately after commit. options.getOnInit is for an application with no server render to agree with.
Writing [RESET] removes the key rather than storing the initial value, which is the difference between "back to the default" and "persisting the default forever".
function
atomWithAsyncStorage
export function atomWithAsyncStorage<T>(
key: string,
initial: T,
storage: AsyncStorageAdapter<T>,
options?: AsyncStorageOptions<T>,
): WritableAtom<Loadable<T>, AsyncSetAction<T> | Reset> { ... }
An atom persisted to a storage whose read is asynchronous — IndexedDB, a React Native AsyncStorage, a preference store behind a request.
const draft = atomWithAsyncStorage("draft", "", indexedDb);
// { state: "loading" }, and then { state: "hasData", data: "…" }
A second constructor rather than an option on [atomWithStorage], because the value is a different type. It is a [Loadable] until the first read settles, for the reason [asyncAtom] gives: a useSyncExternalStore reader cannot suspend, so a value that has not arrived is a state to render rather than a promise to throw. Jotai's atomWithStorage takes an async storage and makes the value T | Promise<T>; taking that signature without Suspense would leave the type promising a T that is not there.
The setter takes a T, so the two type parameters differ. Its reducer form is handed the Loadable, not the value, because a write can happen before the first read has settled and there may be nothing to reduce.
Three behaviours worth knowing before reaching for it, each of them a decision rather than a consequence:
- a write while the first read is in flight wins, and the read is abandoned and its signal aborted — the load stops being a dependency, and the cell underneath decides the rest;
- a read that *fails* is
{ state: "hasError" } rather than initial, which is the whole reason this constructor can exist and the synchronous one cannot do it: "the database is locked" is not "nobody set a preference". An absent key is still initial; - a *write* that fails is silent. The value the caller wrote is the atom's value regardless; it simply will not outlive the session.
[RESET] removes the key and puts the atom back to initial without reading again — a re-read would race the removal it has not waited for.
function
unwrap
export function unwrap<T>(target: ReadonlyAtom<Loadable<T>>, fallback: T): ReadonlyAtom<T> { ... }
The data an asynchronous atom is holding, or fallback until it has some.
function
createStore
export function createStore(): Store { ... }
A store of your own.
One per request on a server, one per test that wants a clean slate, one per subtree that needs to disagree with the page around it.
function
getDefaultStore
export function getDefaultStore(): Store { ... }
The store everything that does not name one uses.
function
read
export function read<T>(target: ReadonlyAtom<T>, store?: Store): T { ... }
Read an atom out of a store, or out of the default store.
The same value a component would see, with no component: this is how a route handler, an event handler outside React, or a test reads state.
function
write
export function write<T, A>(target: WritableAtom<T, A>, argument: A, store?: Store): void { ... }
Write an atom in a store, or in the default store.
function
subscribe
export function subscribe<T>(
target: ReadonlyAtom<T>,
listener: () => void,
store?: Store,
): Unsubscribe { ... }
Be told when an atom's value changes, outside React.
Subscribing is also what mounts the atom, so an atom with an onMount is started by the first subscriber and stopped by the last — including when that subscriber is a component.
component
Provider
export component Provider(store?: Store, children: React.Node) { ... }
Give a subtree its own store.
With no store, the provider owns one it creates for itself, which is the shortest way to isolate a subtree — or a test — from everything else.
hook
useStore
export hook useStore(store?: Store): Store { ... }
The store this part of the tree reads: the scoped one, or the default.
hook
useAtomValue
export hook useAtomValue<T>(target: ReadonlyAtom<T>, store?: Store): T { ... }
Read an atom, re-rendering when — and only when — its value changes.
A component that reads three atoms re-renders when any of the three changes and not when a fourth does, because the subscription is per atom rather than per store. A derived atom that recomputes to the value it already had does not re-render its readers at all.
hook
useSetAtom
export hook useSetAtom<T, A>(target: WritableAtom<T, A>, store?: Store): (argument: A) => void { ... }
Write an atom without reading it.
A component that only dispatches does not subscribe, so it does not re-render when the value it writes changes. The setter's identity is stable for as long as the store and the atom are, so passing it to a memoised child costs that child nothing.
hook
useAtom
export hook useAtom<T, A>(target: WritableAtom<T, A>, store?: Store): [T, (argument: A) => void] { ... }
Read and write an atom, in the shape useState returns.
hook
useResetAtom
export hook useResetAtom<T>(
target: WritableAtom<T, SetAction<T> | Reset>,
store?: Store,
): () => void { ... }
Put a resettable atom back, from a component.
Jotai's useResetAtom. useSetAtom(target) already does this — the argument is [RESET] — and the difference is the shape of what comes back: () => void goes straight onto an onClick, where the setter needs a wrapper that supplies the symbol.
It takes an atom whose write accepts RESET as well as a value, which is what [atomWithReset], [atomWithDefault] and [atomWithStorage] all return. A plain [atom] is refused, and refused at the call rather than with a runtime error, because its argument type does not include the symbol. [atomWithAsyncStorage]'s setter has a different value type, so it resets through useSetAtom — its own doc comment says so.
hook
useAtomCallback
export hook useAtomCallback<Args extends $ReadOnlyArray<mixed>, Result>(
callback: (get: Getter, set: Setter, ...args: Args) => Result,
store?: Store,
): (...args: Args) => Result { ... }
A callback that can read and write any atom, and subscribes to none of them.
Jotai's useAtomCallback. For the handler that needs the *current* value of something it does not render — a submit that reads a draft, an analytics call that reads a filter — where useAtomValue would re-render the component every time that value changed for a value it only ever looks at once.
The get and set are the same pair a [writableSelector]'s write is given, resolved through the store this component is in. Reading through it records no dependency, because a handler runs outside any computation and there is nothing for one to be recorded against — which is the point: the component that holds this callback is subscribed to nothing.
The identity of what comes back changes when callback does, so a handler written inline is a new function every render unless something memoizes it — which, inside a component the React Compiler compiled, it does. The same rule as every other hook that takes a function.
hook
useCell
export hook useCell<T>(source: Cell<T>): T { ... }
Subscribe a component to a cell directly.
The escape hatch to the layer below: a route loader hands out @uniflowed/cell cells, and a component that reads one should not have to wrap it in an atom to do so. A cell holds its own value, so no store is involved and the store argument the other hooks take would mean nothing.
opaque-type
Reset
export opaque type Reset = symbol;
The argument that puts an atom back the way it was.
Opaque so it cannot be confused with a value: an atom of symbol would otherwise have a value that silently means "reset".
type
StorageAdapter
export type StorageAdapter<T> = {
readonly getItem: (key: string, initial: T) => T,
readonly setItem: (key: string, value: T) => void,
readonly removeItem: (key: string) => void,
readonly subscribe?: (key: string, onChange: (value: T) => void, initial: T) => () => void,
};
Where [atomWithStorage] keeps a value.
Typed in the value rather than in strings, which is the difference between this and the string store underneath it: a cookie jar, a React Native AsyncStorage, an in-memory map in a test and localStorage do not agree on a serialisation, and the one they would agree on is JSON — which is [createJSONStorage]'s job, not this type's.
getItem takes the initial value so that "nothing is stored" and "the stored value will not parse" have one answer in one place rather than an ?T every caller unwraps the same way.
removeItem is required, not optional. A persistence layer that cannot delete is not one: state that can be stored and never un-stored has no expression for RESET, and an application that offers "clear my preferences" would have to reach past this type to do it.
subscribe is optional because most storages have no way to tell anyone they changed. One that does — the browser's storage event — is how a second tab reaches this one. It is given the initial value to report when the key is removed elsewhere.
type
StringStorage
export type StringStorage = {
readonly getItem: (key: string) => null | string,
readonly setItem: (key: string, value: string) => void,
readonly removeItem: (key: string) => void,
...
};
The part of the Web Storage API [createJSONStorage] needs.
Named rather than taken as Storage, because a server has no Storage and a test should not need one: anything with these three methods will do. Inexact, so a real localStorage — which has length, key and clear as well — is one of these.
type
JSONStorageOptions
export type JSONStorageOptions<T> = {
/**
* Turn what `JSON.parse` produced into a `T`, or throw to fall back to the
* initial value.
*
* This is the package's one unchecked step, and the option exists so that a
* caller who minds can check it: `JSON.parse` answers `any`, and what comes
* back out of storage was written by an older version of the application, by
* another tab, or by a person editing their own `localStorage`. The default
* trusts it, because persistence is a cache and a cache that refuses to
* start is worse than one that is occasionally stale. `revive: (raw) =>
* schema.parse(raw)` with `@uniflowed/validator` is the version that does
* not trust it, and it needs no support from here — a `revive` that throws
* is a value that will not parse.
*/
readonly revive?: (raw: mixed) => T,
};
What [createJSONStorage] can be told about the values it reads.
type
AtomFamily
export type AtomFamily<Key, Member> = {
(key: Key): Member,
readonly remove: (key: Key) => void,
readonly size: () => number,
...
};
A keyed collection of atoms, created on first use.
remove matters more than it looks: a family keyed by something unbounded — a search term, a date — otherwise keeps one atom per key ever asked for, and the atoms are reachable from the family, so nothing collects them.
function
atomFamily
export function atomFamily<Key, Member>(create: (key: Key) => Member): AtomFamily<Key, Member> { ... }
One atom per key, created on first use and the same one thereafter.
The alternative — one atom holding a map — re-renders every reader when any entry changes, because the map is one value. A family gives each key its own atom, so a list of a thousand rows re-renders one row.
type
StorageOptions
export type StorageOptions<T> = {
readonly debugLabel?: string,
readonly equals?: (previous: T, next: T) => boolean,
/**
* Read the stored value the first time the atom is used in a store, rather
* than when it is mounted there. `false` by default.
*
* The default is what makes a server-rendered page hydrate: the first client
* render has to be the one the server already sent, and it cannot be if the
* value has been read out of `localStorage` before React runs. Turning this
* on says "there is no server render to agree with" — an application that is
* only ever a browser tab, where a first paint holding `initial` is a flash
* of the wrong theme and nothing else is at stake.
*
* Even on, the read is per store and happens on demand; it never happens
* while the module is being evaluated.
*/
readonly getOnInit?: boolean,
};
What [atomWithStorage] accepts on top of what every atom does.
function
createJSONStorage
export function createJSONStorage<T>(
getStringStorage: () => StringStorage | null | void,
options?: JSONStorageOptions<T>,
): StorageAdapter<T> { ... }
A [StorageAdapter] over localStorage, sessionStorage, or anything else that holds strings.
const theme = atomWithStorage("theme", "light", createJSONStorage(() => localStorage));
The thunk is what makes that line safe in a file a server imports, and the reason is more specific than "the storage might be missing": on a runtime without Web Storage the *identifier* localStorage is not defined, so evaluating it throws a ReferenceError rather than producing undefined. The thunk is called inside a try every time, so a module that names a storage no runtime here has still imports, and every operation on it becomes a no-op returning the initial value.
That is the whole portability story, and it was checked against four runtimes: Node has Web Storage from 22 and only with a flag, Deno has localStorage for a page it can name an origin for, Bun has it since 1.2, and an edge runtime has neither it nor a window. Every one of those is a try around the thunk and a null check afterwards.
getItem and setItem are also wrapped: a browser with site data blocked throws on the property, and Safari in private mode throws on a write once its quota is reached. A storage that cannot be written to degrades to an atom that is not persisted, which is the behaviour every one of those applications wants and none of them would write by hand.
subscribe listens for the browser's storage event, which is how another tab reaches this one. It deliberately does *not* announce writes made in this process: two stores in one process are two stores, and keeping them in step would undo the isolation they exist for.
# Why this guard is written twice
@uniflowed/hooks's useStorage (packages/hooks/state.js) guards the same four hazards — a storage property that throws, a read that throws, a write that throws, and finding the object a storage event arrives on — and was written from the same reasoning by somebody who had not read this. Merging the two was considered and declined; ubugeeei-prod/uf#318 is the issue, and this is half of the decision. The other half is in useStorage.
A shared helper has to live somewhere both packages may depend on, and there is no such place. @uniflowed/web is the obvious home — it already owns the platform bindings, and cookie.js reaches for globalThis the same way — but @uniflowed/hooks is on npm and @uniflowed/web is not (tools/release/pending-packages.txt). That edge would make npm install @uniflowed/hooks answer ETARGET: the tarball would name a version of @uniflowed/web the registry does not have, and it would install perfectly from this workspace, which is exactly how #409 stayed hidden. tools/ci/publishable.sh now refuses that edge, so the reason this decision rests on is a check rather than a paragraph.
The two are also less alike than the list of hazards suggests. This one is defined over a thunk the caller supplies and answers a T; that one picks its area from a boolean and answers a string. That one keeps a registry of same-tab listeners so two components sharing a key agree, which this one must not have — see the paragraph above. What is common once those are taken out is four try blocks and a JSON.parse with a fallback, and the shared thing worth extracting from that is the *reasoning*, which is why each copy now names the other.
What would change the answer, precisely: @uniflowed/web reaching npm (#210) removes the blocking reason, and a *third* caller writing these guards a third time removes the other one. Neither has happened, and a helper built before either is a dependency edge bought on speculation.
type
AsyncStorageAdapter
export type AsyncStorageAdapter<T> = {
readonly getItem: (key: string, initial: T, context: LoadContext) => Promise<T>,
readonly setItem: (key: string, value: T) => Promise<mixed> | void,
readonly removeItem: (key: string) => Promise<mixed> | void,
readonly subscribe?: (key: string, onChange: (value: T) => void, initial: T) => () => void,
};
Where [atomWithAsyncStorage] keeps a value: IndexedDB, a React Native AsyncStorage, a server-backed preference store.
The same shape as [StorageAdapter] with the read made a promise, and it is a separate type rather than a widening of that one because the difference is not the adapter's — it is the *atom's value type*, which becomes a [Loadable]. See [atomWithAsyncStorage].
getItem is handed the load's LoadContext, so an adapter that can stop an IndexedDB request or a fetch has the signal to stop it with. Ignoring the third parameter is fine and is what a Map-backed adapter in a test does: whether a settled read is still the one the atom is waiting for is decided by @uniflowed/cell either way, and the signal only decides whether the work carries on in the meantime.
setItem and removeItem answer a promise or nothing, and the atom does not wait for either — see the note about failed writes on [atomWithAsyncStorage].
type
AsyncSetAction
export type AsyncSetAction<T> = T | ((current: Loadable<T>) => T);
What [atomWithAsyncStorage]'s setter accepts.
A value, or a function of what the atom currently holds — which is a [Loadable] rather than a T, and that is not a wrinkle to be smoothed over. A write can happen before the first read has settled, so there may be no current value to reduce; a reducer typed (current: T) => T would be a promise the atom cannot keep. Handed the loadable, a caller who wants to increment a persisted counter has to say what "increment" means before the count has arrived, which is a question they have to answer anyway.
type
AsyncStorageOptions
export type AsyncStorageOptions<T> = {
readonly debugLabel?: string,
/**
* When two values of `T` are the same value.
*
* Over `T` rather than over `Loadable<T>`, because `T` is what a caller has
* an opinion about; the constructor lifts it to the loadable, where
* `loading` equals `loading` and an error equals itself.
*/
readonly equals?: (previous: T, next: T) => boolean,
};
What [atomWithAsyncStorage] accepts on top of what every atom does.
type
SetAction
export type SetAction<T> = T | ((current: T) => T);
What a setter accepts: a value, or a reducer over the current one.
type
Loadable
export type Loadable<T> =
| { readonly state: "loading" }
| { readonly state: "hasData", readonly data: T }
| { readonly state: "hasError", readonly error: mixed };
The three states an asynchronous read can be in.
A discriminated union rather than { loading, data, error } with three optional fields, because two of those eight combinations are nonsense and match can prove this one is exhaustive.
type
AtomMount
export type AtomMount<T> = {
readonly get: () => T,
readonly set: (value: T) => void,
readonly subscribe: (listener: () => void) => Unsubscribe,
};
What an atom's onMount is handed: the atom, bound to the store it was mounted in.
set goes through the store's ordinary write path, so an atom that mounts a subscription feeds values in exactly the way an event handler would.
type
AtomOptions
export type AtomOptions<T> = {
readonly debugLabel?: string,
readonly equals?: (previous: T, next: T) => boolean,
};
What every constructor accepts.
type
PrimitiveOptions
export type PrimitiveOptions<T> = {
readonly debugLabel?: string,
readonly equals?: (previous: T, next: T) => boolean,
readonly onMount?: (mount: AtomMount<T>) => void | (() => void),
};
What a primitive atom accepts, which is a mount as well.
Without a doc comment