How escrcpy Uses Vue 3 Composition API and `<script setup>` to Build Its Electron Interface
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. 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.
<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) synchronizes UI preferences with the Electron store:
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) 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) demonstrates complex composition by importing multiple stores and hooks:
<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) uses reactive refs with watchers to bridge user input to ADB commands:
<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) 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) 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:
<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 and 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. 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 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, which implements bidirectional data binding between Vue refs and hardware controls through IPC.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →