Skip to content

Repository files navigation

Electron React App

A modern Electron starter kit with React, Vite, TypeScript, and TailwindCSS — built around electron-conveyor for type-safe IPC and cross-window state.



Electron   React   TypeScript   Vite   Shadcn   Tailwind   Conveyor


Stack

🔹 Electron - Cross-platform desktop application framework.
🔹 React - The library for web and native user interfaces.
🔹 electron-conveyor - Type-safe IPC + cross-window state.
🔹 TypeScript - Type-safe JavaScript.
🔹 Shadcn UI - Beautiful and accessible component library.
🔹 TailwindCSS - Utility-first CSS framework.
🔹 Electron Vite - Lightning-fast build tool based on Vite for fastest hot-reload.
🔹 Electron Builder - Configured for packaging applications.


In-Built Features

Feature Description
Conveyor Type-safe IPC: queries, commands, streams, events — end-to-end inference
Cross-Window Stores Main-owned state synced live across every window, with opt-in persistence
Demo Branch Live playground of every primitive on the demo branch, main stays minimal
Sandboxed Renderer sandbox: true out of the box — the conveyor preload is sandbox-compatible
Custom Titlebar & Menus Style the window titlebar and menus as you want
Clean Project Structure Separation of main and renderer processes
Resources Protocol Access local file resources via res:// protocol
Import Path Aliases Keep your imports organized and clean
Theme Switcher Built-in theme switching for dark and light mode
Error Boundary Built-in React error boundary with detailed error reporting
Code Formatting Prettier and ESLint pre-configured for code quality
Hot Reload Lightning-fast development with Vite's HMR
VS Code Debugging Pre-configured launch configurations for debugging main and renderer processes

Installation

# Clone the repository
git clone https://github.com/guasam/electron-react-app

# Change directory
cd electron-react-app

# Install dependencies (use any package manager: npm, yarn, pnpm, bun)
npm install

Development

npm run dev

This starts Electron with hot-reload. main is deliberately minimal — a themed window frame, titlebar, menus, and the typed IPC layer — so you can start building your app on top of it immediately.

Removing the welcome screen

The window opens on a short tour of the stack. It is the one piece meant to be thrown away, and it is built so that costs nothing: it lives entirely in app/components/welcome, nothing else imports it, and it adds no IPC modules of its own.

rm -rf app/components/welcome

Then drop the <Welcome /> line and its import from app/app.tsx. What's left is an empty window with the shell still around it, ready for your app.

Try the demo

Want to see everything the stack can do first? The demo branch is an interactive playground of every IPC primitive (cross-window state, streaming, background tasks, middleware), with the real source behind each demo:

git switch demo
npm install
npm run dev

Switch back to main (and re-run npm install) when you're ready to build.


Conveyor — Inter-Process Communication

IPC is powered by electron-conveyor. One definition in main is the single source of truth for a feature; the renderer client is inferred from it — no channel strings, no hand-written API classes, no query keys.

Five primitives:

You want… Use Renderer side
Read something from main query() await it(), or .useQuery()
Tell main to do something command() await it(), or .useMutation()
Chunks pushed as they're produced stream() for await, or .useStream()
Main pushing to the renderer event() .subscribe(cb), or .useEvent(cb)
State shared live across all windows defineStore useConveyorStore(def)

Adding a feature (two edits)

1. Define the module in conveyor/modules/:

// conveyor/modules/notes.ts — runs in MAIN only
import { z } from 'zod'
import { defineModule, query, command } from '../init'

export const notesModule = defineModule({
  list: query(() => readNotes()),

  // input crosses the trust boundary → schema required, validated on every call
  save: command(z.object({ title: z.string(), body: z.string() }), ({ input }) => saveNote(input)),
})

2. Register it in conveyor/router.ts:

export const router = createRouter(
  {
    window: windowModule,
    web: webModule,
    notes: notesModule, // ← the key becomes the module id
  },
  { createContext, use: [devLogger] }
)

Done — the renderer client already knows it, fully typed:

import { conveyor } from '@/conveyor/client'

function Notes() {
  const notes = conveyor.notes.list.useQuery() // key derived from the path — never hand-written
  const save = conveyor.notes.save.useMutation({
    onSuccess: () => conveyor.notes.list.invalidate(),
  })

  return <button onClick={() => save.mutate({ title: 'Hi', body: '...' })}>Save</button>
}

Outside React, every member is a plain typed call: await conveyor.notes.list().

Handler context

Every handler receives ctx: the calling window and sender, plus the app context defined in conveyor/init.ts (this starter kit provides appStartedAt, the windows manager, and openWindow). Middleware can guard and widen it:

const authed = command.use(requireUser) // a reusable guarded base
export const account = defineModule({
  delete: authed(({ ctx }) => deleteAccount(ctx.user.id)),
})

Streams (LLM-style)

// main
respond: stream(z.string(), async function* ({ input, signal }) {
  for await (const token of llm.complete(input)) {
    if (signal.aborted) return
    yield token
  }
})

// renderer
for await (const token of conveyor.stream.respond(prompt)) append(token)

Events (main → renderer push)

// main — typed emitters per window or fan-out via the window manager
const emit = createEmitter(windowModule, win)
win.on('focus', () => emit.onFocusChange(true))

// renderer
conveyor.window.onFocusChange.useEvent(setFocused)

Cross-window stores

// conveyor/stores/shared.ts — pure, imported by BOTH processes
export const sharedStore = defineStore('shared', {
  state: { count: 0, notes: [] as string[] },
  schemas: { add: z.string() }, // payloads validated in main; types flow from the schema
  actions: {
    add: (s, note) => {
      s.notes.push(note)
    },
    increment: (s) => {
      s.count += 1
    },
  },
  persist: true, // survives restarts (JSON under userData)
})

// renderer — feels local, synced across every window
const count = useConveyorStore(sharedStore, (s) => s.count)
const { add, increment } = useConveyorActions(sharedStore)

Errors

Failures re-throw in the renderer as ConveyorError with a stable code — including custom codes thrown by your handlers (throw new ConveyorError('LOCKED', '...')). Branch on err.code, never on message strings. See the demo branch's Middleware page for a working example.

📖 Full API documentation: electron-conveyor


Custom Window Components

This starter kit includes a custom window implementation with:

  • Custom titlebar with app icon
  • Window control buttons (minimize, maximize, close)
  • Menu system with keyboard shortcuts
  • Dark/light mode toggle
  • Cross-platform support for Windows and macOS

Titlebar Menu Toggle

The titlebar menu can be toggled using:

  • Windows: Press the Alt key
  • macOS: Press the Option (⌥) key

When you press the toggle key:

  • If the menu is hidden, it becomes visible
  • If the menu is already visible, it gets hidden
  • The menu only toggles if menu items are available

Customizing Menu Items

To add, remove or modify menu items, update the following file:

  • app/shell/menu.ts

Key Directories Explained

app/ - Renderer Process

  • React application that runs in the browser window
  • app/shell/ — titlebar, menus, window frame, theme

conveyor/ - The IPC Surface

  • conveyor/init.ts — authoring primitives bound to the app's context
  • conveyor/modules/ — feature modules (main-process only; the renderer imports only type AppRouter)
  • conveyor/router.ts — the single registration point (modules, stores, middleware, context)
  • conveyor/client.ts — the typed renderer client with hooks

lib/main/ - Main Process

  • Window creation (app.ts, with the window manager), app lifecycle, res:// protocol

lib/preload/ - Preload Script

  • Two lines: expose the conveyor bridge. It never changes as your API grows, and it is sandbox-compatible — the renderer runs with sandbox: true

Path Aliases

import { Button } from '@/app/components/ui/button'
import { conveyor } from '@/conveyor/client'
  • @/app/app/ (renderer)
  • @/lib/lib/ (main + preload)
  • @/conveyor/conveyor/ (the IPC surface)
  • @/resources/resources/ (build resources)

Development Workflow

  1. UI Development: Work in app/ with React components
  2. IPC: Add a module in conveyor/modules/, register it in conveyor/router.ts
  3. Window Features: Customize the shell in app/shell/
  4. Checks: npm run typecheck, npm run lint, npm run format

Building for Production

# For Windows
npm run build:win

# For macOS
npm run build:mac

# For Linux
npm run build:linux

# Unpacked for all platforms
npm run build:unpack

Distribution files will be located in the dist directory.

About

Modern desktop application starter kit with Electron, React, TypeScript, TailwindCSS & Shadcn/UI to build cross-platform apps.

Topics

Resources

Stars

782 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages