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

> Discover how escrcpy persists state in Pinia stores using a dual-layer approach: automatic localStorage serialization and custom Electron store for cross-process durability.

- Repository: [viarotel-org/escrcpy](https://github.com/viarotel-org/escrcpy)
- Tags: deep-dive
- Published: 2026-09-10

---

**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](https://github.com/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`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/store/index.js), the application creates the Pinia instance and installs the persistence plugin before mounting to the Vue application:

```javascript
// 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`](https://github.com/viarotel-org/escrcpy/blob/main/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`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/store/device/index.js)) demonstrates manual persistence by reading and writing device configurations through the Electron store:

```javascript
// 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`](https://github.com/viarotel-org/escrcpy/blob/main/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:

```javascript
// 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`](https://github.com/viarotel-org/escrcpy/blob/main/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`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/store/device/index.js) and [`desktop/src/store/theme/index.js`](https://github.com/viarotel-org/escrcpy/blob/main/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`](https://github.com/viarotel-org/escrcpy/blob/main/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.