@systemfsoftware/xstate-store 0.0.0-stage → 5.0.0-beta.6
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/CHANGELOG.md +7 -0
- package/LICENSE +22 -0
- package/README.md +526 -2
- package/dist/index.d.ts +147 -0
- package/dist/index.mjs +70 -0
- package/dist/persist.d.ts +256 -0
- package/dist/persist.mjs +630 -0
- package/dist/reset.d.ts +51 -0
- package/dist/reset.mjs +61 -0
- package/dist/store-BMZdYhoE.mjs +755 -0
- package/dist/types-0n0RpfhK.d.ts +442 -0
- package/dist/undo.d.ts +77 -0
- package/dist/undo.mjs +222 -0
- package/dist/validate.d.ts +35 -0
- package/dist/validate.mjs +138 -0
- package/dist/validationError-Ndy-qk0W.mjs +6 -0
- package/package.json +80 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @systemfsoftware/xstate-store
|
|
2
|
+
|
|
3
|
+
# @systemfsoftware/xstate-store@5.0.0-beta.6
|
|
4
|
+
|
|
5
|
+
`createStore` accepts a `warn` option — a `(message: string) => void` sink for non-fatal runtime warnings from the store and its extensions, defaulting to `console.warn`. The `validateSchemas` extension now reports its "no schemas to validate" development warning through this sink, so callers can observe or silence it without patching the process-wide `console`.
|
|
6
|
+
|
|
7
|
+
Add `@systemfsoftware/xstate-store`, the event-driven store, forked from statelyai/xstate's `next` branch at 2146ae26 (MIT). It behaves like upstream's 5.0.0-alpha.5. Under `exactOptionalPropertyTypes`, optional members that can hold `undefined` declare it explicitly. It has no `effect` dependency or peer: the Effect 4.0.1 peer floor applies only to `@systemfsoftware/xstate-effect` and `@systemfsoftware/xstate-test`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015 David Khourshid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -1,3 +1,527 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@xstate/store`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> `@systemfsoftware/xstate-store` is forked from [statelyai/xstate](https://github.com/statelyai/xstate) (`packages/xstate-store`, branch `next` at `2146ae26ebfc7e6a624b3a1f237f9e6ddc30b9f5`), MIT. The text below is upstream's.
|
|
4
|
+
|
|
5
|
+
XState Store is a library for **simple event-based state management**. If you want a state management library that allows you to update a store's state via events, `@xstate/store` is a great option. If you need more complex application logic needs, like state machines/statecharts, effects, communicating actors, and more, consider [using XState instead](https://github.com/statelyai/xstate).
|
|
6
|
+
|
|
7
|
+
- **Extremely simple**: transitions update state via events, just like Redux, Zustand, Pinia, etc.
|
|
8
|
+
- **Extremely small**: less than 1kb minified/gzipped
|
|
9
|
+
- **XState compatible**: use it with (or without) XState, or convert to XState machines when you need to handle more complex logic & effects. `fromStore()` requires XState v6; with XState v5, use `@xstate/store@4`.
|
|
10
|
+
- **Extra type-safe**: great typing out of the box, with strong inference and no awkwardness.
|
|
11
|
+
|
|
12
|
+
> [!NOTE]
|
|
13
|
+
> This readme is written for [TypeScript](#typescript) users. If you are a JavaScript user, just remove the types.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
nix build github:systemfsoftware/xstate#xstate-store
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
See the root README's [Install](../../README.md#install) section for wiring a catalog entry. Its peer `@systemfsoftware/xstate` comes from the same flake (`#xstate`).
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createStore } from '@xstate/store'
|
|
27
|
+
|
|
28
|
+
export const donutStore = createStore({
|
|
29
|
+
context: {
|
|
30
|
+
donuts: 0,
|
|
31
|
+
favoriteFlavor: 'chocolate',
|
|
32
|
+
},
|
|
33
|
+
on: {
|
|
34
|
+
addDonut: (context) => ({
|
|
35
|
+
...context,
|
|
36
|
+
donuts: context.donuts + 1,
|
|
37
|
+
}),
|
|
38
|
+
changeFlavor: (context, event: { flavor: string }) => ({
|
|
39
|
+
...context,
|
|
40
|
+
favoriteFlavor: event.flavor,
|
|
41
|
+
}),
|
|
42
|
+
eatAllDonuts: (context) => ({
|
|
43
|
+
...context,
|
|
44
|
+
donuts: 0,
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
donutStore.subscribe((snapshot) => {
|
|
50
|
+
console.log(snapshot.context)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Equivalent to
|
|
54
|
+
// donutStore.send({ type: 'addDonut' });
|
|
55
|
+
donutStore.trigger.addDonut()
|
|
56
|
+
// => { donuts: 1, favoriteFlavor: 'chocolate' }
|
|
57
|
+
|
|
58
|
+
// donutStore.send({
|
|
59
|
+
// type: 'changeFlavor',
|
|
60
|
+
// flavor: 'strawberry' // Strongly-typed!
|
|
61
|
+
// });
|
|
62
|
+
donutStore.trigger.changeFlavor({ flavor: 'strawberry' })
|
|
63
|
+
// => { donuts: 1, favoriteFlavor: 'strawberry' }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Checking events
|
|
67
|
+
|
|
68
|
+
<!-- store.can API from packages/xstate-store/src/types.ts -->
|
|
69
|
+
|
|
70
|
+
Use `store.can` to check whether an event is allowed without updating the store:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const store = createStore({
|
|
74
|
+
context: { count: 0 },
|
|
75
|
+
on: {
|
|
76
|
+
increment: (context, event: { by: number }) => {
|
|
77
|
+
if (context.count + event.by > 10) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
count: context.count + event.by,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
store.can.increment({ by: 4 })
|
|
89
|
+
// => true
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Returning `undefined` marks the event as not allowed. Returning the same context
|
|
93
|
+
object is still allowed, and transitions that enqueue effects are allowed.
|
|
94
|
+
|
|
95
|
+
## Persistence
|
|
96
|
+
|
|
97
|
+
<!-- persist and flushStorage behavior from src/persist.ts -->
|
|
98
|
+
|
|
99
|
+
Use the `persist` extension to save state after events are committed. Calling
|
|
100
|
+
`store.can` or the pure `store.transition` method does not schedule writes.
|
|
101
|
+
Writes preserve commit order, including events triggered synchronously by effects
|
|
102
|
+
or subscriptions. A newer eligible event can supersede an older pending write;
|
|
103
|
+
`onDone` runs for writes actually performed. Asynchronous writes execute in event
|
|
104
|
+
order for each store. Call
|
|
105
|
+
`await flushStorage(store)` to write buffered changes and wait for queued writes
|
|
106
|
+
to finish. Synchronous storage adapters continue to write synchronously.
|
|
107
|
+
|
|
108
|
+
With throttling, `pick` runs once per write on the latest buffered context.
|
|
109
|
+
`clearStorage(store)` cancels buffered writes and removes saved data after any
|
|
110
|
+
already queued writes finish; await its result when using asynchronous storage.
|
|
111
|
+
New events sent after clearing can persist new state. For asynchronous storage,
|
|
112
|
+
call `await rehydrateStore(store)` to load saved state; initial read failures are
|
|
113
|
+
reported through `onError`.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { flushStorage, persist } from '@xstate/store/persist'
|
|
117
|
+
|
|
118
|
+
const savedDonutStore = donutStore.with(persist({ name: 'donuts' }))
|
|
119
|
+
savedDonutStore.trigger.addDonut()
|
|
120
|
+
await flushStorage(savedDonutStore)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Undo and persistence
|
|
124
|
+
|
|
125
|
+
<!-- snapshot undo restoration behavior from src/undo.ts -->
|
|
126
|
+
|
|
127
|
+
Snapshot-based `undoRedo` restores historical state while preserving live
|
|
128
|
+
extension metadata, including persistence hydration. A custom `restore` callback
|
|
129
|
+
can enqueue events; their updated extension metadata is preserved too. Apply
|
|
130
|
+
`persist` after `undoRedo` when undo and redo themselves should write to storage:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { undoRedo } from '@xstate/store/undo'
|
|
134
|
+
|
|
135
|
+
const undoableDonutStore = donutStore
|
|
136
|
+
.with(undoRedo({ strategy: 'snapshot' }))
|
|
137
|
+
.with(persist({ name: 'undoable-donuts' }))
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Atom updates and subscriptions
|
|
141
|
+
|
|
142
|
+
<!-- writable updater tracking and subscriber errors from src/atom.ts -->
|
|
143
|
+
|
|
144
|
+
Reads inside a writable atom's `set(previous => next)` updater do not create
|
|
145
|
+
reactive dependencies. Use a computed atom getter to track other atoms instead.
|
|
146
|
+
If a synchronous subscriber throws, other queued subscribers still receive their
|
|
147
|
+
notifications before the first error is rethrown to the caller. The atom's value
|
|
148
|
+
has already changed; later updates continue to notify subscribers normally.
|
|
149
|
+
|
|
150
|
+
## Async atoms
|
|
151
|
+
|
|
152
|
+
<!-- createAsyncAtom dependency and cancellation behavior from src/atom.ts -->
|
|
153
|
+
|
|
154
|
+
`createAsyncAtom` loads a value lazily and exposes a `pending`, `done`, or
|
|
155
|
+
`error` state. Atoms read synchronously by its getter remain dependencies after
|
|
156
|
+
the request succeeds or fails. When a dependency changes, subscribed async atoms
|
|
157
|
+
reload; otherwise, they reload on the next read. Read dependencies before the
|
|
158
|
+
first `await` to track them.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { createAsyncAtom, createAtom } from '@xstate/store'
|
|
162
|
+
|
|
163
|
+
const userId = createAtom('ada')
|
|
164
|
+
const user = createAsyncAtom(async ({ signal }) => {
|
|
165
|
+
const id = userId.get()
|
|
166
|
+
const response = await fetch(`/users/${id}`, { signal })
|
|
167
|
+
return response.json()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
user.subscribe((state) => {
|
|
171
|
+
if (state.status === 'done') {
|
|
172
|
+
console.log(state.data)
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
userId.set('grace') // Reloads even after the previous request settled.
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Recomputing aborts the previous getter's signal and ignores its stale result.
|
|
180
|
+
|
|
181
|
+
## Usage with React
|
|
182
|
+
|
|
183
|
+
Import `useSelector` from `@xstate/store-react`. Select the data you want via `useSelector(…)` and send events using `store.send(eventObject)`:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
import { useSelector } from '@xstate/store-react'
|
|
187
|
+
import { donutStore } from './donutStore.ts'
|
|
188
|
+
|
|
189
|
+
function DonutCounter() {
|
|
190
|
+
const donutCount = useSelector(donutStore, (state) => state.context.donuts)
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div>
|
|
194
|
+
<button onClick={() => donutStore.send({ type: 'addDonut' })}>
|
|
195
|
+
Add donut ({donutCount})
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Usage with SolidJS
|
|
203
|
+
|
|
204
|
+
Import `useSelector` from `@xstate/store-solid`. Select the data you want via `useSelector(…)` and send events using `store.send(eventObject)`:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { useSelector } from '@xstate/store-solid'
|
|
208
|
+
import { donutStore } from './donutStore.ts'
|
|
209
|
+
|
|
210
|
+
function DonutCounter() {
|
|
211
|
+
const donutCount = useSelector(donutStore, (state) => state.context.donuts)
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div>
|
|
215
|
+
<button onClick={() => donutStore.send({ type: 'addDonut' })}>
|
|
216
|
+
Add donut ({donutCount()})
|
|
217
|
+
</button>
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Usage with Immer
|
|
224
|
+
|
|
225
|
+
XState Store works well with immutable update libraries like [Immer](https://github.com/immerjs/immer) or [Mutative](https://github.com/unadlib/mutative). Use `produce(...)` inside your transition functions:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { createStore } from '@xstate/store'
|
|
229
|
+
import { produce } from 'immer' // or { create } from 'mutative'
|
|
230
|
+
|
|
231
|
+
const donutStore = createStore({
|
|
232
|
+
context: {
|
|
233
|
+
donuts: 0,
|
|
234
|
+
favoriteFlavor: 'chocolate',
|
|
235
|
+
},
|
|
236
|
+
on: {
|
|
237
|
+
addDonut: (context) =>
|
|
238
|
+
produce(context, (draft) => {
|
|
239
|
+
draft.donuts++
|
|
240
|
+
}),
|
|
241
|
+
changeFlavor: (context, event: { flavor: string }) =>
|
|
242
|
+
produce(context, (draft) => {
|
|
243
|
+
draft.favoriteFlavor = event.flavor
|
|
244
|
+
}),
|
|
245
|
+
eatAllDonuts: (context) =>
|
|
246
|
+
produce(context, (draft) => {
|
|
247
|
+
draft.donuts = 0
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
If a transition should be unavailable, return `undefined` from the transition
|
|
254
|
+
before calling `produce(...)`:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
on: {
|
|
258
|
+
eatDonut: ;
|
|
259
|
+
;((context) => {
|
|
260
|
+
if (context.donuts === 0) {
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return produce(context, (draft) => {
|
|
265
|
+
draft.donuts--
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Immer treats a producer that returns `undefined` the same as a producer that
|
|
272
|
+
does not explicitly return anything. If you need `produce(...)` itself to return
|
|
273
|
+
`undefined`, return Immer's `nothing` token from the producer:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { nothing, produce } from 'immer'
|
|
277
|
+
|
|
278
|
+
on: {
|
|
279
|
+
eatDonut: ;
|
|
280
|
+
;((context) =>
|
|
281
|
+
produce(context, (draft) => {
|
|
282
|
+
if (draft.donuts === 0) {
|
|
283
|
+
return nothing
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
draft.donuts--
|
|
287
|
+
}))
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## TypeScript
|
|
292
|
+
|
|
293
|
+
XState Store is written in TypeScript and provides full type safety, _without_ you having to specify generic type parameters. The `context` type is inferred from the initial context object, and the event types are inferred from the event object payloads you provide in the transition functions.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { createStore } from '@xstate/store'
|
|
297
|
+
|
|
298
|
+
const donutStore = createStore({
|
|
299
|
+
// Context inferred as:
|
|
300
|
+
// {
|
|
301
|
+
// donuts: number;
|
|
302
|
+
// favoriteFlavor: string;
|
|
303
|
+
// }
|
|
304
|
+
context: {
|
|
305
|
+
donuts: 0,
|
|
306
|
+
favoriteFlavor: 'chocolate',
|
|
307
|
+
},
|
|
308
|
+
on: {
|
|
309
|
+
// Event inferred as:
|
|
310
|
+
// {
|
|
311
|
+
// type: 'changeFlavor';
|
|
312
|
+
// flavor: string;
|
|
313
|
+
// }
|
|
314
|
+
changeFlavor: (context, event: { flavor: string }) => {
|
|
315
|
+
context.favoriteFlavor = event.flavor
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
donutStore.getSnapshot().context.favoriteFlavor // string
|
|
321
|
+
donutStore.get().context.favoriteFlavor // same snapshot, readable/tracked read
|
|
322
|
+
|
|
323
|
+
donutStore.send({
|
|
324
|
+
type: 'changeFlavor', // Strongly-typed from transition key
|
|
325
|
+
flavor: 'strawberry', // Strongly-typed from { flavor: string }
|
|
326
|
+
})
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
If you want to provide event or emitted-event types explicitly, you can use `schemas` with any library that implements the [Standard Schema](https://standardschema.dev/) interface. Schemas define the store's runtime-readable contract: the shape of its context, accepted events, and emitted events. Store uses schemas for type inference and metadata by default; it does not validate schema-declared values unless you opt in with `validateSchemas()`.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { createStore } from '@xstate/store'
|
|
333
|
+
import { z } from 'zod'
|
|
334
|
+
|
|
335
|
+
const store = createStore({
|
|
336
|
+
schemas: {
|
|
337
|
+
context: z.object({
|
|
338
|
+
donuts: z.number(),
|
|
339
|
+
favoriteFlavor: z.string(),
|
|
340
|
+
}),
|
|
341
|
+
events: {
|
|
342
|
+
changeFlavor: z.object({
|
|
343
|
+
flavor: z.string(),
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
emitted: {
|
|
347
|
+
flavorChanged: z.object({
|
|
348
|
+
flavor: z.string(),
|
|
349
|
+
}),
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
context: {
|
|
353
|
+
donuts: 0,
|
|
354
|
+
favoriteFlavor: 'chocolate',
|
|
355
|
+
},
|
|
356
|
+
on: {
|
|
357
|
+
changeFlavor: (context, event, enqueue) => {
|
|
358
|
+
enqueue.emit.flavorChanged({ flavor: event.flavor })
|
|
359
|
+
return {
|
|
360
|
+
...context,
|
|
361
|
+
favoriteFlavor: event.flavor,
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Event and emitted-event schemas describe payload objects. Use an empty object
|
|
369
|
+
schema for events without payload:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
schemas: {
|
|
373
|
+
events: {
|
|
374
|
+
reset: z.object({})
|
|
375
|
+
},
|
|
376
|
+
emitted: {
|
|
377
|
+
reset: z.object({})
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Validating schemas
|
|
383
|
+
|
|
384
|
+
Use the `validateSchemas()` extension when the store should validate its schema
|
|
385
|
+
contract at runtime:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { createStore } from '@xstate/store'
|
|
389
|
+
import { validateSchemas } from '@xstate/store/validate'
|
|
390
|
+
import { z } from 'zod'
|
|
391
|
+
|
|
392
|
+
const store = createStore({
|
|
393
|
+
schemas: {
|
|
394
|
+
context: z.object({
|
|
395
|
+
count: z.number(),
|
|
396
|
+
}),
|
|
397
|
+
events: {
|
|
398
|
+
increment: z.object({
|
|
399
|
+
by: z.number(),
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
emitted: {
|
|
403
|
+
increased: z.object({
|
|
404
|
+
by: z.number(),
|
|
405
|
+
}),
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
context: { count: 0 },
|
|
409
|
+
on: {
|
|
410
|
+
increment: (context, event, enqueue) => {
|
|
411
|
+
enqueue.emit.increased({ by: event.by })
|
|
412
|
+
return { count: context.count + event.by }
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
}).with(validateSchemas())
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`validateSchemas()` validates store macrosteps. It validates the event sent to
|
|
419
|
+
the store, the final context after the transition completes, and emitted events
|
|
420
|
+
before any effects execute. Events queued internally with `enqueue.trigger` are
|
|
421
|
+
processed as part of the same macrostep; their payloads are not separately
|
|
422
|
+
validated in this version.
|
|
423
|
+
|
|
424
|
+
Invalid `send(...)`, `trigger.*(...)`, or `transition(...)` calls throw a
|
|
425
|
+
`StoreValidationError`. `store.can.*(...)` always returns a boolean; validation
|
|
426
|
+
errors make it return `false`.
|
|
427
|
+
|
|
428
|
+
By default, unknown events and emitted events throw when the corresponding
|
|
429
|
+
schema map exists. Extension-added event types are treated as known, even when
|
|
430
|
+
they do not have payload schemas. You can opt out:
|
|
431
|
+
|
|
432
|
+
```ts
|
|
433
|
+
const store = createStore({
|
|
434
|
+
// ...
|
|
435
|
+
}).with(
|
|
436
|
+
validateSchemas({
|
|
437
|
+
unknownEvents: 'ignore',
|
|
438
|
+
unknownEmitted: 'ignore',
|
|
439
|
+
}),
|
|
440
|
+
)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Use `store.getSnapshot()` when you want an explicit snapshot read from the store itself. Use `store.get()` when consuming the store as a `Readable` value in tracked or reactive code.
|
|
444
|
+
|
|
445
|
+
If you want to make the `context` type more specific, you can strongly type the `context` outside of `createStore(…)` and pass it in:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
import { createStore } from '@xstate/store'
|
|
449
|
+
|
|
450
|
+
interface DonutContext {
|
|
451
|
+
donuts: number
|
|
452
|
+
favoriteFlavor: 'chocolate' | 'strawberry' | 'blueberry'
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const donutContext: DonutContext = {
|
|
456
|
+
donuts: 0,
|
|
457
|
+
favoriteFlavor: 'chocolate',
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const donutStore = createStore({
|
|
461
|
+
context: donutContext,
|
|
462
|
+
on: {
|
|
463
|
+
// ... (transitions go here)
|
|
464
|
+
},
|
|
465
|
+
})
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Effects and Side Effects
|
|
469
|
+
|
|
470
|
+
You can enqueue effects in state transitions using the `enqueue` argument:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { createStore } from '@xstate/store'
|
|
474
|
+
|
|
475
|
+
const store = createStore({
|
|
476
|
+
context: { count: 0 },
|
|
477
|
+
on: {
|
|
478
|
+
incrementDelayed: (context, event, enqueue) => {
|
|
479
|
+
enqueue.effect(async () => {
|
|
480
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
481
|
+
store.send({ type: 'increment' })
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
return context
|
|
485
|
+
},
|
|
486
|
+
increment: (context) => ({
|
|
487
|
+
...context,
|
|
488
|
+
count: context.count + 1,
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
})
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
## Emitting Events
|
|
495
|
+
|
|
496
|
+
You can emit events from transitions by declaring them in `schemas.emitted` and using `enqueue.emit`:
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
import { createStore } from '@xstate/store'
|
|
500
|
+
import { z } from 'zod'
|
|
501
|
+
|
|
502
|
+
const store = createStore({
|
|
503
|
+
schemas: {
|
|
504
|
+
emitted: {
|
|
505
|
+
increased: z.object({
|
|
506
|
+
by: z.number(),
|
|
507
|
+
}),
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
context: { count: 0 },
|
|
511
|
+
on: {
|
|
512
|
+
inc: (context, event: { by: number }, enqueue) => {
|
|
513
|
+
enqueue.emit.increased({ by: event.by })
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
...context,
|
|
517
|
+
count: context.count + event.by,
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// Listen for emitted events
|
|
524
|
+
store.on('increased', (event) => {
|
|
525
|
+
console.log(`Count increased by ${event.by}`)
|
|
526
|
+
})
|
|
527
|
+
```
|