Shared State
DevTools Kit provides a built-in shared state system for synchronizing data between server and clients. Changes made on either side are automatically propagated to all connected parties.
Overview
Server-Side Usage
Creating Shared State
Use ctx.rpc.sharedState.get() to create or access shared state:
const plugin: Plugin = {
devtools: {
async setup(ctx) {
const state = await ctx.rpc.sharedState.get('my-plugin:state', {
initialValue: {
count: 0,
items: [],
settings: { theme: 'dark' },
},
})
// Read current value
console.log(state.value())
// => { count: 0, items: [], settings: { theme: 'dark' } }
}
}
}Reading State
const state = await ctx.rpc.sharedState.get('my-plugin:state', {
initialValue: { count: 0 },
})
// Get current value
const current = state.value()
console.log(current.count) // 0Mutating State
Use state.mutate() to update the state. Changes are automatically synced to all clients:
// Mutate with a function (recommended)
state.mutate((draft) => {
draft.count += 1
draft.items.push({ id: 1, name: 'New item' })
})
// The mutation function receives a mutable draft
// Changes are batched and synced automaticallyExample: Real-Time Updates
const plugin: Plugin = {
devtools: {
async setup(ctx) {
const state = await ctx.rpc.sharedState.get('my-plugin:state', {
initialValue: { modules: [], lastUpdate: 0 },
})
// Update state when Vite processes modules
ctx.viteServer?.watcher.on('change', (file) => {
state.mutate((draft) => {
draft.modules.push(file)
draft.lastUpdate = Date.now()
})
})
}
}
}Client-Side Usage
Accessing Shared State
Use client.rpc.sharedState.get() to access the shared state:
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
const client = await getDevToolsRpcClient()
const state = await client.rpc.sharedState.get('my-plugin:state')
// Read current value
console.log(state.value())Subscribing to Changes
Use state.on('updated', ...) to react to state changes:
const state = await client.rpc.sharedState.get('my-plugin:state')
// Initial value
console.log(state.value()) // { count: 0 }
// Subscribe to updates
state.on('updated', (newState) => {
console.log('State updated:', newState)
// { count: 1 } - after server mutation
})Framework Integration
Vue
Create a reactive ref that syncs with shared state:
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
import { shallowRef } from 'vue'
export async function useSharedState<T>(name: string) {
const client = await getDevToolsRpcClient()
const sharedState = await client.rpc.sharedState.get<T>(name)
const state = shallowRef(sharedState.value())
sharedState.on('updated', (newState) => {
state.value = newState
})
return state
}
// Usage in component
const state = await useSharedState('my-plugin:state')
// `state` is now reactive and auto-updatesVue Composable (Full Example)
<script setup lang="ts">
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
import { onMounted, shallowRef } from 'vue'
interface PluginState {
count: number
items: string[]
}
const state = shallowRef<PluginState | null>(null)
onMounted(async () => {
const client = await getDevToolsRpcClient()
const shared = await client.rpc.sharedState.get<PluginState>('my-plugin:state')
state.value = shared.value()
shared.on('updated', (newState) => {
state.value = newState
})
})
</script>
<template>
<div v-if="state">
<p>Count: {{ state.count }}</p>
<ul>
<li v-for="item in state.items" :key="item">
{{ item }}
</li>
</ul>
</div>
</template>React
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
import { useEffect, useState } from 'react'
function useSharedState<T>(name: string, fallback: T) {
const [state, setState] = useState<T>(fallback)
useEffect(() => {
let mounted = true
async function init() {
const client = await getDevToolsRpcClient()
const sharedState = await client.rpc.sharedState.get<T>(name)
if (mounted) {
setState(sharedState.value() ?? fallback)
sharedState.on('updated', (newState) => {
if (mounted) {
setState(newState)
}
})
}
}
init()
return () => {
mounted = false
}
}, [name])
return state
}
// Usage
function MyComponent() {
const state = useSharedState('my-plugin:state', { count: 0 })
return (
<div>
Count:
{state.count}
</div>
)
}Svelte
<script lang="ts">
import { onMount } from 'svelte'
import { writable } from 'svelte/store'
import { getDevToolsRpcClient } from '@vitejs/devtools-kit/client'
interface PluginState {
count: number
}
const state = writable<PluginState>({ count: 0 })
onMount(async () => {
const client = await getDevToolsRpcClient()
const sharedState = await client.rpc.sharedState.get<PluginState>('my-plugin:state')
state.set(sharedState.value())
sharedState.on('updated', (newState) => {
state.set(newState)
})
})
</script>
<p>Count: {$state.count}</p>Type Safety
Extend DevToolsRpcSharedStates for type-safe shared state:
// src/types.ts
import '@vitejs/devtools-kit'
interface MyPluginState {
count: number
items: Array<{ id: string, name: string }>
settings: {
theme: 'light' | 'dark'
notifications: boolean
}
}
declare module '@vitejs/devtools-kit' {
interface DevToolsRpcSharedStates {
'my-plugin:state': MyPluginState
}
}Now TypeScript will validate your state access:
const state = await ctx.rpc.sharedState.get('my-plugin:state', {
initialValue: {
count: 0,
items: [],
settings: { theme: 'dark', notifications: true },
},
})
// ✓ Type-checked
state.mutate((draft) => {
draft.count += 1
draft.settings.theme = 'light'
})
// ✗ Error: 'invalid' is not assignable to 'light' | 'dark'
state.mutate((draft) => {
draft.settings.theme = 'invalid'
})Best Practices
Use Namespaced Keys
Prefix state keys with your plugin name to avoid collisions:
// Good
'my-plugin:state'
'my-plugin:settings'
// Bad - may conflict with other plugins
'state'
'settings'Keep State Serializable
Shared state must be JSON-serializable. Avoid:
// ✗ Bad - functions can't be serialized
{
count: 0,
increment: () => this.count++
}
// ✗ Bad - circular references
const obj = { child: null }
obj.child = obj
// ✓ Good - plain data
{
count: 0,
items: [{ id: 1, name: 'Item' }]
}Batch Updates
When making multiple changes, use a single mutate call:
// ✓ Good - single sync event
state.mutate((draft) => {
draft.count += 1
draft.lastUpdate = Date.now()
draft.items.push(newItem)
})
// ✗ Bad - multiple sync events
state.mutate((d) => {
d.count += 1
})
state.mutate((d) => {
d.lastUpdate = Date.now()
})
state.mutate((d) => {
d.items.push(newItem)
})Consider State Size
Large state objects may impact performance. For large datasets, consider:
// Instead of storing all data in shared state
{
allModules: [...thousands of modules...]
}
// Store just IDs and fetch details via RPC
{
moduleIds: ['a', 'b', 'c'],
selectedModule: 'a'
}
// Use RPC to fetch full module data
const module = await rpc.call('my-plugin:get-module', state.selectedModule)