Table of Contents
はじめに
前回は Electron ForgeでReact + Vite + TypeScriptのデスクトップアプリ開発環境構築 を行いました。
その際の問題点としてメインプロセスとレンダラープロセスのデータ通信を行う際、IPCを利用しますが、その部分の型はglobal.d.tsのようなファイルを用意して手動で定義する必要がありました。
人の手で型定義する場合、ミスが生まれバグを生んでしまう可能性があるので、ここをなんとか解決したいです。
今回はelectron-trpc というライブラリを利用して型安全なIPCを実現したいと思います。
環境
- Node 18.7.0
- TypeScript 5.0.4
- Electron Forge 6.1.1
- Electron 24.3.0
- React 18.2.0
必要パッケージのインストール
まずは必要なパッケージをすべてインストールします。
$ npm install @trpc/client @trpc/server @trpc/react-query @tanstack/react-query electron-trpc zod
メインプロセスでルーターを定義する
最初はメインプロセス側を書いていきます。
tRPCのルーターを定義して必要なAPIをそろえます。入力値のバリデーションにはzodを使っています。
src/api.tsに以下のように書きます。
import z from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create({ isServer: true });
const procedure = t.procedure;
export const router = t.router({
getTodos: procedure.query(async () => {
const todos = // Todoデータ一覧取得処理
return {
todos
};
}),
createTodo: procedure.input(z.object({ text: z.string(), })).mutation(async (req) => {
const { input } = req;
const todo = // Todoデータ作成処理
return todo;
}),
deleteTodo: t.procedure.input(z.object({ id: z.number() })).mutation(async (req) => {
const { input } = req;
// Todoデータ削除処理
})
});
export type AppRouter = typeof router;
コメントしている箇所にはSQLiteなどの処理や、メインプロセス側で必要な処理を記述することになります。
あとはエントリファイルであるsrc/main.tsでelectron-trpcのcreateIPCHandler()
を読んであげるだけになります。
import { app, BrowserWindow } from 'electron';
import path from 'path';
import { createIPCHandler } from 'electron-trpc/main'; // ←追加
import { router } from './api'; // ←追加
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
// ↓追加
createIPCHandler({ router, windows: [mainWindow] });
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
}
};
app.on('ready', () => {
createWindow();
});
// 省略
preload.tsの設定
プロセス間でのやり取りを許可するにはpreload.tsにも記述が必要です。
electron-trpcの場合はexposeElectronTRPC()
だけ呼び出すだけで良くなります。
import { exposeElectronTRPC } from 'electron-trpc/main';
process.once('loaded', async () => {
exposeElectronTRPC();
});
レンダラープロセスでAPI呼び出し
あとはReactの世界の話で @trpc/react-query
というライブラリを使うと簡単にAPI呼び出し、取得データの状態管理ができるようになります。
まずはsrc/trpc.tsを用意して以下のように書きます。
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from './api';
export const trpcReact = createTRPCReact<AppRouter>();
用意したtrpcReactを使ってクライアント作成、ReactのAppコンポーネントに追加します。
src/app.tsxで以下のように書きます。
import * as ReactDOM from 'react-dom';
import { useState } from 'react';
import { ipcLink } from 'electron-trpc/renderer';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import Todo from './components/Todo';
import { trpcReact } from './trpc';
const queryClient = new QueryClient();
const trpcClient = trpcReact.createClient({
links: [ipcLink()],
})
function App() {
return (
<trpcReact.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<Todo />
</QueryClientProvider>
</trpcReact.Provider>
);
}
function render() {
ReactDOM.render(<App/>, document.body);
}
render();
そしてsrc/components/Todo.tsxを作成して、そこでAPI呼び出しを行います。
import { FormEvent, useState } from "react";
import { trpcReact } from "../trpc";
export default function Todo() {
const { data, refetch } = trpcReact.getTodos.useQuery();
const [text, setText] = useState("");
const { mutate: createTodo } = trpcReact.createTodo.useMutation({
onSuccess: () => {
refetch();
},
});
const { mutate: deleteTodo } = trpcReact.deleteTodo.useMutation({
onSuccess: () => {
refetch();
},
});
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
createTodo({ text });
};
const handleDelete = (id: number) => {
deleteTodo({ id });
};
return (
<>
<form onSubmit={handleSubmit}>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Add Todo</button>
</form>
<ul>
{data?.todos?.map((todo) => (
<li>
{todo.text}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
</>
);
}
今回は必要最低限でエラーハンドリングなどせず、雑に作ってます。
書いてる途中で気づくと思いますが、ちゃんと trpcReact.
と書いてる間に補完が動き、メインプロセスで定義したAPIの型がちゃんと効いてるのがわかると思います。
queryのレスポンスデータやmutationの入力値までもしっかり型安全になっています。
完成したので npm start
を実行してTodoの追加、一覧表示、削除ができるアプリが見えると思います。※ src/api.tsのコメント部分の記述は必要
おわりに
本記事で解説したように、preload.tsで定義せずにメインプロセス側でtRPCのルーターとzodで入力値のバリデーションを書いてあげることで、レンダラープロセス側で型安全なAPI呼び出しが実現できました。
これでメインプロセスとレンダラープロセスのデータ通信の齟齬起因のバグを防ぐこともでき、開発体験も上がるはずです。
Related Posts
Daiki Urata
2024/06/10
Daiki Urata
2023/05/22