Using React and tRPC with Electron

I published a branch in my Electron template repository that uses React. See https://github.com/awohletz/electron-prisma-trpc-example#what-about-react

This branch uses React with Vite for building, tRPC for communication, and Prisma for an SQLite DB.

The tRPC integration is done via an IPC request handler in the Main process:


import {AnyRouter, inferRouterContext, resolveHTTPResponse} from "@trpc/server";
import {HTTPRequest} from "@trpc/server/dist/http/internals/types";
import {IpcRequest, IpcResponse} from "../api";

export async function ipcRequestHandler<TRouter extends AnyRouter>(
  opts: {
    req: IpcRequest;
    router: TRouter;
    batching?: { enabled: boolean };
    onError?: (o: { error: Error; req: IpcRequest }) => void;
    endpoint: string;
    createContext?: (params: { req: IpcRequest }) => Promise<inferRouterContext<TRouter>>;
  },
): Promise<IpcResponse> {
  const createContext = async () => {
    return opts.createContext?.({req: opts.req});
  };

// adding a fake "https://electron" to the URL so it can be parsed
  const url = new URL("https://electron" + opts.req.url);
  const path = url.pathname.slice(opts.endpoint.length + 1);
  const req: HTTPRequest = {
    query: url.searchParams,
    method: opts.req.method,
    headers: opts.req.headers,
    body: opts.req.body,
  };

  const result = await resolveHTTPResponse({
    req,
    createContext,
    path,
    router: opts.router,
    batching: opts.batching,
    onError(o) {
      opts?.onError?.({...o, req: opts.req});
    },
  });

  return {
    body: result.body,
    headers: result.headers,
    status: result.status,
  };
}

This handler listens for IPC requests from the Renderer:

  app.whenReady().then(() => {
  ipcMain.handle('trpc', (event, req: IpcRequest) => {
    return ipcRequestHandler({
      endpoint: "/trpc",
      req,
      router: appRouter,
      createContext: async () => {
        return {};
      }
    });
  })
//...

In the Renderer, in the top-level App component I create the tRPC client with a custom link:

import {useState} from 'react'
import {trpc} from "./util";
import {httpBatchLink, loggerLink} from "@trpc/client";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {IpcRequest} from "../api";
import {Home} from "./Home";
import superjson from "superjson";

function App() {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        refetchOnWindowFocus: false,
      }
    }
  }));
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        loggerLink(),
        httpBatchLink({
          url: '/trpc',

          // custom fetch implementation that sends the request over IPC to Main process
          fetch: async (input, init) => {
            const req: IpcRequest = {
              url: input instanceof URL ? input.toString() : typeof input === 'string' ? input : input.url,
              method: input instanceof Request ? input.method : init?.method!,
              headers: input instanceof Request ? input.headers : init?.headers!,
              body: input instanceof Request ? input.body : init?.body!,
            };

            const resp = await window.appApi.trpc(req);

            return new Response(resp.body, {
              status: resp.status,
              headers: resp.headers,
            });
          }
        }),
      ],
      transformer: superjson
    }),
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        <Home/>
      </QueryClientProvider>
    </trpc.Provider>
  )
}

export default App

In essence, I’m tricking tRPC into thinking that it’s communicating over HTTP. The trick works by stringifying the JSON request from renderer and parsing it in Main.

I’ve plugged in superjson too, so that Dates and such are handled transparently on both sides.

With this integration, you can write your React and tRPC code as if you’re writing a web app!