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、余白、ボタンなどはコンポーネント化している。
次はToDo一覧ページ
このページを作成しているときに、「ここのデータくらいは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 |
【脱線】chatGPTはとても便利
ということで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内のファイルを更新したらこのコマンドを実行することにする。
たぶんこのやり方違う気しかしないのでちゃんと調べてみた。
こちらの記事を参考にさせてもらった
今回は個人開発なのでファイル直接編集&resetしまくってるけど、結構危険なことしていたようで。
共同開発するときはマイグレーションファイルを追加した方良いのだとわかった。
todosテーブルと合わせてusersテーブルも合わせて作成した。
テーブルを作ったのでダミーデータをseeds.rbより入れる。一つ一つ手入力でも良いのだけどダミーデータ生成のfakerというgemがあるようなので、今回はこれを使ってみる。
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が原因か…?」となったのでそちら路線で再調査。
調べるとこんな記事を発見
まさにこれだ!!!となり
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の公式ドキュメントによるといろんな種類のダミーデータがあるので、これでダミーデータには困らなさそう。
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
の有無だと判明。
これについて調べてみる。
こちらによると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テーブルの一覧を取得してみる。
class TodosController < ApplicationController
# GET /todos
def index
@todos = Todo.all
render json: @todos
end
end
Rails.application.routes.draw do
resources :todos
end
postmanで確認するとデータが返ってきたのでひとまず一覧の取得はできた!
今のルートだとlocalhost:3000/todosみたいな状態でフロント側のURLっぽい感じもあるので、間にapiとか入れたい。なので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をつかってハッシュ化の実装。こちらを参考にさせてもらいました。
実装は結構簡単ですることは- userテーブルに
password_digest
カラムを追加(※この時すでにpassword
カラムがあれば削除する) - 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を配置し実装を進める。
以前見た本によるとダイナミックルーティングをするときはgetStaticPaths
とgetStaticProps
を使うと書いてあったのでひとまずこれに従う。
ということでgetStaticPaths
で全Todoを取得しparamsにidを登録。getStaticProps
でアクセスされたidから個別のTodoを取得しそれをページへ反映させる。
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 };
};
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を使う予定。使い方などを調べてみる。
Paranoiaについてはこちらを参考にさせてもらいました。
一旦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を追加
gem 'rack-cors'
↑もともとコメントアウトの状態で記載されていたのでコメントアウトを解除した
で忘れずに、
docker-compose build
docker-compose run --rm api bundle exec bundle install
からのサーバー再起動
でlocalhost:8000からの接続を許可するために下記ファイルを編集
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
get 'delete-reverse/', to: 'todos#destroyReverse'
ルートも書いたのでこれでいけるだろ!と余裕をかましていたところしっかりエラー。今回は404エラー。
......params[:id]と書いているのにルートにそれを受け取るところないやん。
ということで修正。
get 'delete-reverse/:id', to: 'todos#destroyReverse'
これで問題なく削除→未削除への実装は完了。
※補足
最初todosコントローラーはこのように書いていた
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
に変更した。
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が担っていると考えられる。
ここまでの理解でざっくり流れを整理する
- ページコンポーネントでuseFormからcontrolを参照。
各中間コンポーネントにcontrolを渡すことでregisterの機能などを付与する。
また合わせてnameも渡すことでそのコンポーネントに名前をつけ重複を防ぐ。 - ページコンポーネントから受け取った各種情報をuseControllerを使って登録する。
登録した情報をさらに下層のコンポーネントに渡す。 - 中間コンポーネントから受け取った情報はinputタグに設置。
ここで入力された情報はここまでの流れの逆順でuseFormのhandleSubmit時のdataとして登録される。
理解間違ってるところありそうだけど、大まかな流れはこんな感じかな。
今回は中間コンポーネントに複数のinputタグが入ることはないけど、中間コンポーネントに複数のinputが入る場合、name属性はページコンポーネントではなく中間コンポーネントで定義する必要がありそう。(でないと重複を許すことになる)
あくまで1のinputタグに対して1つのnameが原則かと。
ということで新規登録と編集機能は実装完了。
- ログイン・ログアウト
Todo新規登録Todo編集Todo完了⇄未完了Todo削除⇄未削除に戻す
最後はログイン・ログアウト