electron + react + recoil + prisma + sqlite やるお

3 min read読了の目安(約3500字

ローカルDB

electronでローカルにDBを置きたくなったGWの昼さがり。

前提事項

  • 軽めのDBで、sqliteにする。
  • ORMを使おうと思うが、prismaがよいらしい。
    TypeORMと比べて、OneToMany等の子供も型安全になるらしい。
  • 状態管理は、recoilを使う。

ぱくり元

適当なreactサンプルを持ってくる。
今回は、【React + Recoil】ちょっとリッチな ToDo アプリにしてみた。

ソースコード

こちらになります。

electron-react-boilerplate

今回は、electron-react-boilerplateを使って、electronプロジェクトを作成する。

git clone --depth 1 --single-branch https://github.com/electron-react-boilerplate/electron-react-boilerplate.git EleSqlPrisma
cd EleSqlPrisma
# ライブラリを最新にする
npx -p npm-check-updates  -c "ncu"
npx -p npm-check-updates  -c "ncu -u"
yarn install
yarn start

そのまま起動するとprocessがないというエラーになるので、以下を設定する。

https://github.com/forest1040/EleSqlPrisma/blob/master/src/main.dev.ts#L80
webPreferences: { nodeIntegration: true, contextIsolation: false }

XSS対策的には、nodeIntegrationは、falseで、contextIsolationを、trueの方がよいらしい。
後でちゃんと設定しよう。。

prismaのインストール

yarn add --dev prisma
yarn add @prisma/client
# prisma/schema.prismaの作成
npx prisma init

prismaの実行

prisma/schema.prisma を作成して、以下を実行する。

npx prisma generate

node_modules/.prisma/client 配下に生成される。

DBに反映させる。

npx prisma migrate dev --preview-feature

IPC通信

electronは、メインプロセスとレンダープロセスに分かれており、nodeモジュールを使うのは、メインプロセスで実行する。
なので、prismaはメインプロセスで実行し、レンダープロセスはIPCで通信する。

とりあえずやっつけで、main.dev.tsに書いてしまう。

メインプロセス側

async function getTaskes() {
  return await prisma.task.findMany();
}

ipcMain.handle('load-tasks', (event, message) => {
  console.log(message);
  return getTaskes();
});

async function createTaske(task: any) {
  return await prisma.task.create({
    data: task,
  });
}

ipcMain.handle('create-task', (event, task) => {
  console.log(task);
  return createTaske(task);
});

ipcMain.handle()でレンダープロセスに返すとPromiseで返せる。

型定義

prismaが型定義を作成しているのでそれを使うのが吉。

import { Task, PrismaClient } from '@prisma/client';
(略)

async function createTaske(task: Task) {
  return await prisma.task.create({
    data: task,
  });
}

ipcMain.handle('create-task', (event, task: Task) => {
  console.log(task);
  return createTaske(task);
});

レンダープロセス側

recoilのdefaultで、DBから取得する。

import { atom, selector } from 'recoil';
import { ipcRenderer } from 'electron';

export const tasksState = atom<
  { content: string; deadline: Date; priority: number }[]
>({
  key: 'tasksState',
  default: selector({
    key: 'savedTasksState',
    get: async () => {
      try {
        return await ipcRenderer.invoke('load-tasks', 'load-task');
      } catch (error) {
        throw error;
      }
    },
  }),
});

レンダー側は、ipcRenderer.invoke()で呼び出す。
するとPromiseで返って来るので、そのままReactでレンダーすると怒られてしまう。

そこで、Suspenseで囲ってしまう。便利。Reactがよしなにやってくれる。

export default function App() {
  return (
    <RecoilRoot>
      <div className="App">
        <AppBar />
        <Suspense fallback={<p>Loading...</p>}>
          <TodoList />
        </Suspense>
      </div>
    </RecoilRoot>
  );
}

登録側。本当は、先にDBを登録して成功したら、recoilState(setTasks)を更新した方がよい。
面倒だから今回はやらない。

  const handleRegister = () => {
    send();
    const newTask = {
      content: taskContent,
      deadline: taskDeadline,
      priority: taskPriority,
    };

    setTasks([...tasks, newTask]);
    createTask(newTask);
    onClose();
  };

あと、更新と削除も面倒だからパス。