How State Persists in escrcpy's Pinia Stores: A Dual-Layer Approach

escrcpy implements a dual persistence strategy that combines global pinia-plugin-persistedstate for automatic localStorage serialization with a custom Electron preload store wrapping electron-store for cross-process data durability.

The open-source Android screen mirroring application viarotel-org/escrcpy manages complex state across its Electron-Vue architecture using two complementary persistence mechanisms. By understanding how state persists in escrcpy's Pinia stores, developers can implement robust data retention strategies that handle both transient UI preferences and critical configuration data surviving application updates.

Automatic Persistence with pinia-plugin-persistedstate

escrcpy registers the pinia-plugin-persistedstate plugin globally to eliminate boilerplate when persisting routine UI state. This mechanism automatically serializes reactive store data to the browser's localStorage on every mutation and restores it when the application initializes.

Global Plugin Configuration

In desktop/src/store/index.js, the application creates the Pinia instance and installs the persistence plugin before mounting to the Vue application:

// desktop/src/store/index.js – global Pinia setup
import { createPinia } from 'pinia'
import persistedState from 'pinia-plugin-persistedstate'

let store
export default {
  install(app) {
    if (!store) store = createPinia()
    store.use(persistedState)          // <‑‑ enables automatic persistence
    app.use(store)
  },
}

What Gets Persisted Automatically

Any Pinia store that does not explicitly opt out of persistence automatically benefits from this mechanism. For example, the preference store at desktop/src/store/preference/index.js relies entirely on the plugin, requiring no manual persistence logic. The plugin intercepts state mutations and writes JSON to localStorage by default, making it ideal for sidebar positions, toggle states, and other ephemeral UI preferences.

Cross-Process Persistence via Electron Store

For data that must bridge the Electron main and renderer processes, escrcpy implements a manual persistence layer through the preload script. This approach ensures critical configuration survives renderer reloads, application updates, and remains accessible to the main process for native operations.

The Preload Store Architecture

The window.$preload.store object exposes the electron-store API to renderer processes. Unlike localStorage, which is confined to the renderer and clears during updates, this wrapper writes JSON to the user's application data directory. The main process can read these values directly, enabling two-way synchronization for settings like external binary paths and device configurations.

Device Store Implementation

The device store (desktop/src/store/device/index.js) demonstrates manual persistence by reading and writing device configurations through the Electron store:

// desktop/src/store/device/index.js – uses Electron store for device data
const $electronStore = window.$preload.store

export const useDeviceStore = defineStore('app-device', () => {
  const list = ref([])
  const config = ref({})

  function init() {
    // Load persisted config from electron‑store
    config.value = { ...( $electronStore.get('device') || {} ) }
    return config.value
  }

  function setRemark(deviceId, value) {
    // Write back to electron‑store; changes are reflected across processes
    $electronStore.set(['device', deviceId, 'remark'], value)
    init()
  }

  return { list, config, init, setRemark }
})

The init() function hydrates the store from the filesystem on startup, while setRemark() demonstrates how specific keys are written back to electron-store using dot-notation paths that organize data hierarchically.

Theme Store Synchronization

Similarly, the theme store (desktop/src/store/theme/index.js) synchronizes UI theme values between processes using the preload store. This ensures the main process can apply native window theming that matches the renderer's selected theme:

// desktop/src/store/theme/index.js – syncs UI theme via electron‑store
export const useThemeStore = defineStore('app-theme', () => {
  const value = ref(window.$preload.store.get('common.theme') || 'system')
  const isDark = ref(window.$preload.store.get('common.isDark') || false)

  async function init(changeValue = value.value) {
    value.value = changeValue
    await invokeAppTheme('update', changeValue)
    isDark.value = await invokeAppTheme('isDark')
    window.$preload.store.set('common.isDark', isDark.value)
    await updateHtml(changeValue)
  }

  // …watchers that react to electron‑store changes…
})

Here, the store initializes its reactive refs from electron-store keys (common.theme, common.isDark) rather than localStorage, ensuring the theme persists across application updates and remains consistent with main-process window controls.

Why escrcpy Uses Two Persistence Layers

The dual approach addresses distinct architectural requirements that single-mechanism strategies cannot satisfy:

  • pinia-plugin-persistedstate handles transient UI state (sidebar widths, panel toggles) that can be safely stored in localStorage and reset during application updates without user impact.

  • window.$preload.store manages critical configuration (device remarks, theme settings, ADB binary paths) that must persist across Electron version updates and remain accessible to the main process for spawning external processes or controlling native UI elements.

Both mechanisms cooperate seamlessly within individual stores: a store may let the plugin handle its reactive state while manually syncing specific keys via the Electron preload store when cross-process durability is required.

Summary

  • escrcpy configures global automatic persistence in desktop/src/store/index.js using store.use(persistedState) from pinia-plugin-persistedstate
  • Routine Pinia stores like preferences rely on localStorage-based serialization without additional persistence code
  • The window.$preload.store wrapper around electron-store provides filesystem-based durability for cross-process data in desktop/src/store/device/index.js and desktop/src/store/theme/index.js
  • Device configurations use $electronStore.set() with dot-notation paths like ['device', deviceId, 'remark'] for hierarchical storage
  • Theme synchronization uses window.$preload.store.get('common.theme') to maintain consistency between main and renderer processes
  • This dual-layer strategy balances developer ergonomics with production reliability in Electron environments

Frequently Asked Questions

Does escrcpy persist all Pinia stores automatically?

No. While most stores inherit automatic persistence through the global plugin, stores handling sensitive or cross-process data like the device store implement manual persistence using window.$preload.store. This selective approach ensures that data requiring main process access or survival across app updates is written to the filesystem rather than browser localStorage.

Where does escrcpy store device configuration data?

Device configurations persist to the user's application data directory via electron-store, accessed through window.$preload.store in desktop/src/store/device/index.js. The init() method reads the entire device object on startup, while setRemark() writes individual device properties using nested key paths that organize data by device ID.

Can I disable persistence for specific stores in escrcpy?

Yes. While escrcpy primarily opts for selective manual persistence rather than global exclusions, the pinia-plugin-persistedstate library supports store-specific configuration using the persist option. Stores that require electron-store integration simply ignore the automatic persistence for specific keys by reading from and writing to window.$preload.store directly while allowing other state to auto-persist.

What is the difference between localStorage and electron-store in escrcpy?

localStorage, used by the Pinia plugin, stores data within the renderer process and clears when the user clears browser data or during certain app updates, making it suitable for temporary UI state. electron-store, wrapped by window.$preload.store, writes encrypted JSON to the operating system's application data directory and maintains data across version updates while enabling two-way communication between the Electron main and renderer processes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →