# How escrcpy Uses Vue 3 Composition API and `<script setup>` to Build Its Electron Interface

> Discover how escrcpy uses Vue 3 Composition API and script setup to build a type-safe, modular Electron interface with reusable composables for reactive state and business logic.

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

---

**escrcpy leverages Vue 3's Composition API together with the `<script setup>` syntax to create a type-safe, modular Electron interface where reactive state, lifecycle hooks, and business logic are encapsulated in reusable composables.**

The open-source project escrcpy provides a graphical interface for scrcpy using Electron and Vue 3. The codebase adopts modern Vue patterns by implementing the **Composition API** exclusively through **`<script setup>`** blocks, eliminating boilerplate while enabling tight integration with native Electron APIs.

## The `<script setup>` Entry Point Pattern

Every Vue component in escrcpy begins with a `<script setup>` block, as seen in [`desktop/src/App.vue`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/App.vue). This syntax compiles into the standard `setup()` function but eliminates the need for explicit return statements—variables declared in the block are automatically exposed to the template.

```vue
<script setup>
import { onMounted } from 'vue'
import Layouts from './layouts/index.vue'
import { useWindowStateSync } from '@/hooks/use-window-state-sync'
import { useStartApp } from '@/hooks/use-start-app'

const { locale, size } = useWindowStateSync()
const startApp = useStartApp()

onMounted(() => {
  startApp.open()
})
</script>

```

In this pattern, **ES module imports** work exactly like standard JavaScript, while Vue **lifecycle hooks** such as `onMounted` are invoked directly. The template accesses `locale`, `size`, and `startApp` without any explicit return object because `<script setup>` implicitly exposes top-level bindings.

## Encapsulating Logic with Custom Composables

Reusable stateful logic lives in the `desktop/src/hooks/` directory, following the Vue Composition API convention of naming functions with the `use` prefix. These composables encapsulate Electron-specific behaviors and provide reactive data to multiple components.

**`useWindowStateSync`** ([`desktop/src/hooks/use-window-state-sync.ts`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/hooks/use-window-state-sync.ts)) synchronizes UI preferences with the Electron store:

```typescript
import { ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { usePreferenceStore } from '@/stores/preference'

export function useWindowStateSync() {
  const preferenceStore = usePreferenceStore()
  const { locale, size } = storeToRefs(preferenceStore)
  
  // Sync with Electron main process
  watch([locale, size], () => {
    window.$preload.ipcRenderer.send('sync-window-state', { 
      locale: locale.value, 
      size: size.value 
    })
  })
  
  return { locale, size }
}

```

**`useStartApp`** ([`desktop/src/hooks/use-start-app.ts`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/hooks/use-start-app.ts)) centralizes the logic for launching scrcpy instances and parsing command-line arguments. Other composables like `useDeviceStatus` and `useAppUpdate` follow identical patterns, utilizing `ref`, `computed`, and `watch` from Vue's reactivity system.

## Component Implementation Examples

The **Device view** ([`desktop/src/views/device/index.vue`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/views/device/index.vue)) demonstrates complex composition by importing multiple stores and hooks:

```vue
<script setup>
import { onMounted } from 'vue'
import { useDeviceList } from '@/hooks/use-device-list'

const { devices, refreshDevices } = useDeviceList()

onMounted(() => {
  refreshDevices()
})
</script>

```

For UI controls, the **Volume component** ([`desktop/src/components/control-bar/volume/index.vue`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/components/control-bar/volume/index.vue)) uses reactive refs with watchers to bridge user input to ADB commands:

```vue
<script setup>
import { ref, watch } from 'vue'

const volume = ref(50)

watch(volume, (newVal) => {
  window.$preload.ipcRenderer.invoke('set-volume', newVal)
})
</script>

<template>
  <el-slider v-model="volume" :min="0" :max="100" />
</template>

```

The **Video Codec selector** ([`desktop/src/components/preference-form/components/select-video-codec/index.vue`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/components/preference-form/components/select-video-codec/index.vue)) retrieves options through a dedicated composable and persists changes via the store, illustrating how `<script setup>` simplifies form handling.

## Bridging Vue Reactivity with Electron

The preload script ([`desktop/electron/preload.js`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/electron/preload.js)) exposes IPC helpers on the window object, which components access directly inside `<script setup>` blocks. This architecture maintains type safety while allowing Vue's reactivity to drive native operations.

Accessing the preload bridge within a component:

```vue
<script setup>
const startApp = useStartApp()

window.$preload.ipcRenderer.on('execute-arguments-change', (_, params) => {
  startApp.open(params)
})
</script>

```

The project also leverages specialized packages like `@escrcpy/electron-ipcx` that provide additional composables for IPC communication. These can be imported alongside standard Vue APIs, creating a unified development experience where Electron main-process calls look identical to local function calls.

## Summary

- **`<script setup>`** serves as the exclusive entry point for all Vue components in escrcpy, automatically exposing top-level variables to templates without boilerplate return statements.
- Custom **composables** in `desktop/src/hooks/` encapsulate Electron-specific logic such as window state synchronization (`useWindowStateSync`) and application lifecycle management (`useStartApp`).
- Components access **Electron IPC** directly through the preload bridge (`window.$preload.ipcRenderer`) within setup blocks, integrating native APIs with Vue's reactivity system.
- The architecture separates concerns by isolating reusable logic in the hooks directory while keeping view components focused on presentation.

## Frequently Asked Questions

### What are the advantages of using `<script setup>` in escrcpy?

**`<script setup>`** eliminates the need for explicit `export default` and return objects, reducing boilerplate while maintaining full TypeScript inference. In escrcpy, this allows developers to declare reactive variables and composable imports at the top level, making the relationship between JavaScript logic and template variables immediately visible without scanning through setup functions.

### How does escrcpy organize its reusable Vue logic?

Reusable logic resides in the **`desktop/src/hooks/`** directory following the Vue composables convention. Files like [`use-window-state-sync.ts`](https://github.com/viarotel-org/escrcpy/blob/main/use-window-state-sync.ts) and [`use-start-app.ts`](https://github.com/viarotel-org/escrcpy/blob/main/use-start-app.ts) export functions that utilize Vue's `ref`, `watch`, and lifecycle hooks, returning reactive state that multiple components can import and share.

### Can components directly access Electron APIs from `<script setup>`?

Yes. Components access the **preload bridge** through `window.$preload.ipcRenderer`, which is exposed by [`desktop/electron/preload.js`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/electron/preload.js). This allows direct invocation of main-process methods inside `<script setup>` blocks while maintaining security isolation, as seen in volume controls that invoke `'set-volume'` IPC events reactively.

### Where can I find examples of complex component composition in escrcpy?

The **[`desktop/src/views/device/index.vue`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/views/device/index.vue)** file demonstrates sophisticated composition by combining multiple hooks like `useDeviceList()` with Pinia stores and lifecycle hooks. For UI-specific reactive patterns, examine **[`desktop/src/components/control-bar/volume/index.vue`](https://github.com/viarotel-org/escrcpy/blob/main/desktop/src/components/control-bar/volume/index.vue)**, which implements bidirectional data binding between Vue refs and hardware controls through IPC.