Open6

Electron x Next.js でTodoアプリを作る

tsuemuratsuemura

プロジェクトを作る

$ npx create-next-app --example with-electron-typescript todoapp
$ cd todoapp

# 初期状態だとなぜか `electron-is-dev` の型定義が無いと言われてしまったので、インストールし直す……
$ npm install 

# 起動
$ npm run dev

tsconfig.jsonの設定

(この節は実は不要だった。コメント参照)

参考: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html

$ npm install --save-dev @tsconfig/node12
// tsconfig.json

{
  "extends": "@tsconfig/node12/tsconfig.json",

  "compilerOptions": {
    "preserveConstEnums": true
  },

  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

TailWind CSSの利用

参考: https://tailwindcss.com/docs/guides/nextjs
参考: https://zenn.dev/matamatanot/articles/e0e6371fe28b7479fb3f

$ npm install tailwindcss@latest postcss@latest autoprefixer@latest
$ npx tailwindcss init -p
// tailwind.conf.js

module.exports = {
  purge: ["./pages/**/*.js", "./components/**/*.js"],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
};

// _app.js

import "tailwindcss/tailwind.css";

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

tsuemuratsuemura

tsconfig.json はNext.JSとElectronそれぞれにデフォルトで存在するので、新たに作る必要はなかった

tsuemuratsuemura

まずはUIのガワだけ作っていく。

コンポーネントはこちらを使わせていただく
https://tailwindcomponents.com/component/todo-list-app

// components/Container.tsx
import TodoList from "./TodoList";

const Container = () => (
  <div
    className="
    h-100
    w-full
    flex
    items-center
    justify-center
    bg-teal-lightest
    font-sans
  "
  >
    <TodoList />
  </div>
);

export default Container;
// components/TodoList.tsx

import TodoItem from './TodoItem'

const TodoList = () => (
  <div className="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
    <div className="mb-4">
      <h1 className="text-grey-darkest">Todo List</h1>
      <div className="flex mt-4">
        <input className="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker" placeholder="Add Todo" />
        <button className="flex-no-shrink p-2 border-2 rounded text-teal border-teal hover:text-white hover:bg-teal">Add</button>
      </div>
    </div>
    <div>
      <TodoItem />
    </div>
  </div>
)

export default TodoList
// components/TodoList.tsx

const TodoItem = () => (
  <div className="flex mb-4 items-center">
    <p className="w-full text-grey-darkest">
      Add another component to Tailwind Components
    </p>
    <button className="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-green border-green hover:bg-green">
      Done
    </button>
    <button className="flex-no-shrink p-2 ml-2 border-2 rounded text-red border-red hover:text-white hover:bg-red">
      Remove
    </button>
  </div>
);

export default TodoItem
// pages/index.tsx
import Container from '../components/Container'

const IndexPage = () => {
  return (
    <Container/>
  )
}

export default IndexPage

この状態で npm run dev すると、こんな感じのUIが出来る。

当たり前だが、まだロジックを書いてないので、何も動かない。

tsuemuratsuemura
// components/TodoList.tsx
import { useState } from 'react'
import TodoItem from './TodoItem'
import { v4 as uuidv4 } from "uuid";

function TodoList() {
  const [tasks, setTasks] = useState([
    {key: uuidv4(), name: 'サンプルだよ'}
  ])

  const [name, setName] = useState('')

  const handleChange = (e) => {
    setName(e.target.value)
  }

  const addTask = () => {
    const task = {
      key: uuidv4(),
      name
    }
    setTasks([
      ...tasks,
      task
    ])
  }

  const removeTask = (key: string) => {
    setTasks(tasks.filter(task => task.key !== key))
  }

  const taskList = () => tasks.map(task => (
    <TodoItem
      key={task.key}
      task={task}
      removeTask={removeTask}
    />
  ))


  return (
    <div className="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
      <div className="mb-4">
        <h1 className="text-grey-darkest">Todo List</h1>
        <div className="flex mt-4">
          <input onChange={handleChange} className="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker" placeholder="Add Todo" />
          <button onClick={addTask} className="flex-no-shrink p-2 border-2 rounded text-teal border-teal hover:text-white hover:bg-teal">Add</button>
        </div>
      </div>
      <div>
        {taskList()}
      </div>
    </div>
  )

}

export default TodoList
// TodoItem.tsx

import { Task } from '../interfaces/Task'

type Props = {
  task: Task
  removeTask: any
}

const TodoItem = ({ task, removeTask}: Props) => (
  <div className="flex mb-4 items-center">
    <p className="w-full text-grey-darkest">
      {task.name}
    </p>
    <button className="flex-no-shrink p-2 ml-4 mr-2 border-2 rounded hover:text-white text-green border-green hover:bg-green">
      Done
    </button>
    <button onClick={() => removeTask(task.key)}className="flex-no-shrink p-2 ml-2 border-2 rounded text-red border-red hover:text-white hover:bg-red">
      Remove
    </button>
  </div>
);

export default TodoItem

タスクの追加と削除が出来るようになった。

  • めんどくさがってreduxを使わないでやってしまったけど、逆にめんどくさくなってしまった
  • TypeScriptが全く使いこなせていない
tsuemuratsuemura
  if (isDev) {
    mainWindow.webContents.openDevTools();
  }

これをElectron側の index.ts に入れておくと開発中のみDevToolsが出て便利