electron-shadcn

Inter-Process Communication (IPC)

How to use IPC in electron-shadcn to enable communication between the main process and renderer processes.

GitHubEdit on GitHub

electron-shadcn uses oRPC for type-safe Inter-Process Communication (IPC) between the main process and renderer processes. This approach provides full TypeScript type inference, making your IPC calls as safe as regular function calls.

Why oRPC?

Traditional Electron IPC can be error-prone:

  • No type safety between processes
  • Easy to misspell channel names
  • No autocomplete for parameters or return types
  • Runtime errors instead of compile-time errors

With oRPC, you get:

  • End-to-end type safety: Full TypeScript inference from renderer to main
  • Schema validation: Input validation with Zod
  • Autocomplete: IDE suggestions for all IPC calls
  • Refactoring support: Rename procedures and TypeScript catches all usages

Directory Structure

IPC-related code is organized in the following directories:

src/
├── actions/          # Client-side actions (renderer)
├── ipc/              # IPC configuration and handlers
│   ├── theme/        # Theme-related IPC
│   └── window/       # Window control IPC

Creating a New IPC Procedure

Define the Schema

Create a Zod schema for your procedure's input:

// src/ipc/files/schema.ts
import { z } from "zod";

export const readFileSchema = z.object({
  path: z.string(),
  encoding: z.enum(["utf-8", "base64"]).default("utf-8"),
});

export const writeFileSchema = z.object({
  path: z.string(),
  content: z.string(),
});

Create the Handler (Main Process)

Define the handler that runs in the main process:

// src/ipc/files/handler.ts
import { os } from "@orpc/server";
import { readFileSchema, writeFileSchema } from "./schema";
import fs from "fs/promises";

export const filesRouter = os.router({
  readFile: os
    .input(readFileSchema)
    .handler(async ({ input }) => {
      const content = await fs.readFile(input.path, input.encoding);
      return { content };
    }),

  writeFile: os
    .input(writeFileSchema)
    .handler(async ({ input }) => {
      await fs.writeFile(input.path, input.content);
      return { success: true };
    }),
});

Register the Router

Add your router to the main IPC configuration:

// src/ipc/router.ts
import { os } from "@orpc/server";
import { windowRouter } from "./window/handler";
import { themeRouter } from "./theme/handler";
import { filesRouter } from "./files/handler";

export const appRouter = os.router({
  window: windowRouter,
  theme: themeRouter,
  files: filesRouter, // Add your new router
});

export type AppRouter = typeof appRouter;

Create the Action (Renderer)

Create a client-side action to call the IPC procedure:

// src/actions/files.ts
import { client } from "@/ipc/client";

export async function readFile(path: string, encoding?: "utf-8" | "base64") {
  return client.files.readFile({ path, encoding });
}

export async function writeFile(path: string, content: string) {
  return client.files.writeFile({ path, content });
}

Use in Components

Call your actions from React components:

// src/routes/index.tsx
import { readFile, writeFile } from "@/actions/files";
import { Button } from "@/components/ui/button";

export default function HomePage() {
  const handleReadFile = async () => {
    const result = await readFile("/path/to/file.txt");
    console.log(result.content);
  };

  return (
    <Button onClick={handleReadFile}>
      Read File
    </Button>
  );
}

Built-in IPC Procedures

electron-shadcn comes with pre-configured IPC procedures:

Window Controls

Control the application window from the renderer:

import { minimizeWindow, maximizeWindow, closeWindow } from "@/actions/window";

// Minimize the window
await minimizeWindow();

// Maximize/restore the window
await maximizeWindow();

// Close the window
await closeWindow();

Theme Management

Control the application theme:

import { setTheme, getTheme } from "@/actions/theme";

// Set theme to dark mode
await setTheme("dark");

// Set theme to light mode
await setTheme("light");

// Use system preference
await setTheme("system");

// Get current theme
const currentTheme = await getTheme();

Using with React Query

If you want to integrate IPC calls with React Query for caching and state management, you can create custom hooks:

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { readFile, writeFile } from "@/actions/files";

export function useFileContent(path: string) {
  return useQuery({
    queryKey: ["file", path],
    queryFn: () => readFile(path),
  });
}

export function useWriteFile() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ path, content }: { path: string; content: string }) =>
      writeFile(path, content),
    onSuccess: (_, { path }) => {
      // Invalidate the file query to refetch
      queryClient.invalidateQueries({ queryKey: ["file", path] });
    },
  });
}

Then use in components:

export default function FileEditor({ path }: { path: string }) {
  const { data, isLoading, error } = useFileContent(path);
  const writeMutation = useWriteFile();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error loading file</div>;

  return (
    <div>
      <textarea defaultValue={data?.content} />
      <Button
        onClick={() => writeMutation.mutate({ path, content: "new content" })}
        disabled={writeMutation.isPending}
      >
        Save
      </Button>
    </div>
  );
}

Error Handling

Handle IPC errors gracefully:

// In your handler
import { os } from "@orpc/server";
import { ORPCError } from "@orpc/server";

export const filesRouter = os.router({
  readFile: os
    .input(readFileSchema)
    .handler(async ({ input }) => {
      try {
        const content = await fs.readFile(input.path, input.encoding);
        return { content };
      } catch (error) {
        if (error.code === "ENOENT") {
          throw new ORPCError({
            code: "NOT_FOUND",
            message: `File not found: ${input.path}`,
          });
        }
        throw new ORPCError({
          code: "INTERNAL_SERVER_ERROR",
          message: "Failed to read file",
        });
      }
    }),
});

Handle errors in the renderer:

const handleReadFile = async () => {
  try {
    const result = await readFile("/path/to/file.txt");
    console.log(result.content);
  } catch (error) {
    if (error.code === "NOT_FOUND") {
      toast.error("File not found");
    } else {
      toast.error("An error occurred");
    }
  }
};

Input Validation with Zod

oRPC uses Zod for runtime validation. Define complex schemas for your inputs:

import { z } from "zod";

// Complex nested schema
export const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  preferences: z.object({
    theme: z.enum(["light", "dark", "system"]),
    notifications: z.boolean(),
  }),
});

// Array inputs
export const batchDeleteSchema = z.object({
  ids: z.array(z.string().uuid()).min(1).max(100),
});

// Union types
export const exportFormatSchema = z.discriminatedUnion("format", [
  z.object({ format: z.literal("json"), pretty: z.boolean() }),
  z.object({ format: z.literal("csv"), delimiter: z.string() }),
]);

Zod schemas provide both compile-time TypeScript types and runtime validation, ensuring your IPC calls are always type-safe.

Best Practices

  • Keep handlers focused: Each handler should do one thing well
  • Use meaningful names: Name procedures descriptively (e.g., readUserSettings not getData)
  • Validate inputs: Always define Zod schemas for input validation
  • Handle errors: Provide meaningful error messages for debugging
  • Organize by feature: Group related IPC procedures in feature-specific directories

On this page