@uniflowed/state 0.0.0-alpha.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +691 -0
- package/internal/atom.js +213 -0
- package/internal/composed.js +882 -0
- package/internal/provider.js +64 -0
- package/internal/store.js +359 -0
- package/package.json +25 -0
package/index.js
ADDED
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/state`: atoms, stores, and the React binding over them.
|
|
4
|
+
//
|
|
5
|
+
// # Where the line between this package and `@uniflowed/cell` is
|
|
6
|
+
//
|
|
7
|
+
// `@uniflowed/cell` is the reactive core: dependency tracking, memoisation,
|
|
8
|
+
// the equality cutoff, batching, glitch-free propagation. It has no opinion
|
|
9
|
+
// about React and no concept of a store.
|
|
10
|
+
//
|
|
11
|
+
// This package adds two things and nothing else:
|
|
12
|
+
//
|
|
13
|
+
// * an atom is a *definition* rather than a value, so the same declaration can
|
|
14
|
+
// have a different value in every store — which is what makes one process
|
|
15
|
+
// able to render two requests, and one page able to hold a draft of itself;
|
|
16
|
+
// * React, through `useSyncExternalStore`.
|
|
17
|
+
//
|
|
18
|
+
// There is no second dependency graph here. A derived atom is instantiated
|
|
19
|
+
// into a `derived` cell whose read is bound to a store, and every question
|
|
20
|
+
// about *when* it recomputes is answered one layer down. Two implementations
|
|
21
|
+
// of dependency tracking in one product is one too many, and the one that
|
|
22
|
+
// would rot is the copy.
|
|
23
|
+
//
|
|
24
|
+
// The vocabularies are close enough to be worth telling apart. One layer down,
|
|
25
|
+
// `state` and `derived` build cells that hold their own values; here, `atom`
|
|
26
|
+
// and `selector` build *definitions* that have a different value in every
|
|
27
|
+
// store. That is also why `read`, `write` and `subscribe` are spelled the same
|
|
28
|
+
// in both packages and take different arguments: an atom needs a store to be
|
|
29
|
+
// resolved against, and a cell is the value.
|
|
30
|
+
//
|
|
31
|
+
// # The file map
|
|
32
|
+
//
|
|
33
|
+
// * `internal/atom.js` — what an atom is before any store exists.
|
|
34
|
+
// * `internal/store.js` — where an atom's value lives, and how React reaches
|
|
35
|
+
// it.
|
|
36
|
+
// * `internal/composed.js` — atoms assembled out of other atoms: families,
|
|
37
|
+
// defaults, persistence.
|
|
38
|
+
// * `internal/provider.js` — which store a React subtree uses.
|
|
39
|
+
//
|
|
40
|
+
// This entry point is the vocabulary: the types an application writes down,
|
|
41
|
+
// the constructors it calls, and the four hooks. Every one of them is thin,
|
|
42
|
+
// because the machinery is behind them rather than in them.
|
|
43
|
+
//
|
|
44
|
+
// # Why the constructors are named rather than overloaded
|
|
45
|
+
//
|
|
46
|
+
// Jotai spells all of this `atom(...)` and decides what was meant from the
|
|
47
|
+
// arguments. That works in TypeScript, where the overload set is resolved at
|
|
48
|
+
// the call site; in Flow the honest version is an intersection of function
|
|
49
|
+
// types that the checker resolves poorly, and inference through composition is
|
|
50
|
+
// a stated requirement here. It is also ambiguous at runtime: `atom(f)` cannot
|
|
51
|
+
// distinguish a derived atom from a primitive one holding a function.
|
|
52
|
+
//
|
|
53
|
+
// So the four kinds are four names — `atom`, `selector`, `writableSelector`,
|
|
54
|
+
// `action` — and each returns exactly one type. In Jotai's terms they are
|
|
55
|
+
// `atom(value)`, `atom(read)`, `atom(read, write)` and `atom(null, write)`.
|
|
56
|
+
//
|
|
57
|
+
// # Why `useSyncExternalStore`
|
|
58
|
+
//
|
|
59
|
+
// A store is an external store, so the binding is the hook React provides for
|
|
60
|
+
// exactly that rather than a `useState` mirror kept in step by an effect. The
|
|
61
|
+
// difference is not stylistic: a mirror is written during commit, so a
|
|
62
|
+
// concurrent render can read one component's copy before another's has caught
|
|
63
|
+
// up, and the two disagree on screen. That is tearing, and this hook is the
|
|
64
|
+
// API that exists to prevent it.
|
|
65
|
+
//
|
|
66
|
+
// Nothing here mutates anything a render can observe, no hook returns a live
|
|
67
|
+
// mutable object, and the callbacks handed to React have an identity that does
|
|
68
|
+
// not change — see `internal/store.js` for why each of those matters.
|
|
69
|
+
//
|
|
70
|
+
// # What is deliberately not here
|
|
71
|
+
//
|
|
72
|
+
// `jotai/utils` is fifteen names and this package answers most of them. The
|
|
73
|
+
// ones it does not are listed here rather than left to be discovered, because
|
|
74
|
+
// "we thought about it and decided against" is worth more to somebody porting
|
|
75
|
+
// an application than silence is. ubugeeei-prod/uf#292 is the triage in full.
|
|
76
|
+
//
|
|
77
|
+
// * `atomWithLazy` — `atomWithDefault` already takes exactly that function.
|
|
78
|
+
// A second name for one thing is worse than one name for it.
|
|
79
|
+
// * `useHydrateAtoms` — hydrates the store the component is already in, on
|
|
80
|
+
// first render. That is a write to shared state during render, and
|
|
81
|
+
// `internal/store.js` sets out the conditions under which this package
|
|
82
|
+
// makes its one render-time write; this would meet none of them. The same
|
|
83
|
+
// job outside React is `createStore()`, `write(...)`, `<Provider store>`,
|
|
84
|
+
// which is one line longer and writes nothing during a render.
|
|
85
|
+
// * `atomFamily`'s `areEqual` — it turns an `O(1)` `Map` lookup into a linear
|
|
86
|
+
// scan of every key the family has ever seen, on a call that happens once
|
|
87
|
+
// per row per render. A key that is a tuple should be made a string;
|
|
88
|
+
// `family(\`${year}:${month}\`)` costs nothing and says what it costs.
|
|
89
|
+
// * `loadable` — deprecated upstream in favour of `unwrap`, which is here,
|
|
90
|
+
// and `asyncAtom` produces the `Loadable` shape directly rather than
|
|
91
|
+
// needing a wrapper to put it there.
|
|
92
|
+
// * `atomWithRefresh` — `refresh(target, store?)` is the same capability as a
|
|
93
|
+
// free function; the doc comment on it says why that shape was chosen.
|
|
94
|
+
// * `splitAtom` — an atom of one atom per row, each keeping its identity
|
|
95
|
+
// across an insert, a remove and a move. Not a stub and not a few lines:
|
|
96
|
+
// the row atoms have to be memoised somewhere, and a `selector`'s read is
|
|
97
|
+
// handed a `get` and nothing else, so there is no store to memoise them
|
|
98
|
+
// against. Every version of it either invents a per-store cache the read
|
|
99
|
+
// cannot reach or shares row atoms between stores and then has to answer
|
|
100
|
+
// what a row means in a store whose list does not contain it. That is a
|
|
101
|
+
// change to the store's model, and it does not belong inside a utility.
|
|
102
|
+
// * `atomWithObservable` — an atom fed by an `Observable` that may itself
|
|
103
|
+
// depend on other atoms. The half that does not is already `atom(initial,
|
|
104
|
+
// { onMount })`. The half that does needs a mount that re-runs when a
|
|
105
|
+
// dependency changes, and `onMount` is a subscription lifecycle: it runs
|
|
106
|
+
// for the first subscriber and stops for the last, and never in between.
|
|
107
|
+
// Offering it on a derived atom — which `internal/store.js` would already
|
|
108
|
+
// honour — would hand callers a `get` whose changes their subscription
|
|
109
|
+
// never hears about, which is worse than not offering it.
|
|
110
|
+
|
|
111
|
+
import type { Cell, LoadContext, Unsubscribe } from "@uniflowed/cell";
|
|
112
|
+
import * as React from "@uniflowed/react";
|
|
113
|
+
import { useSyncExternalStore } from "@uniflowed/react";
|
|
114
|
+
|
|
115
|
+
import type {
|
|
116
|
+
AtomOptions,
|
|
117
|
+
AtomRecord,
|
|
118
|
+
Loadable,
|
|
119
|
+
PrimitiveOptions,
|
|
120
|
+
SetAction,
|
|
121
|
+
} from "./internal/atom.js";
|
|
122
|
+
import { defineAction, defineAsync, definePrimitive, defineSelector } from "./internal/atom.js";
|
|
123
|
+
import type {
|
|
124
|
+
AsyncSetAction,
|
|
125
|
+
AsyncStorageAdapter,
|
|
126
|
+
AsyncStorageOptions,
|
|
127
|
+
Reset,
|
|
128
|
+
StorageAdapter,
|
|
129
|
+
StorageOptions,
|
|
130
|
+
} from "./internal/composed.js";
|
|
131
|
+
import {
|
|
132
|
+
RESET,
|
|
133
|
+
atomWithAsyncStorage as composeWithAsyncStorage,
|
|
134
|
+
atomWithDefault as composeWithDefault,
|
|
135
|
+
atomWithReducer as composeWithReducer,
|
|
136
|
+
atomWithReset as composeWithReset,
|
|
137
|
+
atomWithStorage as composeWithStorage,
|
|
138
|
+
freezeAtom as composeFreeze,
|
|
139
|
+
selectAtom as composeSelect,
|
|
140
|
+
unwrap as composeUnwrap,
|
|
141
|
+
} from "./internal/composed.js";
|
|
142
|
+
import type { StoreInstance } from "./internal/store.js";
|
|
143
|
+
import { bindCell, createStore as createStoreInstance, defaultStore } from "./internal/store.js";
|
|
144
|
+
import { StoreScope, useStoreInstance } from "./internal/provider.js";
|
|
145
|
+
|
|
146
|
+
export type {
|
|
147
|
+
AsyncSetAction,
|
|
148
|
+
AsyncStorageAdapter,
|
|
149
|
+
AsyncStorageOptions,
|
|
150
|
+
AtomFamily,
|
|
151
|
+
JSONStorageOptions,
|
|
152
|
+
Reset,
|
|
153
|
+
StorageAdapter,
|
|
154
|
+
StorageOptions,
|
|
155
|
+
StringStorage,
|
|
156
|
+
} from "./internal/composed.js";
|
|
157
|
+
export type {
|
|
158
|
+
AtomMount,
|
|
159
|
+
AtomOptions,
|
|
160
|
+
Loadable,
|
|
161
|
+
PrimitiveOptions,
|
|
162
|
+
SetAction,
|
|
163
|
+
} from "./internal/atom.js";
|
|
164
|
+
export type { Cell, LoadContext, Unsubscribe };
|
|
165
|
+
|
|
166
|
+
export { atomFamily, createJSONStorage, RESET } from "./internal/composed.js";
|
|
167
|
+
export { batch } from "@uniflowed/cell";
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Any atom, as something to read.
|
|
171
|
+
*
|
|
172
|
+
* Every other atom type is a subtype of this one, so a function that only
|
|
173
|
+
* reads takes a `ReadonlyAtom<T>` and accepts all of them.
|
|
174
|
+
*/
|
|
175
|
+
export opaque type ReadonlyAtom<T> = AtomRecord<T, empty>;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* An atom that can be written with an argument of type `A`.
|
|
179
|
+
*
|
|
180
|
+
* `A` is not always the value: an atom holding a list may take an "add this
|
|
181
|
+
* one" argument, and keeping the two apart is what lets the setter a component
|
|
182
|
+
* is handed be typed exactly.
|
|
183
|
+
*/
|
|
184
|
+
export opaque type WritableAtom<T, A>: ReadonlyAtom<T> = AtomRecord<T, A>;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* A piece of state: readable, and writable the way `useState` is.
|
|
188
|
+
*
|
|
189
|
+
* The `useState`-shaped argument — a value, or a function of the current one —
|
|
190
|
+
* is the reason this is a type of its own rather than
|
|
191
|
+
* `WritableAtom<T, T>`: `setCount((n) => n + 1)` has to mean what it does
|
|
192
|
+
* everywhere else in React.
|
|
193
|
+
*/
|
|
194
|
+
export opaque type Atom<T>: WritableAtom<T, SetAction<T>> = AtomRecord<T, SetAction<T>>;
|
|
195
|
+
|
|
196
|
+
/** An atom that is only ever written: an action. */
|
|
197
|
+
export opaque type WriteOnlyAtom<A>: WritableAtom<null, A> = AtomRecord<null, A>;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* An atom whose value arrives from a promise: what [`asyncAtom`] returns.
|
|
201
|
+
*
|
|
202
|
+
* Nominally distinct from `ReadonlyAtom<Loadable<T>>` even though the two are
|
|
203
|
+
* the same record, and the distinction earns its place at exactly one call
|
|
204
|
+
* site: [`refresh`] can only mean something for an atom that has a load to
|
|
205
|
+
* run, and a `selector` that happens to return a `Loadable` — a cache lookup
|
|
206
|
+
* projected into one, say — has nothing to refresh. Without the name that
|
|
207
|
+
* mistake is a runtime error; with it, Flow says so at the call site.
|
|
208
|
+
*/
|
|
209
|
+
export opaque type AsyncAtom<T>: ReadonlyAtom<Loadable<T>> = AtomRecord<Loadable<T>, empty>;
|
|
210
|
+
|
|
211
|
+
/** Reading another atom, inside a read or a write. */
|
|
212
|
+
export type Getter = <V>(target: ReadonlyAtom<V>) => V;
|
|
213
|
+
|
|
214
|
+
/** Writing another atom, inside a write. */
|
|
215
|
+
export type Setter = <V, A>(target: WritableAtom<V, A>, argument: A) => void;
|
|
216
|
+
|
|
217
|
+
/** What `useSetAtom` hands back for a `useState`-shaped atom. */
|
|
218
|
+
export type AtomSetter<T> = (next: SetAction<T>) => void;
|
|
219
|
+
|
|
220
|
+
/** What `useAtom` hands back: the shape `useState` returns. */
|
|
221
|
+
export type AtomTuple<T> = [T, AtomSetter<T>];
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Where atom values live.
|
|
225
|
+
*
|
|
226
|
+
* Opaque, and deliberately small: `get`, `set` and `sub` are everything an
|
|
227
|
+
* application needs, and everything a test needs to assert on a tree's state
|
|
228
|
+
* without rendering one.
|
|
229
|
+
*/
|
|
230
|
+
export opaque type Store = StoreInstance;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A piece of state.
|
|
234
|
+
*
|
|
235
|
+
* Declaring one allocates nothing and belongs to no store — the value appears
|
|
236
|
+
* the first time a store is asked for it. That is what makes it safe to
|
|
237
|
+
* declare atoms at module scope in a file a server imports.
|
|
238
|
+
*
|
|
239
|
+
* `onMount` runs when the first subscriber in a store arrives and its return
|
|
240
|
+
* value runs when the last one leaves, which is where a subscription to
|
|
241
|
+
* anything outside the graph belongs: a socket, an interval, a media query.
|
|
242
|
+
*/
|
|
243
|
+
export function atom<T>(initial: T, options?: PrimitiveOptions<T>): Atom<T> {
|
|
244
|
+
return definePrimitive(initial, options);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* State derived from other atoms, recomputed only when what it read changes.
|
|
249
|
+
*
|
|
250
|
+
* No dependency array: the read discovers its own dependencies by running, and
|
|
251
|
+
* they are rebuilt every time it runs. A read that branches —
|
|
252
|
+
* `get(showAll) ? get(all) : get(some)` — depends on the branch it took, so
|
|
253
|
+
* writing to the other one recomputes nothing.
|
|
254
|
+
*
|
|
255
|
+
* A read that returns an unchanged value does not re-render its readers, and
|
|
256
|
+
* `options.equals` is how a read that builds a fresh array each time says what
|
|
257
|
+
* "unchanged" means for it.
|
|
258
|
+
*/
|
|
259
|
+
export function selector<T>(read: (get: Getter) => T, options?: AtomOptions<T>): ReadonlyAtom<T> {
|
|
260
|
+
return defineSelector(read, null, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* A selector you can also write to.
|
|
265
|
+
*
|
|
266
|
+
* The write is given `get` and `set`, so it can decide what a change to this
|
|
267
|
+
* atom means in terms of the atoms it is derived from — a "full name" atom
|
|
268
|
+
* whose write splits into first and last, a filter atom that also resets the
|
|
269
|
+
* page number. Everything it sets happens in one batch, so subscribers to
|
|
270
|
+
* three of those atoms are woken once each rather than once per `set`.
|
|
271
|
+
*
|
|
272
|
+
* `get` inside a write does not create a dependency. A write is not a
|
|
273
|
+
* computation, and an atom that recomputed because a handler looked at
|
|
274
|
+
* something would be very hard to explain.
|
|
275
|
+
*/
|
|
276
|
+
export function writableSelector<T, A>(
|
|
277
|
+
read: (get: Getter) => T,
|
|
278
|
+
write: (get: Getter, set: Setter, argument: A) => void,
|
|
279
|
+
options?: AtomOptions<T>,
|
|
280
|
+
): WritableAtom<T, A> {
|
|
281
|
+
return defineSelector(read, write, options);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* An atom that is only written: a named operation over a store.
|
|
286
|
+
*
|
|
287
|
+
* A component that dispatches one does not subscribe to anything, so it does
|
|
288
|
+
* not re-render when the state the action changes changes. That is the whole
|
|
289
|
+
* point of having it as an atom rather than a function: it is written where
|
|
290
|
+
* the state is, it can be replaced in a test by providing a different store,
|
|
291
|
+
* and dispatching it costs the caller no subscription.
|
|
292
|
+
*/
|
|
293
|
+
export function action<A>(
|
|
294
|
+
write: (get: Getter, set: Setter, argument: A) => void,
|
|
295
|
+
options?: AtomOptions<null>,
|
|
296
|
+
): WriteOnlyAtom<A> {
|
|
297
|
+
return defineAction(write, options);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* A derived atom whose read is asynchronous.
|
|
302
|
+
*
|
|
303
|
+
* Its value is a [`Loadable`] — `loading`, `hasData` or `hasError` — rather
|
|
304
|
+
* than a promise a component suspends on. Suspense is not available to a
|
|
305
|
+
* `useSyncExternalStore` reader without throwing a promise from inside a
|
|
306
|
+
* snapshot, which is neither supported nor safe under concurrent rendering, so
|
|
307
|
+
* this package makes the loading state a value the caller renders rather than
|
|
308
|
+
* a control-flow trick. `unwrap` is there for callers that just want a
|
|
309
|
+
* fallback.
|
|
310
|
+
*
|
|
311
|
+
* The load is tracked: `asyncAtom((get) => fetchUser(get(userId)))` reloads
|
|
312
|
+
* when `userId` changes, and — this is the part that is hard to get right by
|
|
313
|
+
* hand — the load already in flight for the previous id is discarded rather
|
|
314
|
+
* than allowed to win a race and deliver the wrong user.
|
|
315
|
+
*
|
|
316
|
+
* Discarded, and also stopped. The load's second argument carries an
|
|
317
|
+
* `AbortSignal`, aborted when a newer load supersedes this one, when
|
|
318
|
+
* [`refresh`] asks for another, and when the atom loses its last subscriber:
|
|
319
|
+
*
|
|
320
|
+
* ```
|
|
321
|
+
* const user = asyncAtom((get, { signal }) =>
|
|
322
|
+
* fetch(`/users/${get(userId)}`, { signal }).then((response) => response.json()),
|
|
323
|
+
* );
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* A load that ignores the signal is still correct — whether a result is
|
|
327
|
+
* adopted is decided by the store either way — but on a search box that
|
|
328
|
+
* reloads per keystroke, ignoring it is one live request per keystroke.
|
|
329
|
+
*
|
|
330
|
+
* An aborted load's rejection is not the atom's error: it never becomes
|
|
331
|
+
* `{ state: "hasError" }`, because it is the answer to a question the atom
|
|
332
|
+
* stopped asking.
|
|
333
|
+
*/
|
|
334
|
+
export function asyncAtom<T>(
|
|
335
|
+
load: (get: Getter, context: LoadContext) => Promise<T>,
|
|
336
|
+
options?: AtomOptions<Loadable<T>>,
|
|
337
|
+
): AsyncAtom<T> {
|
|
338
|
+
return defineAsync(load, options);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Load an asynchronous atom again, with the dependencies it already has.
|
|
343
|
+
*
|
|
344
|
+
* The ordinary case after a mutation, and behind the Retry button on the error
|
|
345
|
+
* state [`Loadable`] exists to make renderable. An `asyncAtom` otherwise
|
|
346
|
+
* reloads only when something it read changes, and writing a dependency the
|
|
347
|
+
* value it already holds is correctly dropped by the equality cutoff — so
|
|
348
|
+
* without this there is no way to say "ask again" at all.
|
|
349
|
+
*
|
|
350
|
+
* A free function rather than a write, which is the choice Jotai makes with
|
|
351
|
+
* `atomWithRefresh`. Two reasons, and the second is the one that decided it:
|
|
352
|
+
* `WritableAtom<T, A>` promises that `A` is the argument type, and an atom
|
|
353
|
+
* that is suddenly writable with no argument muddies that; and a free function
|
|
354
|
+
* works from a route handler, an event handler and a test, none of which have
|
|
355
|
+
* a component to hold a setter.
|
|
356
|
+
*
|
|
357
|
+
* The atom passes through `{ state: "loading" }` on the way, so a list that
|
|
358
|
+
* shows a spinner while it refetches gets one without asking.
|
|
359
|
+
*/
|
|
360
|
+
export function refresh<T>(target: AsyncAtom<T>, store?: Store): void {
|
|
361
|
+
(store ?? defaultStore()).reload(target);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* An atom whose value is computed until someone writes one, and again after
|
|
366
|
+
* [`RESET`].
|
|
367
|
+
*/
|
|
368
|
+
export function atomWithDefault<T>(
|
|
369
|
+
getDefault: (get: Getter) => T,
|
|
370
|
+
options?: AtomOptions<T>,
|
|
371
|
+
): WritableAtom<T, SetAction<T> | Reset> {
|
|
372
|
+
return composeWithDefault(getDefault, options);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* A piece of state that also answers to [`RESET`].
|
|
377
|
+
*
|
|
378
|
+
* `atom(0)` cannot be reset — its argument type is `SetAction<number>` and
|
|
379
|
+
* nothing else — so this is what an application reaches for when "back to the
|
|
380
|
+
* default" is a thing the interface offers. It is [`atomWithDefault`] taking a
|
|
381
|
+
* value instead of a read, which is what a caller who has one already has.
|
|
382
|
+
*/
|
|
383
|
+
export function atomWithReset<T>(
|
|
384
|
+
initial: T,
|
|
385
|
+
options?: AtomOptions<T>,
|
|
386
|
+
): WritableAtom<T, SetAction<T> | Reset> {
|
|
387
|
+
return composeWithReset(initial, options);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* State and the actions that change it: `useReducer`, as an atom.
|
|
392
|
+
*
|
|
393
|
+
* ```
|
|
394
|
+
* const count = atomWithReducer<number, "increment" | "reset">(0, (n, action) =>
|
|
395
|
+
* action === "increment" ? n + 1 : 0,
|
|
396
|
+
* );
|
|
397
|
+
* ```
|
|
398
|
+
*
|
|
399
|
+
* The value and the argument are different types, which is the reason to use
|
|
400
|
+
* this rather than an [`atom`]: `useSetAtom(count)` hands a component a
|
|
401
|
+
* dispatch that takes an action, so a state written to it by mistake is a
|
|
402
|
+
* type error rather than a state machine with a hole in it.
|
|
403
|
+
*/
|
|
404
|
+
export function atomWithReducer<State, Action>(
|
|
405
|
+
initial: State,
|
|
406
|
+
reduce: (state: State, action: Action) => State,
|
|
407
|
+
options?: AtomOptions<State>,
|
|
408
|
+
): WritableAtom<State, Action> {
|
|
409
|
+
return composeWithReducer(initial, reduce, options);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* One part of another atom, so that a reader of the part is woken only when
|
|
414
|
+
* the part changes.
|
|
415
|
+
*
|
|
416
|
+
* ```
|
|
417
|
+
* const name = selectAtom(user, (current) => current.name);
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* A component reading that re-renders when the name changes and not when
|
|
421
|
+
* anything else about the user does. `equals` is for a selection that builds a
|
|
422
|
+
* fresh value each time — an array of ids, a filtered list — which would
|
|
423
|
+
* otherwise be a new value on every recompute and wake every reader.
|
|
424
|
+
*
|
|
425
|
+
* Jotai's selector also takes the previous slice. This one does not: a read
|
|
426
|
+
* that can see its own output is not a pure function of its dependencies,
|
|
427
|
+
* which is what the equality cutoff underneath rests on, and `equals` is the
|
|
428
|
+
* direct way to say the thing that parameter was used to say.
|
|
429
|
+
*/
|
|
430
|
+
export function selectAtom<T, Slice>(
|
|
431
|
+
source: ReadonlyAtom<T>,
|
|
432
|
+
select: (value: T) => Slice,
|
|
433
|
+
equals?: (previous: Slice, next: Slice) => boolean,
|
|
434
|
+
): ReadonlyAtom<Slice> {
|
|
435
|
+
return composeSelect(source, select, equals);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The same atom, deeply frozen, so a mutation in place fails where it happens.
|
|
440
|
+
*
|
|
441
|
+
* `state.items.push(row)` does not replace the value, so the equality cutoff
|
|
442
|
+
* correctly reports no change and nothing re-renders — the most expensive
|
|
443
|
+
* beginner bug in this style of state, because the symptom appears in a
|
|
444
|
+
* component that is not the one at fault. Frozen, the push throws in a module
|
|
445
|
+
* and does nothing outside one, and either way it is at the line that did it.
|
|
446
|
+
*
|
|
447
|
+
* There is one object, not two: the value handed back is the value that came
|
|
448
|
+
* in, so the atom this derives from is frozen along with it. That is what
|
|
449
|
+
* makes the guard worth anything, and it is worth knowing before wrapping an
|
|
450
|
+
* atom other code writes to.
|
|
451
|
+
*/
|
|
452
|
+
export function freezeAtom<T>(source: ReadonlyAtom<T>): ReadonlyAtom<T> {
|
|
453
|
+
return composeFreeze(source);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* An atom mirrored into a key-value store on every write, and read back from
|
|
458
|
+
* it when a store mounts it.
|
|
459
|
+
*
|
|
460
|
+
* ```
|
|
461
|
+
* const theme = atomWithStorage("theme", "light", createJSONStorage(() => localStorage));
|
|
462
|
+
* ```
|
|
463
|
+
*
|
|
464
|
+
* The read happens on mount rather than when the atom is declared, and that is
|
|
465
|
+
* the whole difference between a persisted preference that survives hydration
|
|
466
|
+
* and one that does not: a server renders `initial`, so the first client
|
|
467
|
+
* render has to be `initial` too, and the stored value arrives immediately
|
|
468
|
+
* after commit. `options.getOnInit` is for an application with no server
|
|
469
|
+
* render to agree with.
|
|
470
|
+
*
|
|
471
|
+
* Writing [`RESET`] removes the key rather than storing the initial value,
|
|
472
|
+
* which is the difference between "back to the default" and "persisting the
|
|
473
|
+
* default forever".
|
|
474
|
+
*/
|
|
475
|
+
export function atomWithStorage<T>(
|
|
476
|
+
key: string,
|
|
477
|
+
initial: T,
|
|
478
|
+
storage?: StorageAdapter<T>,
|
|
479
|
+
options?: StorageOptions<T>,
|
|
480
|
+
): WritableAtom<T, SetAction<T> | Reset> {
|
|
481
|
+
return composeWithStorage(key, initial, storage, options);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* An atom persisted to a storage whose read is asynchronous — IndexedDB, a
|
|
486
|
+
* React Native `AsyncStorage`, a preference store behind a request.
|
|
487
|
+
*
|
|
488
|
+
* ```
|
|
489
|
+
* const draft = atomWithAsyncStorage("draft", "", indexedDb);
|
|
490
|
+
* // { state: "loading" }, and then { state: "hasData", data: "…" }
|
|
491
|
+
* ```
|
|
492
|
+
*
|
|
493
|
+
* A second constructor rather than an option on [`atomWithStorage`], because
|
|
494
|
+
* the value is a different type. It is a [`Loadable`] until the first read
|
|
495
|
+
* settles, for the reason [`asyncAtom`] gives: a `useSyncExternalStore` reader
|
|
496
|
+
* cannot suspend, so a value that has not arrived is a state to render rather
|
|
497
|
+
* than a promise to throw. Jotai's `atomWithStorage` takes an async storage
|
|
498
|
+
* and makes the value `T | Promise<T>`; taking that signature without Suspense
|
|
499
|
+
* would leave the type promising a `T` that is not there.
|
|
500
|
+
*
|
|
501
|
+
* The setter takes a `T`, so the two type parameters differ. Its reducer form
|
|
502
|
+
* is handed the `Loadable`, not the value, because a write can happen before
|
|
503
|
+
* the first read has settled and there may be nothing to reduce.
|
|
504
|
+
*
|
|
505
|
+
* Three behaviours worth knowing before reaching for it, each of them a
|
|
506
|
+
* decision rather than a consequence:
|
|
507
|
+
*
|
|
508
|
+
* * a write while the first read is in flight wins, and the read is abandoned
|
|
509
|
+
* and its signal aborted — the load stops being a dependency, and the cell
|
|
510
|
+
* underneath decides the rest;
|
|
511
|
+
* * a read that *fails* is `{ state: "hasError" }` rather than `initial`,
|
|
512
|
+
* which is the whole reason this constructor can exist and the synchronous
|
|
513
|
+
* one cannot do it: "the database is locked" is not "nobody set a
|
|
514
|
+
* preference". An absent key is still `initial`;
|
|
515
|
+
* * a *write* that fails is silent. The value the caller wrote is the atom's
|
|
516
|
+
* value regardless; it simply will not outlive the session.
|
|
517
|
+
*
|
|
518
|
+
* [`RESET`] removes the key and puts the atom back to `initial` without
|
|
519
|
+
* reading again — a re-read would race the removal it has not waited for.
|
|
520
|
+
*/
|
|
521
|
+
export function atomWithAsyncStorage<T>(
|
|
522
|
+
key: string,
|
|
523
|
+
initial: T,
|
|
524
|
+
storage: AsyncStorageAdapter<T>,
|
|
525
|
+
options?: AsyncStorageOptions<T>,
|
|
526
|
+
): WritableAtom<Loadable<T>, AsyncSetAction<T> | Reset> {
|
|
527
|
+
return composeWithAsyncStorage(key, initial, storage, options);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** The data an asynchronous atom is holding, or `fallback` until it has some. */
|
|
531
|
+
export function unwrap<T>(target: ReadonlyAtom<Loadable<T>>, fallback: T): ReadonlyAtom<T> {
|
|
532
|
+
return composeUnwrap(target, fallback);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* A store of your own.
|
|
537
|
+
*
|
|
538
|
+
* One per request on a server, one per test that wants a clean slate, one per
|
|
539
|
+
* subtree that needs to disagree with the page around it.
|
|
540
|
+
*/
|
|
541
|
+
export function createStore(): Store {
|
|
542
|
+
return createStoreInstance();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/** The store everything that does not name one uses. */
|
|
546
|
+
export function getDefaultStore(): Store {
|
|
547
|
+
return defaultStore();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Read an atom out of a store, or out of the default store.
|
|
552
|
+
*
|
|
553
|
+
* The same value a component would see, with no component: this is how a route
|
|
554
|
+
* handler, an event handler outside React, or a test reads state.
|
|
555
|
+
*/
|
|
556
|
+
export function read<T>(target: ReadonlyAtom<T>, store?: Store): T {
|
|
557
|
+
return (store ?? defaultStore()).get(target);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Write an atom in a store, or in the default store. */
|
|
561
|
+
export function write<T, A>(target: WritableAtom<T, A>, argument: A, store?: Store): void {
|
|
562
|
+
(store ?? defaultStore()).set(target, argument);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Be told when an atom's value changes, outside React.
|
|
567
|
+
*
|
|
568
|
+
* Subscribing is also what mounts the atom, so an atom with an `onMount` is
|
|
569
|
+
* started by the first subscriber and stopped by the last — including when
|
|
570
|
+
* that subscriber is a component.
|
|
571
|
+
*/
|
|
572
|
+
export function subscribe<T>(
|
|
573
|
+
target: ReadonlyAtom<T>,
|
|
574
|
+
listener: () => void,
|
|
575
|
+
store?: Store,
|
|
576
|
+
): Unsubscribe {
|
|
577
|
+
return (store ?? defaultStore()).sub(target, listener);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Give a subtree its own store.
|
|
582
|
+
*
|
|
583
|
+
* With no `store`, the provider owns one it creates for itself, which is the
|
|
584
|
+
* shortest way to isolate a subtree — or a test — from everything else.
|
|
585
|
+
*/
|
|
586
|
+
export component Provider(store?: Store, children: React.Node) {
|
|
587
|
+
return <StoreScope store={store ?? null}>{children}</StoreScope>;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** The store this part of the tree reads: the scoped one, or the default. */
|
|
591
|
+
export hook useStore(store?: Store): Store {
|
|
592
|
+
return useStoreInstance(store);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Read an atom, re-rendering when — and only when — its value changes.
|
|
597
|
+
*
|
|
598
|
+
* A component that reads three atoms re-renders when any of the three changes
|
|
599
|
+
* and not when a fourth does, because the subscription is per atom rather than
|
|
600
|
+
* per store. A derived atom that recomputes to the value it already had does
|
|
601
|
+
* not re-render its readers at all.
|
|
602
|
+
*/
|
|
603
|
+
export hook useAtomValue<T>(target: ReadonlyAtom<T>, store?: Store): T {
|
|
604
|
+
const bound = useStoreInstance(store).bind(target);
|
|
605
|
+
// The server snapshot is the same read: a store holds its value on both
|
|
606
|
+
// sides, so hydration compares like with like instead of a placeholder.
|
|
607
|
+
return useSyncExternalStore(bound.subscribe, bound.snapshot, bound.snapshot);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Write an atom without reading it.
|
|
612
|
+
*
|
|
613
|
+
* A component that only dispatches does not subscribe, so it does not
|
|
614
|
+
* re-render when the value it writes changes. The setter's identity is stable
|
|
615
|
+
* for as long as the store and the atom are, so passing it to a memoised child
|
|
616
|
+
* costs that child nothing.
|
|
617
|
+
*/
|
|
618
|
+
export hook useSetAtom<T, A>(target: WritableAtom<T, A>, store?: Store): (argument: A) => void {
|
|
619
|
+
return useStoreInstance(store).bind(target).setter;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Read and write an atom, in the shape `useState` returns. */
|
|
623
|
+
export hook useAtom<T, A>(target: WritableAtom<T, A>, store?: Store): [T, (argument: A) => void] {
|
|
624
|
+
return [useAtomValue(target, store), useSetAtom(target, store)];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Put a resettable atom back, from a component.
|
|
629
|
+
*
|
|
630
|
+
* Jotai's `useResetAtom`. `useSetAtom(target)` already does this — the
|
|
631
|
+
* argument is [`RESET`] — and the difference is the shape of what comes back:
|
|
632
|
+
* `() => void` goes straight onto an `onClick`, where the setter needs a
|
|
633
|
+
* wrapper that supplies the symbol.
|
|
634
|
+
*
|
|
635
|
+
* It takes an atom whose write accepts `RESET` as well as a value, which is
|
|
636
|
+
* what [`atomWithReset`], [`atomWithDefault`] and [`atomWithStorage`] all
|
|
637
|
+
* return. A plain [`atom`] is refused, and refused at the call rather than
|
|
638
|
+
* with a runtime error, because its argument type does not include the symbol.
|
|
639
|
+
* [`atomWithAsyncStorage`]'s setter has a different value type, so it resets
|
|
640
|
+
* through `useSetAtom` — its own doc comment says so.
|
|
641
|
+
*/
|
|
642
|
+
export hook useResetAtom<T>(
|
|
643
|
+
target: WritableAtom<T, SetAction<T> | Reset>,
|
|
644
|
+
store?: Store,
|
|
645
|
+
): () => void {
|
|
646
|
+
const setter = useSetAtom<T, SetAction<T> | Reset>(target, store);
|
|
647
|
+
return () => {
|
|
648
|
+
setter(RESET);
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* A callback that can read and write any atom, and subscribes to none of them.
|
|
654
|
+
*
|
|
655
|
+
* Jotai's `useAtomCallback`. For the handler that needs the *current* value of
|
|
656
|
+
* something it does not render — a submit that reads a draft, an analytics
|
|
657
|
+
* call that reads a filter — where `useAtomValue` would re-render the
|
|
658
|
+
* component every time that value changed for a value it only ever looks at
|
|
659
|
+
* once.
|
|
660
|
+
*
|
|
661
|
+
* The `get` and `set` are the same pair a [`writableSelector`]'s write is
|
|
662
|
+
* given, resolved through the store this component is in. Reading through it
|
|
663
|
+
* records no dependency, because a handler runs outside any computation and
|
|
664
|
+
* there is nothing for one to be recorded against — which is the point: the
|
|
665
|
+
* component that holds this callback is subscribed to nothing.
|
|
666
|
+
*
|
|
667
|
+
* The identity of what comes back changes when `callback` does, so a handler
|
|
668
|
+
* written inline is a new function every render unless something memoizes it —
|
|
669
|
+
* which, inside a `component` the React Compiler compiled, it does. The same
|
|
670
|
+
* rule as every other hook that takes a function.
|
|
671
|
+
*/
|
|
672
|
+
export hook useAtomCallback<Args extends $ReadOnlyArray<mixed>, Result>(
|
|
673
|
+
callback: (get: Getter, set: Setter, ...args: Args) => Result,
|
|
674
|
+
store?: Store,
|
|
675
|
+
): (...args: Args) => Result {
|
|
676
|
+
const instance = useStoreInstance(store);
|
|
677
|
+
return (...args: Args) => callback(instance.get, instance.set, ...args);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Subscribe a component to a cell directly.
|
|
682
|
+
*
|
|
683
|
+
* The escape hatch to the layer below: a route loader hands out
|
|
684
|
+
* `@uniflowed/cell` cells, and a component that reads one should not have to
|
|
685
|
+
* wrap it in an atom to do so. A cell holds its own value, so no store is
|
|
686
|
+
* involved and the `store` argument the other hooks take would mean nothing.
|
|
687
|
+
*/
|
|
688
|
+
export hook useCell<T>(source: Cell<T>): T {
|
|
689
|
+
const bound = bindCell(source);
|
|
690
|
+
return useSyncExternalStore(bound.subscribe, bound.snapshot, bound.snapshot);
|
|
691
|
+
}
|