Renderer
The renderer side of ElectroJS is a normal frontend package that uses @electrojs/renderer.
Each view package owns:
- its
view.config.ts - its
index.html - its frontend source
- its generated
electro-env.d.ts
The recommended layout is one renderer view per package.
View Package Shape
views/main/
├── package.json
├── tsconfig.json
├── view.config.ts
├── electro-env.d.ts
├── index.html
└── src/
├── main.tsx
└── app.tsxThe package-local electro-env.d.ts is what provides typed bridge and signals.
view.config.ts
import { defineViewConfig } from "@electrojs/config";
import react from "@vitejs/plugin-react";
export default defineViewConfig({
viewId: "main",
entry: "./index.html",
plugins: [react()],
});viewId must match the runtime-side @View({ source: "view:main" }).
Bootstrapping
Use ElectroRenderer.initialize(...).
import { ElectroRenderer } from "@electrojs/renderer";
import ReactDOM from "react-dom/client";
import { App } from "./app";
await ElectroRenderer.initialize(() => {
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
});This does three things:
- reads the preload API from
window.__ELECTRO_RENDERER__ - creates the bridge client
- creates the signals client
bridge and signals must not be used before initialization finishes.
Bridge
Import bridge from @electrojs/renderer:
import { bridge } from "@electrojs/renderer";
const notes = await bridge.notes.getNotes();
await bridge.notes.createNote("Title", "Body");The available namespaces and methods are generated from runtime source and constrained by the current view's access list.
Signals
Import signals from @electrojs/renderer:
import { signals } from "@electrojs/renderer";
const subscription = signals.subscribe("notes:changed", (payload) => {
console.log(payload);
});
subscription.unsubscribe();One-time subscriptions are also supported:
signals.once("notes:changed", (payload) => {
console.log(payload);
});Signal keys and payloads are also generated from runtime source and constrained by the current view's signals list.
Typing Model
ElectroJS augments @electrojs/renderer inside each view package's electro-env.d.ts.
That means you do not manually write bridge typings in renderer code. You only:
- declare runtime
@query()/@command()/@signal()contracts - declare view access in runtime
@View(...) - include
electro-env.d.tsin the view packagetsconfig.json
{
"include": ["src", "electro-env.d.ts"]
}Errors You Will See
Renderer-side misuse shows up as explicit runtime errors:
- calling
ElectroRenderer.initialize()twice - using
bridgebefore initialization - using
signalsbefore initialization - missing preload API on
window.__ELECTRO_RENDERER__
These are framework errors, not silent failures.
Relationship to Runtime @View()
The renderer package does not declare runtime permissions itself.
The source of truth is the runtime-side @View() class:
@View({
source: "view:main",
access: ["notes:getNotes", "notes:createNote"],
signals: ["notes:changed"],
})
export class MainView extends ViewProvider {}The renderer package is just the frontend surface bound to that runtime declaration.
Development Model
During electro dev:
- each view package gets its own Vite dev server
- renderer source changes go through HMR
- runtime/preload changes rebuild and restart Electron
This separation is one of the reasons ElectroJS recommends one view per package as the default architecture.