🙆

Next.jsでtodoアプリ を作ろうとしたらRecoil + React Hook Form v7で楽ができた件

6 min read

はじめに

なろう形式のタイトルってわかりやすいですね。
初投稿です。
Reactの学習のため、Todoアプリを作成しました。
どうせなら最新のライブラリを使用しようとRecoil, React Hook Form v7を使ってみましたので、備忘録がてらその内容を紹介します。

環境

Windows10 64bit(20H2)
WSL(Ubuntu 18.) v1
node v14.17.1
yarn 1.22.5
VSCode

完成図

こんな感じの簡単なtodoアプリを作ってみました。
見た目は完全に度外視しています。
スクリーンショット 2021-07-06 143548.png

install

まずWSLで下記コマンドを実行して、Next.js(TypeScript)のプロジェクトを作成します。

$ yarn create next-app --typescript
// 対話形式でプロジェクト名を聞かれるので、任意の名前を入力します。
What is your project named? … next-recil-todo

プロジェクトの中身はこのようになっています。

$ ls -1 next-recil-todo
README.md
next-env.d.ts
next.config.js
node_modules
package.json
pages
public
styles
tsconfig.json
yarn.lock

インストールできたので、動作確認します。

$ cd next-recil-todo
$ yarn dev

この画面が出ていれば成功です。
スクリーンショット 2021-07-06 105824.png

続いて使用するライブラリをインストールします。

$ yarn add recoil react-hook-form moment

これでインストールは完了です。

実装

コードはgithubに上げましたので、全体が見たい方は参照してください。
Qiita

atoms

まずはRecoilで使用するatomのファイルを配置するディレクトリを作成します。

$ mkdir -p src/atoms

作成したディレクトリに下記のファイルを作成します。

states.ts
import { atom } from "recoil";

export type Todo= {
    id: string
    title: string
    text: string
    isComplete: boolean
}

// Todoリストを保持
const todoListState = atom<Todo[]>({
    key: 'todoListState',
    default: [],
});

export { todoListState }

components

次にcomponetを管理するディレクトリを作成します。

$ mkdir src/components

作成したディレクトリに下記2つのファイルを作成します。

React Hook Form v7でRecoilを使用する際、validationを指定するには「 {...register(~」と記述する必要があります。
React Hook Form v6でRecoilでサンプルを記載しているサイトが多かったので、React Hook FormのVersionに注意してください。

src/components/edit.tsx
import { useSetRecoilState } from "recoil";
import { todoListState } from "../atoms/states";
import moment from "moment";
import { useForm } from 'react-hook-form'

type FormData = {
    id: string,
    title: string,
    text: string,
    isComplete: boolean,
}

const Edit: React.FC = () => {
    const setTodoList = useSetRecoilState(todoListState);

    // react-hook-formを設定
    const { register, handleSubmit, formState: { errors }, reset } = useForm<FormData>({
        mode: 'onChange',
        defaultValues: {
            id : moment().format('YYYYMMDDHHmmss'),
            title: '',
            text: '',
            isComplete: false,
        }
    })

    // Todoを追加
    const addTodo = (data: FormData) => {
        setTodoList((oldTodoList) => [
            ...oldTodoList,
            data,
        ]);
        reset()
    }

    return (
        <div>
            <form onSubmit={handleSubmit(addTodo)}>
                <label>title:</label>
                {/* 入力要素とvalidationを設定 */}
                <input
                    type="text"
                    {...register(
                        "title",
                        {
                            required: '必須項目です',
                            maxLength: {
                                value: 20,
                                message: '20文字以内で入力してください',
                            },
                        }
                    )}
                />
                {errors.title && <span>{errors.title.message}</span>}
                <br />
                <label>text:</label>
                {/* 入力要素とvalidationを設定 */}
                <input
                    type="text"
                    {...register(
                        "text",
                        {
                            required: '必須項目です',
                            maxLength: {
                                value: 20,
                                message: '20文字以内で入力してください',
                            },
                        }
                    )} />
                {errors.text && <span className="error.main">{errors.text.message}</span>}
                <br />
                <button type="submit">送信</button>
            </form>
        </div>
    )
}

export default Edit
src/components/list.tsx
import { useRecoilState, useSetRecoilState } from "recoil";
import { Todo, todoListState } from "../atoms/states";

const TodoList = () => {
    const [todoList, setTodoList] = useRecoilState(todoListState)
    // 完了したTodoを削除
    const deleteTodo = (id:string) => {
        const target = todoList.filter((todo) => {
            return (todo.id != id)
        })
        setTodoList(target);
    }

    return (
        <div>
            {todoList.map(item =>
                <div key={item.id}>
                    タイトル:{item.title} <br />
                    テキスト:{item.text} <br />
                    <button  onClick={() => { deleteTodo(item.id) }} >
                        完了
                    </button>
                </div>
            )}
        </div>
    )
}

export default TodoList

Recoilを使用したcomponetを表示します。

pages/_app.tsx
import React from 'react';
import type { AppProps } from 'next/app'
import { RecoilRoot } from 'recoil';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    // RecoilRootを設定
    <RecoilRoot>
      <Component {...pageProps} />
    </RecoilRoot>
  )
}

export default MyApp
pages/index.tsx
import TodoList from '../src/components/list'
import Edit from '../src/components/edit'

export default function Home() {
  return (
    <div>
      {/* componetを表示 */}
      <Edit />
      <TodoList />
    </div>
  )
}

まとめ

Reactの書籍ではReduxを使用していましたが分かりづらいのもあり、Recoilを使ってみたところ非常に簡単で使いやすかったです。
これからはもっと発展させてGraphQLとの連携を行ってみようと思います。