---
title: "Shared State"
description: "Shared state is observable, immutable-by-default state synced between the node side and every RPC client, surviving reconnects: a new RPC client gets the snapshot."
canonical_url: "https://devfra.me/guide/shared-state"
---
# Shared State

> Shared state is observable, immutable-by-default state synced between the node side and every RPC client, surviving reconnects: a new RPC client gets the snapshot.

Shared state is observable, immutable-by-default state synced between the node side and every RPC client, surviving reconnects: a new RPC client gets the snapshot.

## Overview

```mermaid {theme="zinc-light" theme-dark="zinc-dark"}
flowchart LR
  subgraph ClientA["RPC client A"]
    A["state.value()"]
  end
  subgraph Server["Node side"]
    S["state.mutate(fn)"]
  end
  subgraph ClientB["RPC client B"]
    B["state.value()"]
  end
  S <-->|RPC sync| A
  S <-->|RPC sync| B
```

## Creating state

A [scoped context](https://devfra.me/guide/scoped-context)'s `rpc.sharedState(key, options)` namespaces it:

```ts
import { defineDevframe } from 'devframe'

export default defineDevframe({
  id: 'my-tool',
  name: 'My Tool',
  async setup(ctx) {
    const my = ctx.scope('my-tool')

    const state = await my.rpc.sharedState('state', { // -> my-tool:state
      initialValue: {
        count: 0,
        items: [] as { id: string, name: string }[],
      },
    })

    console.log(state.value().count) // 0
  },
})
```

The scope prefixes `<devframe-id>:` (wrapping `ctx.rpc.sharedState.get(...)`), so pass a bare key.

## Reading

`state.value()` returns an immutable snapshot.

```ts
const current = state.value()
console.log(current.count)
// current.count = 1 // ✗ TypeScript error: snapshot is Immutable<T>
```

## Mutating

Pass a recipe to `state.mutate()`:

```ts
state.mutate((draft) => {
  draft.count += 1
  draft.items.push({ id: 'a', name: 'Alpha' })
})
```

Devframe applies the recipe to a draft. When Immer returns a new state reference, Devframe emits `updated` and broadcasts it to RPC clients. Explicit replacement objects also notify. With patches enabled, `updated` carries `SharedStatePatch[]`. Sync IDs remain recorded for unchanged recipes, so replays stay idempotent.

## Patches (advanced)

Enable patches for minimal network diffs; `updated` then carries `Patch[]`:

```ts
const state = await ctx.rpc.sharedState.get('my-tool:big-state', {
  initialValue: largeTree,
  /** sharedState-level enablePatches is opt-in: */
  sharedState: createSharedState({ initialValue: largeTree, enablePatches: true }),
})
```

## Subscribing

```ts
state.on('updated', (fullState, patches, syncId) => {
  // `patches` is populated only when enablePatches is set.
})
```

## Client-side access

The same key is on the browser RPC client, scoped identically; browser-side mutations round-trip the node side, keeping `state.value()` authoritative.

```ts
import { connectDevframe } from 'devframe/client'

const my = (await connectDevframe()).scope('my-tool')

const state = await my.rpc.sharedState('state') // -> my-tool:state

console.log(state.value().count)

state.mutate((draft) => {
  draft.count += 1
})
```

## Enumerating keys

Both sides expose `keys()` and `onKeyAdded`:

```ts
for (const key of ctx.rpc.sharedState.keys()) {
  console.log(key)
}

const unsubscribe = ctx.rpc.sharedState.onKeyAdded((key) => {
  console.log('new shared-state key:', key)
})
```

Protocol adapters (e.g. [MCP](https://devfra.me/guide/agent-native)) surface shared state as dynamic resources.

## Type-safe keys

Augment `DevframeRpcSharedStates` to type each key once; lookups stay typed:

```ts
declare module 'devframe' {
  interface DevframeRpcSharedStates {
    'my-tool:state': {
      count: number
      items: { id: string, name: string }[]
    }
  }
}
```

## When to use shared state vs RPC

| Use shared state for                                      | Use RPC for                                      |
| --------------------------------------------------------- | ------------------------------------------------ |
| Long-lived UI state (selections, filters, expanded nodes) | One-shot queries (`get-modules`, `read-file`)    |
| Coordination across RPC clients                           | Commands / actions with side effects             |
| Data that should reappear after reconnect                 | Event streams (prefer `broadcast` / `callEvent`) |

For actions and events, use `ctx.rpc.register` + `broadcast` ([RPC](https://devfra.me/guide/rpc)).


## Sitemap

See the full [sitemap](https://devfra.me/sitemap.md) for all pages.
