electron + react + recoil + prisma + sqlite やるお
ローカル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がないというエラーになるので、以下を設定する。
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();
};
あと、更新と削除も面倒だからパス。
Discussion