EIPC
Documentation

Events

Send events from main process to renderer.

Events allow the main process to push data to renderer processes. They’re useful for notifications, real-time updates, and system events.

Defining Events

Use the [Event] attribute on a method to define an event:

[RendererAPI]
[ContextBridge]
interface Notifications {
  // Regular method
  getUnreadCount() -> number

  // Event - main can dispatch this to renderer
  [Event]
  newNotification(title: string, body: string)

  [Event]
  notificationCleared(id: number)
}

Events don’t have return types — they’re one-way from main to renderer.

Dispatching Events

In the main process, use the dispatcher to send events:

import { Notifications } from '../ipc/browser/MyApp';

// Set up implementation and get dispatcher
const dispatcher = Notifications.for(mainWindow.webContents).setImplementation({
  async getUnreadCount() {
    return unreadNotifications.length;
  },
});

// Dispatch events to this window
dispatcher.dispatchNewNotification('New Message', 'You have a new message from John');
dispatcher.dispatchNotificationCleared(123);

Event Naming

The schema event name is used to generate both the dispatch method and the listener:

Schema EventDispatch MethodRenderer Listener
newNotificationdispatchNewNotificationonNewNotification
dataChangeddispatchDataChangedonDataChanged
shutdowndispatchShutdownonShutdown

Listening to Events

In the renderer, import the interface and subscribe to events using the auto-generated on + EventName methods:

import { Notifications } from '../ipc/renderer/MyApp';

// Subscribe to events
Notifications.onNewNotification((title, body) => {
  console.log(`New notification: ${title} - ${body}`);
  showNotificationToast(title, body);
});

Notifications.onNotificationCleared((id) => {
  console.log(`Notification ${id} cleared`);
  removeFromList(id);
});

Unsubscribing

The subscription returns an unsubscribe function:

const unsubscribe = Notifications.onNewNotification((title, body) => {
  console.log(`New notification: ${title}`);
});

// Later, stop listening
unsubscribe();

React Integration

Since event listeners return an unsubscribe function, they work perfectly with React’s useEffect cleanup:

import { useEffect, useState } from 'react';
import { Notifications } from '../ipc/renderer/MyApp';

function NotificationToast() {
  const [message, setMessage] = useState<string | null>(null);

  useEffect(() => {
    return Notifications.onNewNotification((title, body) => {
      setMessage(`${title}: ${body}`);
    });
  }, []);

  if (!message) return null;
  return <div className="toast">{message}</div>;
}

The unsubscribe function is returned directly from useEffect, so React automatically cleans up the listener when the component unmounts.

Event Arguments

Events can have any number of typed arguments:

[RendererAPI]
[ContextBridge]
interface System {
  [Event]
  shutdown()

  [Event]
  error(code: number, message: string)

  [Event]
  progress(taskId: string, percent: number, status: Status)

  [Event]
  dataReceived(data: ComplexData)
}

Real-World Examples

Download Progress

structure DownloadProgress {
  id: string
  filename: string
  bytesReceived: number
  totalBytes: number
  percent: number
}

[RendererAPI]
[ContextBridge]
interface Downloads {
  startDownload(url: string) -> string
  cancelDownload(id: string)

  [Event]
  progress(progress: DownloadProgress)

  [Event]
  complete(id: string, filePath: string)

  [Event]
  error(id: string, error: string)
}

Main process:

const dispatcher = Downloads.for(webContents).setImplementation({
  async startDownload(url) {
    const id = generateId();

    downloadFile(url, {
      onProgress: (bytes, total) => {
        dispatcher.dispatchProgress({
          id,
          filename: path.basename(url),
          bytesReceived: bytes,
          totalBytes: total,
          percent: (bytes / total) * 100,
        });
      },
      onComplete: (filePath) => {
        dispatcher.dispatchComplete(id, filePath);
      },
      onError: (error) => {
        dispatcher.dispatchError(id, error.message);
      },
    });

    return id;
  },
  // ...
});

Renderer:

import { Downloads } from '../ipc/renderer/MyApp';

const downloadId = await Downloads.startDownload('https://example.com/file.zip');

Downloads.onProgress((progress) => {
  if (progress.id === downloadId) {
    updateProgressBar(progress.percent);
  }
});

Downloads.onComplete((id, filePath) => {
  if (id === downloadId) {
    showSuccess(`Downloaded to ${filePath}`);
  }
});

Window State

enum WindowState {
  Normal
  Minimized
  Maximized
  Fullscreen
}

[RendererAPI]
[ContextBridge]
interface Window {
  getState() -> WindowState

  [Event]
  stateChanged(state: WindowState)

  [Event]
  focusChanged(focused: boolean)
}

Main process:

const dispatcher = Window.for(win.webContents).setImplementation({
  async getState() {
    if (win.isFullScreen()) return 'Fullscreen';
    if (win.isMaximized()) return 'Maximized';
    if (win.isMinimized()) return 'Minimized';
    return 'Normal';
  },
});

win.on('maximize', () => dispatcher.dispatchStateChanged('Maximized'));
win.on('unmaximize', () => dispatcher.dispatchStateChanged('Normal'));
win.on('minimize', () => dispatcher.dispatchStateChanged('Minimized'));
win.on('focus', () => dispatcher.dispatchFocusChanged(true));
win.on('blur', () => dispatcher.dispatchFocusChanged(false));

Events vs Stores

Use Events when:

  • You need to notify about discrete occurrences
  • Multiple events can happen over time
  • Subscribers need to react to each occurrence

Use Stores when:

  • You have a single piece of state that changes
  • You want React integration with hooks
  • You need the current value, not just changes

Next Steps