|
| 1 | +--- |
| 2 | +title: 'Persisting and restoring state in XState' |
| 3 | +description: Learn how to persist and restore your state machine state in XState |
| 4 | +tags: [] |
| 5 | +authors: [david] |
| 6 | +date: 2023-09-19 |
| 7 | +slug: 2023-09-19-persisting-state |
| 8 | +image: /blog/2023-09-19-persisting-state.png |
| 9 | +--- |
| 10 | + |
| 11 | +State machines have a useful feature: modeling state. But what happens when your user closes the browser and comes back later? Or when your server restarts? Or when your user logs out and logs back in? In this article, we'll learn how to persist and restore state in XState. |
| 12 | + |
| 13 | +Note: this article applies to XState v5 beta. |
| 14 | + |
| 15 | +## Quick reference |
| 16 | + |
| 17 | +If you're already familiar with XState, here's a quick reference for persisting and restoring state: |
| 18 | + |
| 19 | +```ts |
| 20 | +import { createActor } from 'xstate'; |
| 21 | +import { someMachine } from './someMachine'; |
| 22 | + |
| 23 | +// Get the state from localStorage (if it exists) |
| 24 | +const stateString = localStorage.getItem('some-state'); |
| 25 | + |
| 26 | +// Create the state from the string (if it exists) |
| 27 | +const restoredState = stateString ? JSON.parse(stateString) : undefined; |
| 28 | + |
| 29 | +const actor = createActor(machine, { |
| 30 | + // Restore the state (if it exists) |
| 31 | + state: restoredState, |
| 32 | +}); |
| 33 | + |
| 34 | +actor.subscribe(() => { |
| 35 | + // Persist the state to localStorage |
| 36 | + const persistedState = actor.getPersistedState(); |
| 37 | + localStorage.setItem('some-state', JSON.stringify(persistedState)); |
| 38 | +}); |
| 39 | + |
| 40 | +actor.start(); |
| 41 | +``` |
| 42 | + |
| 43 | +## Persisting and restoring state |
| 44 | + |
| 45 | +Let's say we have a state machine that represents a user's shopping cart: |
| 46 | + |
| 47 | +```ts |
| 48 | +import { createMachine, createActor, assign } from 'xstate'; |
| 49 | + |
| 50 | +const checkoutMachine = createMachine({ |
| 51 | + id: 'checkout', |
| 52 | + initial: 'shopping', |
| 53 | + context: { |
| 54 | + items: [], |
| 55 | + }, |
| 56 | + states: { |
| 57 | + shopping: { |
| 58 | + on: { |
| 59 | + 'item.add': { |
| 60 | + actions: assign({ |
| 61 | + items: ({ context, event }) => { |
| 62 | + return [...context.items, event.item]; |
| 63 | + }, |
| 64 | + }), |
| 65 | + }, |
| 66 | + checkout: 'checkingOut', |
| 67 | + }, |
| 68 | + }, |
| 69 | + checkingOut: { |
| 70 | + // ... |
| 71 | + }, |
| 72 | + }, |
| 73 | +}); |
| 74 | + |
| 75 | +const checkoutActor = createActor(machine, { |
| 76 | + // ... |
| 77 | +}); |
| 78 | +``` |
| 79 | + |
| 80 | +You may want to persist the state of this machine so that when the user comes back to the site, the items in their cart are still there, and the step in the checkout process is still the same. To do this, we need to remember to do two things: |
| 81 | + |
| 82 | +- **Persist the state** to some storage (e.g., localStorage, a database, etc.) |
| 83 | +- **Restore the state** from the storage when creating the actor |
| 84 | + |
| 85 | +First, you should determine your [persistence strategy](#persistence-strategies). For this example, we'll use [the `localStorage` API](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). |
| 86 | + |
| 87 | +Actors created with `createActor` have a `.getPersistedState()` method that returns the state that should be persisted. This state is a plain object that can be serialized to JSON using `JSON.stringify(persistedState)`. Note that this "persisted state" may be slightly different from the `snapshot` state returned from `actor.getSnapshot()`. Since the persisted state is a plain object, you can persist it to any storage that you want, as long as it can be restored as an object. |
| 88 | + |
| 89 | +```ts |
| 90 | +// Read the state to persist from the actor (sync) |
| 91 | +const persistedState = actor.getPersistedState(); |
| 92 | + |
| 93 | +// Persist the state to localStorage |
| 94 | +localStorage.setItem('some-state-key', JSON.stringify(persistedState)); |
| 95 | +``` |
| 96 | + |
| 97 | +Retrieve the persisted state from storage and restore it as an object when creating the actor: |
| 98 | + |
| 99 | +```ts |
| 100 | +const actor = createActor(machine, { |
| 101 | + // Restore the state (if it exists) |
| 102 | + state: restoredState, |
| 103 | +}); |
| 104 | + |
| 105 | +actor.start(); |
| 106 | +``` |
| 107 | + |
| 108 | +The actor will start at the `restoredState`, if it exists. If it is `undefined`, the actor will start at the initial state of the actor logic provided to `createActor(logic)`. |
| 109 | + |
| 110 | +## Persistence strategies |
| 111 | + |
| 112 | +For the browser, you can: |
| 113 | + |
| 114 | +- Use [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) |
| 115 | +- Use a client-side database like [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) |
| 116 | +- Use [cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) |
| 117 | +- Use [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) |
| 118 | +- Store it in runtime memory (e.g., a global variable or some other persistent store) |
| 119 | + |
| 120 | +For the server, you can: |
| 121 | + |
| 122 | +- Use a database (e.g., [MongoDB](https://www.mongodb.com/), [PostgreSQL](https://www.postgresql.org/), etc.) |
| 123 | +- Use a cache (e.g., [Redis](https://redis.io/)) |
| 124 | +- Use cookies or session storage |
| 125 | + |
| 126 | +## Persisting other kinds of actor logic |
| 127 | + |
| 128 | +TODO: all kinds of actor logic can be persisted, not just machines |
| 129 | + |
| 130 | +## Recursive persistence |
| 131 | + |
| 132 | +TODO: all invoked and spawned actors can be persisted, as long as they're named |
| 133 | + |
| 134 | +## Examples |
| 135 | + |
| 136 | +TODO: list example(s) here |
0 commit comments