Server rendering
Use XState actors with Nuxt and server-rendered Vue.
XState v6 is in alpha
Server rendering runs setup() on the server. onMounted does not run there, so an actor created by useActorRef(...) or useActor(...) is never started during SSR: the server renders the initial snapshot, and the actor starts when the component mounts in the browser.
The initial snapshot is therefore what the server output and the first client render must agree on.
One actor per request
Never keep a running actor in a module that the server imports. On a server the module is shared by every request, so one user's cart, session or upload would be visible to the next.
// ❌ leaks state between users on the server
export const cartActor = createActor(cartMachine).start();Create the actor inside the component tree and share it with provide/inject instead. See Share actors. Module-scope actors are for browser-only code.
Render-only state on the server
When the server needs the state but not a running actor, compute it without starting anything.
import { getInitialSnapshot } from 'xstate';
const snapshot = getInitialSnapshot(checkoutMachine, { cartId });getInitialSnapshot(...) and getNextSnapshot(...) are pure: no effects, no invoked actors, no cleanup. They are the safe way to derive markup on the server.
Hydrating with a persisted snapshot
To resume the same state in the browser, serialize a persisted snapshot on the server and pass it as the snapshot option on the client.
// server
const actor = createActor(checkoutMachine, { input: { cartId } }).start();
const persisted = actor.getPersistedSnapshot();
actor.stop();In Nuxt, carry it across with useState(...), which is serialized into the payload:
<script setup lang="ts">
import { useActor } from '@xstate/vue';
const persisted = useState('checkout', () => getPersistedCheckout());
const { snapshot, send } = useActor(checkoutMachine, {
snapshot: persisted.value
});
</script>The values in the payload must be JSON-serializable, which is the same constraint persistence already imposes.
Avoid hydration mismatches
The server renders the initial or restored snapshot. The client must render the same thing on its first pass, or Vue will report a mismatch.
- Do not send events during
setup(). Send them fromonMountedor from user interaction. - Keep
Date.now(),Math.random()andwindowout of the initial context. Pass them asinput, or set them from an entry action after the actor starts. - Delayed transitions only run once the actor starts in the browser, so a state entered via
afteris never part of the server output.
Warning: A persisted snapshot that was serialized mid-request restarts its invoked actors on restore. A checkout restored inside
chargingwill charge again unless the machine guards against it with an idempotency key.
Browser-only work
Put browser APIs behind onMounted, or invoke them from actor logic. Actors only start on the client, so a callback actor that attaches a resize or media-element listener never runs on the server.