Skip to Content
GuidesShared state

Shared state

@screenplay.space/state bridges a piece of your app’s state to the canvas. Change it in one person’s frame and every other viewer’s frame updates too. It’s useful for things like the signed-in user, the selected plan, or a billing toggle, so a whole team can look at the same screen in the same state.

You don’t need the package for the route and scroll position; every frame already shares those, and a frame follows another viewer’s route without reloading (see Frames). Use it for state the page keeps itself.

The route in the selected frame’s address bar shows a {} when shared state is present. Hover it to see the JSON. Screenplay doesn’t edit shared state from the canvas; it only displays it.

The agent knows this package through the screenplay-share-state skill. Try “share the billing toggle with the canvas”.

Install

npm install @screenplay.space/state

React 17 or later is a peer dependency.

Two-way sync

Pass the value and its setter to keep existing UI state in sync in both directions:

import { useState } from "react" import { useSharedState } from "@screenplay.space/state" export function BillingToggle() { const [annual, setAnnual] = useState(false) useSharedState("billing", annual ? "annual" : "monthly", (v) => setAnnual(v === "annual") ) return <Toggle value={annual} onChange={setAnnual} /> }

This is the demo app’s pricing page. Flip the toggle in one frame and every frame on /pricing flips with it.

Publish only

Leave out the setter to publish a value the canvas can see, but never write back:

const user = useUser() useSharedState("user", user ? { id: user.id, role: user.role } : null)

Share a whole store

If your app keeps its state in a zustand store, share the whole store with one call instead of one useSharedState per value:

import { create } from "zustand" import { shareStore } from "@screenplay.space/state" export const useSales = create((set) => ({ contactOpen: false, plan: "Growth", seats: 5, open: (plan) => set({ contactOpen: true, plan }), close: () => set({ contactOpen: false }), setSeats: (seats) => set({ seats }), })) shareStore("sales", useSales)

Open the contact-sales modal in one frame and pick 12 seats, and every other viewer’s frame shows the same modal with the same seats. The store’s data is shared under the one key; changes from other viewers are merged back with setState, so the store’s actions keep working everywhere.

Only fields that are plain JSON are shared. Actions, and fields holding a Date, Map, Set or class instance, stay local and are never overwritten. shareStore returns a function that stops sharing.

Without React

import { setSharedState, subscribeSharedState, clearSharedState, } from "@screenplay.space/state" const remove = setSharedState("session", { id: "u_1", role: "admin" }) const unsubscribe = subscribeSharedState("session", (next) => { console.log("session changed", next) }) remove() // or clearSharedState("session") unsubscribe()

On a mockup

A mockup’s page has no bundler, so it doesn’t install the package. It already has a global screenplay.shareState(key, initial, onChange) that follows the same rules. onChange runs at once with the current value and again on every change, your own set included, so one function can draw the page:

<script> const tab = screenplay.shareState("tab", "overview", (value) => { document.body.dataset.tab = value }) document.querySelector("#plans").onclick = () => tab.set("plans") </script>

You won’t usually write this yourself: the chat that drew the mockup adds it when you ask for a page that remembers what people clicked.

Rules

  • Values must be JSON. Functions, class instances, undefined, and circular references are dropped.
  • The combined state is capped at 64 KB. Larger updates are skipped with a console warning.
  • State is kept with the frame across reloads and carries into play mode. Keep keys stable, like ids: renaming a key drops the old entry.
  • The room’s state wins when a frame loads. A frame that loads or reloads first asks the canvas for the room’s state and waits for the answer before publishing, so its initial values don’t overwrite what other viewers picked. Keys the room doesn’t have yet take the frame’s value. If the canvas doesn’t answer within half a second, the frame publishes anyway.
  • Like knobs, it’s development-only and does nothing outside a Screenplay frame, so it’s safe to ship. It works in Vite and other bundlers with no setup, like knobs.
Last updated on