Closed61

Next.jsとRailsをつかってToDoアプリを作ってみる

しょーたしょーた

個人開発するなら記録とZennの使い方の練習も兼ねてここに残すことにする。

今回開発するもの

ToDoアプリ

このアプリでできること

  • ログインしたユーザーがToDoを登録できる
  • ToDoの新規登録、編集、削除が可能

開発環境

  • フロントエンド:Next.js TypeScript
  • バックエンド:Ruby on Rails(APIモード)
  • サーバー:Docker
しょーたしょーた

専門用語とか合ってるかとかは全くわからんけどとりあえず記録する

しょーたしょーた

こちらの記事からの変更点

  • front/DockerfileのNodeバージョンについて
    この記事のfront/Dockerfileでは下記のような記述になっている。
FROM node:14-alpine
WORKDIR /usr/src/app

ただNext.jsのv13以降はnodeのバージョンがv16.8.0以降(2023/2時点)でないと動作しないため下記のように書き換える。

FROM node:16
WORKDIR /usr/src/app
しょーたしょーた

とりあえず開発できる感じになったので簡単にデザインを作る。ここに時間をかける必要はないためFigmaで適当に作成

しょーたしょーた

デザインできたので実装進める。
機能実装も含めると詰まりまくる未来しか見えないのでまずは静的に実装する。

ログイン画面の実装予定はこんな感じ

なんのひねりもないログイン画面

しょーたしょーた

できた。
デザインと違うところはあるけど、あまりこだわらずに進める。

labelとinput、余白、ボタンなどはコンポーネント化している。

しょーたしょーた

このページを作成しているときに、「ここのデータくらいはDBから取得したいな」と思い少し機能実装もすることにした。
ToDoアプリごときにDB設計なんて必要なさそうだが、とりあえずやってみる。
…といってもここはchatGPTにおまかせした。

投げた質問はこれ

ToDoアプリのDB設計を行う。
ログインしたユーザーがTodoの登録、編集、削除を行えるようにし、削除した場合すぐに物理削除されるのではなく論理削除されるように設定したい。

帰ってきた答えがこれ

テーブル名:users

列名 データ型 制約
id INTEGER PRIMARY KEY, AUTOINCREMENT
username TEXT NOT NULL
password TEXT NOT NULL

テーブル名:todos

列名 データ型 制約
id INTEGER PRIMARY KEY, AUTOINCREMENT
user_id INTEGER NOT NULL
title TEXT NOT NULL
description TEXT
is_completed INTEGER NOT NULL DEFAULT 0
is_deleted INTEGER NOT NULL DEFAULT 0
created_at TEXT NOT NULL
updated_at TEXT NOT NULL

論理削除はis_deletedではなくdeleted_atの方が良さそうかなと思った。調べてみるとRailsにはParanoiaっていうgemを使えば簡単に論理削除ができるとのことだったので、これを使う前提でDBを再編成。
…した結果はこちら
usersは同じなのでtodosのDBだけ変更

テーブル名:todos

列名 データ型 制約
id INTEGER PRIMARY KEY, AUTOINCREMENT
user_id INTEGER NOT NULL
title TEXT NOT NULL
description TEXT
is_completed INTEGER NOT NULL DEFAULT 0
created_at TEXT NOT NULL
updated_at TEXT NOT NULL
deleted_at TEXT NOT NULL
しょーたしょーた

ということでtodosテーブルを作る。

docker-compose run --rm api bundle exec rails g model Todo title:string description:text is_completed:boolean user_id:integer deleted_at:datatime

を実行後マイグレーションする

しょーたしょーた

マイグレーションしようとしたところ
deleted_at:datatime
のdatatimeというものはない!的なエラーが出たので、とりあえずこのカラムは消した。とはいえ必要なので後で追加する方針

しょーたしょーた

先ほどのカラムを消すにあたりdb/migrate内のcreate_todos.rbファイルを直接編集していたのだけれど、変更を保存してrails db:migrateをしても変更が反映されなかった。キャッシュとかがあるのだと思っている。
今は

docker-compose run --rm api bundle exec rails db:environment:set RAILS_ENV=development

を実行すれば反映されるので、とりあえずmigrate内のファイルを更新したらこのコマンドを実行することにする。

しょーたしょーた

todosテーブルと合わせてusersテーブルも合わせて作成した。
テーブルを作ったのでダミーデータをseeds.rbより入れる。一つ一つ手入力でも良いのだけどダミーデータ生成のfakerというgemがあるようなので、今回はこれを使ってみる。

gemfileに追記

Gemfile
gem 'faker'

今回はDocker環境なので下記を実行

docker-compose run --rm api bundle exec bundle install

するとこんなエラーが。

Could not find gem 'faker' in any of the gem sources listed in your Gemfile.

英語苦手だけど、なんとなく翻訳では「fakerがないよ!!」と言われている気がした。
……いや、ないからインストールしようとしているんだがなぁと思いつつ原因調査する。

しょーたしょーた

【やったこと①】Gemfile.lockの削除
→意味なかった。

【やったこと②】gem install fakerの実行
→ターミナル上ではインストールできたとなったが、Gemfile,Gemfile.lock上に記載が追加されなかったためできてるようでできていないと思われる。

ここでふと「........Dockerが原因か…?」となったのでそちら路線で再調査。
調べるとこんな記事を発見
https://qiita.com/Masa9/items/cc36f9223e6ce1f0a8a9

まさにこれだ!!!となり

docker-compose build
↑これを実行後
docker-compose run --rm api bundle exec bundle install

をしたら無事追加できた!!エラー地獄の時はやめたくなるがこの瞬間が楽しかったりする😊

しょーたしょーた

seeds.rbはこんな感じで記載した。

8.times {
  User.create!(
    name: Faker::JapaneseMedia::StudioGhibli.unique.character,
    email: Faker::Internet.unique.email,
    password: Faker::Number.hexadecimal(digits: 15)
  )
}

8.times {
  Todo.create!(
    title: Faker::Lorem.sentence,
    deadline: Faker::Date.in_date_period,
    description: Faker::Lorem.sentence
  )
}

fakerの公式ドキュメントによるといろんな種類のダミーデータがあるので、これでダミーデータには困らなさそう。
https://github.com/faker-ruby/faker

しょーたしょーた

DB側の準備はできたので、次はフロント側との繋ぎ込み。

現状だともちろん色々足りていないので追加していく。まずはルーティング。
ふとここで疑問。

最初の環境構築の時postsって特に何も記載しなくてもルーティングとかできてたけどなんで…??

最初のマイグレーションファイル作成の際のコマンドを見てみた

docker-compose run --rm api bundle exec rails g scaffold post title:string

さっき実行したtodos

docker-compose run --rm api bundle exec rails g model Todo title:string description:text …

上下をにらめっこした結果→scaffoldの有無だと判明。
これについて調べてみる。
https://udemy.benesse.co.jp/development/system/scaffold.html

こちらによるとscaffoldはいろんな必要なファイルとかルーティングとかを自動でやってくれる優れものとのこと。こんなの便利なのがあったのかと落胆しつつちゃんと学ぶ良い機会ということで、scaffoldがやってくれることを一つ一つやっていくこととする。

しょーたしょーた

一度Laravelを触ったことがある浅ーい知識から「MVCってやつだよな!だとするとまずはControllerだよな!」と考えた。ということでまずはControllerを準備する。

docker-compose run --rm api bundle exec rails g controller todos
と
docker-compose run --rm api bundle exec rails g controller users
しょーたしょーた

まずはTodo側から
最初の環境構築でpostのコントローラーがあったのでこちらを参考に
いったんTodoテーブルの一覧を取得してみる。

todos_controller.rb
class TodosController < ApplicationController
  # GET /todos
  def index
    @todos = Todo.all

    render json: @todos
  end
end
routes.rb
Rails.application.routes.draw do
  resources :todos
end

postmanで確認するとデータが返ってきたのでひとまず一覧の取得はできた!

しょーたしょーた

今のルートだとlocalhost:3000/todosみたいな状態でフロント側のURLっぽい感じもあるので、間にapiとか入れたい。なのでroute.rbを編集する。

route.rb
Rails.application.routes.draw do
  scope 'api' do
    resources :todos
    resources :users
  end
end

最初scope 'api' doのところをnamespace :api doと書いていたところ全く動かなかった。
というのもnamespace :api do と書いてしまうとルーティングのURLだけでなくコントローラーへのパスもapi/todos#indexのようになってしまうためであった。また一つ勉強になった。

しょーたしょーた

ルーディングを調べているときにDBのデータ型に関する記事を見た。

ここで

マイグレーションしようとしたところ
deleted_at:datatime
のdatatimeというものはない!的なエラーが出たので、とりあえずこのカラムは消した。とはいえ必要なので後で追加する方針

のエラー原因が判明。datatimeではなくdatetimeだった…😭初歩的ミスでした。

しょーたしょーた

todoの方はCRUDできることを確認したので、userの方も同じようにする

userの方はパスワードがあるのでこれをハッシュ化する必要がありそう。調べるとbcryptというgemがよく使われるとのこと。早速こちらを導入。これはGemfileにもともと記述がありコメントアウトされている状態であった。なのでコメントアウトを解除。またこのタイミングで論理削除用のgemのparanoiaも一緒に入れておく

gem 'bcrypt', '~> 3.1.7'
gem 'paranoia'

今回はちゃんとdocker-compose buildからのbundle installをして無事ダウンロード完了。

早速bcryptをつかってハッシュ化の実装。こちらを参考にさせてもらいました。
https://freesworder.net/rails-gem-bcrypt/
実装は結構簡単ですることは

  1. userテーブルにpassword_digestカラムを追加(※この時すでにpasswordカラムがあれば削除する)
  2. Userモデルにhas_secure_passwordを追記
    たったこれだけ!めっちゃ簡単😊
しょーたしょーた

seeds.rbによるデータ挿入ではうまくいったが、postができない。というかgetもできなくなったので調査

しょーたしょーた

症状としては500エラー
has_secure_passwordを消すと問題なく動くのでbcrypt周辺が絶対に怪しい…

色々調べてもわからず。chatGPTにも聞いてみたが「ログを確認してください」としか言われず。
…ログってそもそもどこ見ればええねん!となったが500エラーになって返ってきてたjsonを見るとcannot load such file -- bcryptと書いてある。

…………ん?でもbcryptはGemfile,Gemfile.lockともに記載があることは確認したからインストールしてるはずなんだが。このエラーをchatGPTに投げると「サーバーを再起動してください」と。
まさかそんなことでなおるはずn ……なおった!!ということで今回の学びは

しょーたしょーた

これでUser,TodoともにCRUDができるようになったので、フロント側からAPI呼び出してデータを表示させる

しょーたしょーた

ルーティングの一覧は
http://localhost:3000/rails/info/routesで確認できる

しょーたしょーた


一覧取得できた!ただ締切日の表示が微妙なので、2023-03-01みたいな表示に変換していく

しょーたしょーた


希望通りの表示にできた!今回はフロント側でdayjsを使用して実装した。
実装する時に「tsxのreturnの中のmapループ内で定数とかって定義できるんだっけ?」となりchatGPTに聞いたところ

const MyComponent: React.FC = () => {
  const items = [1, 2, 3, 4, 5];

  return (
    <div>
      {items.map((item) => {
        const doubledItem = item * 2;
        return <p key={item}>{doubledItem}</p>;
      })}
    </div>
  );
};

こう書けば良いよ!と教えてくれたのでこのまま実装してみたところうまくいった😊

しょーたしょーた

普通にいけば次は新規追加ページの実装だが「Next.jsはダイナミックルーティングができるんだよな!!」となり実装してみたくなったので色々すっ飛ばして編集ページを実装する。URLは/edit/[id]としたいのでeditフォルダ内に[id].tsxを配置し実装を進める。

しょーたしょーた

以前見た本によるとダイナミックルーティングをするときはgetStaticPathsgetStaticPropsを使うと書いてあったのでひとまずこれに従う。
ということでgetStaticPathsで全Todoを取得しparamsにidを登録。getStaticPropsでアクセスされたidから個別のTodoを取得しそれをページへ反映させる。

getStaticPaths
export const getStaticPaths: GetStaticPaths = async () => {
    const response = await axios
        .get("http://api:3000/api/todos")
        .then((res) => {
            return res.data;
        })
        .catch((error) => {
            console.error(error);
        });
    const paths = await response.map((todo: Todo) => ({
        params: { id: todo.id.toString() },
    }));
    return { paths, fallback: false };
};
getStaticProps
export const getStaticProps: GetStaticProps = async (context) => {
    const id = context.params?.id;
    const data = await axios
        .get(`http://api:3000/api/todos/${id}`)
        .then((res: AxiosResponse): object => {
            return res.data;
        })
        .catch((error) => {
            console.error(error);
        });

    return {
        props: { id, data },
    };
};
しょーたしょーた


ひとまず個別の記事情報取得まではできた!編集や削除の機能はこれから。次は新規登録画面を作成する。

しょーたしょーた


ほぼ編集画面。登録ボタンがめちゃくちゃ長いのは気にしない。

しょーたしょーた

次は完了したTodo一覧画面。Todoテーブルにはis_completedカラムがあるので、is_completedがtrueのもののみを取得するような関数?モデル?メソッド?をtodoのコントローラーに追加する。

しょーたしょーた
  # GET/todos complete_only
  def completeTodos
    @todos = Todo.where(is_completed: true)

    render json: @todos
  end

Laravelでもあったwhereを使って実装。あとTodo一覧が完了・未完了関係なく全て取得していたので未完了のものだけを取得するように変更した。

しょーたしょーた

次は削除したTodo一覧。これはちょっと前に追加したParanoiaというgemを使う予定。使い方などを調べてみる。

しょーたしょーた

一旦postmanで確認する。
id:1のtodoに対してdeleteしてみた。

ちゃんとdeleted_atに時刻が入っていることを確認。

削除したTodo一覧のページを作りたいので、論理削除されたもののみを取得するやつをtodoコントローラーに追加する。

  # GET/todos delete_only
  def deleteTodos
    @todos = Todo.only_deleted

    render json: @todos
  end
しょーたしょーた


削除済み一覧ページも完了。これでフロント側の実装はできたのであとは機能実装を進める。
今できていない機能としては

  • ログイン・ログアウト
  • Todo新規登録
  • Todo編集
  • Todo完了⇄未完了
  • Todo削除⇄未削除に戻す

これらについて実装を進める。

しょーたしょーた

まずは簡単そうな

  • Todo完了⇄未完了
    から実装を進める。

完了についてはis_completedカラムをtrueにすれば良いだけなので、完了ボタンに対して

    const onClickCompleteTodo = async (id?: number) => {
        const isComplete = {
            todo: {
                is_completed: true,
            },
        };

        await axios
            .put(`http://api:3000/api/todos/${id}`,isComplete)
            .then((res: AxiosResponse) => {
                console.log(res.data);
            })
            .catch((error) => {
                console.error(error);
            });
    };

で一度試してみる。なおpostmanではこれでうまくいったので問題なくできるはず。

しょーたしょーた

できねぇ・・・・・
net::ERR_NAME_NOT_RESOLVEDというエラーが出てDockerのログを見ても通信すらしてなさそう。

しょーたしょーた

getStaticProps内ではhttp://api:3000/api/...というURLだが、今回の場合はhttp://localhost:3000/api/...となるみたい。でこのように記載したところ次はCORSエラーが出た。
今回は
フロント→localhost:8000
バック→localhost:3000
でドメインが異なる判定となるため出たようで。
これはバックエンド側でgemを追加することで解消できるみたい。

ということでgemを追加

Gemfile
gem 'rack-cors'
↑もともとコメントアウトの状態で記載されていたのでコメントアウトを解除した

で忘れずに、

docker-compose build
docker-compose run --rm api bundle exec bundle install
からのサーバー再起動

でlocalhost:8000からの接続を許可するために下記ファイルを編集

config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8000'

    resource '*',
             headers: :any,
             methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
↑これもコメントアウトですでに記載されていたものをコメントアウト解除した

これでaxios.get...などをするとエラーが解消された!

しょーたしょーた

Todo一覧と完了一覧にそれぞれis_completedのtrue,falseを切り替える関数を追記。
流れとしてはTableコンポーネントのprops→ButtonコンポーネントのonClick。

問題なく実装できたのでここは完了

  • ログイン・ログアウト
  • Todo新規登録
  • Todo編集
  • Todo完了⇄未完了
  • Todo削除⇄未削除に戻す

次は削除⇄未削除をやろうかな。

しょーたしょーた

削除⇄未削除については
削除→destroy
未削除→todo.restore
みたいな感じで実装できるみたいだからこれで進める。

削除はすでにtodosのコントローラーに記載済み。追加で未削除の方もおなじコントローラーに記載する。

  # DELETE REVERSE /todos/1
  def destroyReverse
    @todo = Todo.restore(params[:id])
  end
route.rb
get 'delete-reverse/', to: 'todos#destroyReverse'

ルートも書いたのでこれでいけるだろ!と余裕をかましていたところしっかりエラー。今回は404エラー。
......params[:id]と書いているのにルートにそれを受け取るところないやん。
ということで修正。

route.rb
get 'delete-reverse/:id', to: 'todos#destroyReverse'

これで問題なく削除→未削除への実装は完了。

※補足
最初todosコントローラーはこのように書いていた

todos_controller.rb
 before_action :set_todo, only: [:destroyReverse]

  # DELETE REVERSE /todos/1
  def destroyReverse
    @todo.restore
  end

private

  def set_todo
    @todo = Todo.find(params[:id])
  end

これだと

  def set_todo
    @todo = Todo.find(params[:id])
  end

この部分で論理削除されたものを探しにいってしまい、削除されている→そんなレコードはないと怒られる。
なのでidを直接指定するような形に変更した。

しょーたしょーた

↑で書いたコントローラーはひとまず未削除に戻すだけの処理だが、削除→未削除については「未完了に戻す」と「完了に戻す」の2種類あるのでそれぞれ処理を書いていく。

しょーたしょーた

と思ったけど、putを使ってフロント側からis_completedに関するデータも一緒に送信する方針にした

  def destroyReverse
    @todo = Todo.restore(params[:id])
    @todo = Todo.find(params[:id])
    @todo.update(todo_params)
    render json: @todo
  end

ここで

@todo = Todo.restore(params[:id])
@todo = Todo.find(params[:id])

としているが、@todo = Todo.restore(params[:id])これだけだと@todoにデータが入ってるけどupdateメソットが使えなかったので、削除→未削除にする→そのidのtodoを再取得→情報更新と言う流れにした

あとAPIルートはgetからputに変更した。

routes.rb
put 'delete-reverse/:id', to: 'todos#destroyReverse'

フロント側はこんな感じ

    const onClickDestroyReverse = async (id?: number) => {
        const isComplete = {
            todo: {
                is_completed: false,
            },
        };
        await axios
            .put(`http://localhost:3000/api/delete-reverse/${id}`, isComplete)
            .then((res: AxiosResponse) => {
                console.log(res.data);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                router.reload();
            });
    };

    const onClickDestroyReverseToComplete = async (id?: number) => {
        const isComplete = {
            todo: {
                is_completed: true,
            },
        };
        await axios
            .put(`http://localhost:3000/api/delete-reverse/${id}`, isComplete)
            .then((res: AxiosResponse) => {
                console.log(res.data);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                router.reload();
            });
    };

axiosのところ記載が重複しているけどひとまずこれで。(本当は良くないと思うけど…)

しょーたしょーた

やっぱり気になったのでまとめた

    const onClickDestroyReverse = async (id?: number) => {
        const isComplete = {
            todo: {
                is_completed: false,
            },
        };

        deleteReverseSend(isComplete, id);
    };

    const onClickDestroyReverseToComplete = async (id?: number) => {
        const isComplete = {
            todo: {
                is_completed: true,
            },
        };
        deleteReverseSend(isComplete, id);
    };

    const deleteReverseSend = async (sendData: object, id?: number) => {
        await axios
            .put(`http://localhost:3000/api/delete-reverse/${id}`, sendData)
            .then((res: AxiosResponse) => {
                console.log(res.data);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                router.reload();
            });
    };
しょーたしょーた
  • ログイン・ログアウト
  • Todo新規登録
  • Todo編集
  • Todo完了⇄未完了
  • Todo削除⇄未削除に戻す

次は新規登録と編集

しょーたしょーた

新規登録はreact-hook-formを使ってやろうと思うけど、今の作り的には以下のようになっている。
ページコンポーネント→LabelタグとInput関連のコンポーネント→inputタグ
で入力されるのはInput関連のコンポーネント内のinputタグだけれど、送信するのはページコンポーネントとなる。そうなるとuseFormを定義するのはページコンポーネントだが、registerとかはInput関連のコンポーネント内のinputタグになるのか…?????となりただいま混乱状態。。。

しょーたしょーた

調べてみるとreact-hook-formにはuseControllerというフックがあるようで。これを使えばコンポーネント間も行き来ができるようで……🤔

しょーたしょーた

【すこし脱線】
これまでaxiosとかの部分書く時、以下のように書いていた。

    const deleteReverseSend = async (sendData: object, id?: number) => {
        await axios
            .put(`http://localhost:3000/api/delete-reverse/${id}`, sendData)
            .then((res: AxiosResponse) => {
                console.log(res.data);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                router.reload();
            });
    };

これめちゃくちゃ間違っていると他の案件で指摘していただいた。
上記ではawaitとthenのどちらも使っているけど、基本的にasync/awaitとthenは共存しない。
なので上記は以下のように書きかえた。

    const deleteReverseSend = async (sendData: object, id?: number) => {
      try{
        await axios.put(`http://localhost:3000/api/delete-reverse/${id}`, sendData)
      } catch (error) {
        console.error(error);
      } finally {
        router.reload();
      }
  };
しょーたしょーた

useController使ったらなんかわからんけどできた。

基本的には以下の感じ。

  • ページコンポーネント
     これまで通りuseFormを定義。定義内容は以下の通り。
    const {
        handleSubmit,
        control,
        formState: { errors },
    } = useForm<Todo>();

ここでのポイントはcontrolというのを入れておくこと。
でこのcontrolは情報を取得したいコンポーネントのpropsに渡す。

<InputWithLabel
 labelText="ToDoタイトル"
 htmlfor="title"
 type="text"
 size="large"
 control={control}
 name={"title"}
/>
しょーたしょーた

InputWithLabelコンポーネント

propsから受け取ったname,controlは下記のようにuseControllerの引数に与える

    const { field } = useController({
        name,
        control,
    });

inputタグを含むInputコンポーネントは以下の通り

<Input
 size={size}
 disabled={disabled}
 id={htmlfor}
 type={type}
 defaultValue={defaultValue}
 inputRef={field.ref}
 onChange={field.onChange}
/>

ここではinputRefをpropsに指定し、そこにはfield.refを指定する

しょーたしょーた

Inputコンポーネント

Inputコンポーネント内のinputタグの内容は以下の通り。

<input
	type={type}
	id={id}
	className={classNames(styles.default, styles[size])}
	disabled={disabled}
	defaultValue={defaultValue}
	onChange={(e) => onChange(e.target.value)}
	ref={inputRef}
/>

refにpropsからのinputRef=field.refを指定している。

しょーたしょーた

とここまで設定方法を書いていったが、何がどうなっているのかさっぱりわかっていない。
ということで一つ一つ解読していこうと思う。

まずタグに指定したref
今の理解では「DOMにアクセスできる」くらいの理解なのでこれがしていることを理解していく。

しょーたしょーた

一般的にReactではref属性を使う時以下のような形で使う。

 const inputRef = useRef(null)

return 
 <input ref={inputRef} className='thisInput' />

こうすることでthisInputのクラス名のinputタグにアクセスできるようになる。
しかし今回のInputコンポーネントではuseRefは明示的に使用していない。
おそらくuseControllerの内部でuseRefが使われていると予測できる。

ということで次はuseControllerについて調べてみる。

しょーたしょーた

まずuseControllerには以下の引数を与えることができる。

  • name 型 : FieldPath
    どのinputかどうかを判定するために必要だから必須。idのような形になるので重複は不可。
  • control 型 : Control
    useForm を起動することにより提供される制御オブジェクト…これだけではよくわからないがuseFormのcontrolを使うなら入れることになるかなと
  • defaultValue 型 : unknown
    inputの初期値を設定する。これを設定するならuseFormのdefaultValueやdefaultValuesにundefinedを適用することは不可。
  • rules 型 : Object
    バリデーション
    required, min, max, minLength, maxLength, pattern, validate
    たとえばrules={{ required: true }}
  • shouldUnregister 型 : boolean = false
    送信しないかどうか…かな
しょーたしょーた

次はcontrol
ここが登録とかを制御しているはず。

まずは何か参照できるものないかなーとファイル漁ってみたところ、型定義があったのでそちらをみてみる。

export type Control<TFieldValues extends FieldValues = FieldValues, TContext = any> = {
    _subjects: Subjects<TFieldValues>;
    _removeUnmounted: Noop;
    _names: Names;
    _state: {
        mount: boolean;
        action: boolean;
        watch: boolean;
    };
    _reset: UseFormReset<TFieldValues>;
    _options: UseFormProps<TFieldValues, TContext>;
    _getDirty: GetIsDirty;
    _resetDefaultValues: Noop;
    _formState: FormState<TFieldValues>;
    _updateValid: (shouldUpdateValid?: boolean) => void;
    _updateFormState: (formState: Partial<FormState<TFieldValues>>) => void;
    _fields: FieldRefs;
    _formValues: FieldValues;
    _proxyFormState: ReadFormState;
    _defaultValues: Partial<DefaultValues<TFieldValues>>;
    _getWatch: WatchInternal<TFieldValues>;
    _updateFieldArray: BatchFieldArrayUpdate;
    _getFieldArray: <TFieldArrayValues>(name: InternalFieldName) => Partial<TFieldArrayValues>[];
    _executeSchema: (names: InternalFieldName[]) => Promise<{
        errors: FieldErrors;
    }>;
    register: UseFormRegister<TFieldValues>;
    unregister: UseFormUnregister<TFieldValues>;
    getFieldState: UseFormGetFieldState<TFieldValues>;
};

なにやらいっぱいあるがこの中にregisterがあるのに着目。これまでならuseFormでの定義の際に記載していたregisterだが、ここで型の定義がしてあるということはその機能はcontrolが担っていると考えられる。

しょーたしょーた

ここまでの理解でざっくり流れを整理する

  1. ページコンポーネントでuseFormからcontrolを参照。
     各中間コンポーネントにcontrolを渡すことでregisterの機能などを付与する。
     また合わせてnameも渡すことでそのコンポーネントに名前をつけ重複を防ぐ。
  2. ページコンポーネントから受け取った各種情報をuseControllerを使って登録する。
     登録した情報をさらに下層のコンポーネントに渡す。
  3. 中間コンポーネントから受け取った情報はinputタグに設置。
     ここで入力された情報はここまでの流れの逆順でuseFormのhandleSubmit時のdataとして登録される。

理解間違ってるところありそうだけど、大まかな流れはこんな感じかな。
今回は中間コンポーネントに複数のinputタグが入ることはないけど、中間コンポーネントに複数のinputが入る場合、name属性はページコンポーネントではなく中間コンポーネントで定義する必要がありそう。(でないと重複を許すことになる)
あくまで1のinputタグに対して1つのnameが原則かと。

しょーたしょーた

ということで新規登録と編集機能は実装完了。

  • ログイン・ログアウト
  • Todo新規登録
  • Todo編集
  • Todo完了⇄未完了
  • Todo削除⇄未削除に戻す

最後はログイン・ログアウト

このスクラップは2024/03/24にクローズされました