- 投稿日:2019-11-15T23:50:36+09:00
Ruby on Rails ToDoアプリ作成【CRUD】勉強会 #1
初心者用Railsアプリ練習帳
勉強会を開き、Rails初学者に日頃教えています。初心者向け勉強回資料用もかねて作成。
ある程度基礎を学習していると、読みながら真似はできるけど、いまいち全体の知識がつながっていないので自分で作ることができない…そんな状態になりがちでした。
対象者
- Railsでなんでもいいからの教材をやった人
- Rails基礎はなんとなくわかってきた人
- Railsで自分でアプリ作るのはまだ無理そうという人
- Rails以外のフレームワークならわかる人
※ 正確さより概念とアプリ作成の流れを重視します。
コンセプト
他にも言っている方がいますが、勉強会で教えている経験から初学者が同じ教材を何周もするのは上達が遅いように感じています。
知識はあるけど自分で一から作るのは無理そう。
エラーが出て対処出来ないということになりがちでした。
こういう場合、実際に手を動かし、自分の頭で考えて簡単なアプリを作る必要があるけど、
- いきなりだと何を作ればいいのかわからない…
- 一連のアプリを作っていく順番がわからない…
- エラーでても対処できない…
- 簡単なのから作ってみればいいと言うが何が簡単なものなのか分からない…
- 何個もアプリ自分で作るといいらしいが何を作れば練習やスキル向上なるか分からない…
そんな人が未経験からプログラミングをやる人には何人もいました。
現状の私の考えですが、世の中にある教材は見た目とか完成度ないと売れないし、強い人達からツッコまれるので分かりやすさより精確さが必要とされる教材になってしまうようにおもいます。なので、後で身につければ良いこともあれもこれも付け足しており、本当にコレだけ!
という事項が初心者には見えにくくなっている気がします。それをうけて今回、ムダなものとにかく剥ぎまくって、コアな部分だけを数多く練習すればみんな出来るようになるのではないかと考え、練習問題を作りました。
今後も感想とかもらって継続的にブラッシュアップしていければと思っています。手順
- とりあえずToDoアプリをなぞって作ってみる(少しだけでも勉強した人が前提) ←今回はココ
- 超シンプルアプリを量産。手を動かして覚える!概念つかむ!
- データベースから欲しいデータを取得する!千本ノックのようにやりまくります!
2,3は個人的に開く勉強会で解説します。
とりあえずToDoアプリをなぞって作ってみる
環境構築はローカルでもいいですけど、WindowsやMacで違うので初心者はcloud9で作っていいと思います。
構築方法は今回省略以下のコマンドでアプリを作成。プロジェクト名になります。なんでもいいです。
今回、私はrails-todo1
にしました$ rails new rails-todo1沢山のファイルが作られる。終わったらプロジェクトのディレクトリに移動
$ cd rails-todo1サーバーを起動させて表示させてみる
$ rails sこんな画面が出たらOK
これから中身を作っていくので一応流れを確認
今回新規作成で一から作っていきますが、だいたいこんな感じで作ると考えていただければよいかと思います。
新しいアプリ(ページ)を作る手順
全体の流れで考えること
- 作るページはどんなページで作る?
- データベースに保存するデータは何か?
- モデルは作ったか?
- マイグレーションファイルを生成&データベースに反映
generate modelコマンドするとmigrationファイルも作られる。
カラム(データベースの列)を追加したり、データベースの中身を変えたいときにもマイグレーションファイル使う
rails db:migrate
でマイグレーションファイルの内容をデータベースに反映ページを表示するのか?データを登録・更新をするのか?
表示するならGET
データの登録ならPOST
データの更新ならPATCH (PUT)
データの削除ならDELETE
ルーティングを決める
URLにパラメータをいれる必要があるのか?(例えば各ユーザーの情報ページを表示するならparams[:id]のように書く。ならばURLに〇〇〇/:idのようにidを含める必要がある)
URLにパラメータいらない(新規登録や全データ表示など)
処理を行うコントローラーは作ったか
コントローラーのアクションは作ったか
GET?POST?PATCH?DELETE?
かを確認してつくるビューファイルはいるか?
さっそく簡単なアプリ作成していく
1.どんなもの作るか
2.データベースに保存するデータは何か
- テーブル名:Todo
- カラム名:title
- デフォルトで作られるカラム(ID, 作成日, 更新日)
3. モデルは作ったか
今回は、まだ何もつくってないので当然必要。
今回作るToDoアプリでは、Todoテーブルのデータ取得・新規登録・データ更新・データ削除
を行うのでModelを1つ作成しますというわけで、モデル作成。
$ rails generate model Todo title:string2.で書いたように必要なテーブルが
Todo
必要なカラムがtitle
なので、このように書いています。①
rails generate model Todo
でモデルファイルが作られる
②title:string
部分は追加したいカラム名と型。
この部分は書かなくても手動で記述すれば作れるのですが、コマンドでこのように打ったほうが楽ですdb/migrate/~~~~~~~~~_create_todos.rbclass CreateTodos < ActiveRecord::Migration[5.2] def change create_table :todos do |t| t.string :title t.timestamps end end end補足:モデルとは
普通はデータベースからデータを取得するときには
SQL
というものを書かないといけなくて初心者にはそれなりに複雑。
Railsではmodelというものを使うとSQL書かなくてもデータ取得できて便利。4. マイグレーションファイルを生成&データベースに反映
今回新規作成なのでモデルをつくるとmigrationファイルも作られるのでコマンドでmigrationファイルを作る必要ありません。
マイグレーションファイルの内容をデータベースに反映させるコマンド打つ
$ rails db:migrate補足:migrationファイル作る必要あるときは
migrationファイルとは:
データベースのテーブルの作成、変更を記述するためのファイルで、rails db:migrate
を実行することでデータベースに反映させる。
【コマンド詳細】カラムの追加。今回は実行必要なし
$ rails generate migration add_password_digest_to_users password_digest:stringこんな感じのコマンド入れてカラムの追加とかやります。
今回は当然新規必要なし
5. ページを表示するのか?データを登録・更新をするのか?
→ 表示するならGET
→ データの登録ならPOST
→ データの更新ならPATCH (PUT)
→ データの削除ならDELETE
Todoを追加するための機能を持つページを「表示」します。
ということでGET
メソッドです6. ルーティングを決める
ルーティングは
どのURLにアクセスすると、どのコントローラーとアクションを使うのか定義するものです。Railsには
resources
という便利機能があってルーティング書くのを便利にできる機能があるんですが、今回は原理を理解するために今回は一つ一つルーティングを記述していきましょう。config/routes.rbget '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create'ルーティングを確認する
コンソールに↓を入力するとルーティングを確認できる。
$ rails routesPrefix Verb URI Pattern Controller#Action todos GET /todos(.:format) todos#index POST /todos(.:format) todos#createもしくは
/rails/info/routes
をアドレスバーにつけると少し見やすい画面になる
http://sample.com/rails/info/routes
みたいな感じ
他タブで開いておくといちいちコマンド入れなくても確認できるので便利!現状では
/todos
にGETを設定してtodoコントローラー
のindexアクション
の処理を使えと指定している補足:_pathについて
$ rails routes
だとPrefix
がtodos
という名前になっていてURLに名前を付けて便利に使えるようになっている。これに_path
を追加すると使えます。
todos
+_path
でtodos_path
です。
todos_path
と書くと/todos
と書いたのと同じことになります。
結構使いますので覚えておくといいです。7. 処理を行うコントローラーは作ったか
コントローラーは大雑把に
ルーティング
とモデル
とビュー
をつなぐ役割、データ返す役割をしているものです。コントローラーは何をするか雑に説明
細かく言うとわかりにくくなるので大雑把に。
- ルーティングで該当のURLにマッチしたら、コントローラーのアクションに処理が飛んでくる。そして処理を行う。
- その際に
params
のデータもらうこともありTodo.new
使ってビューファイルで記述するform_with
(form_forも含む)で使うフォームでデータを送信するために必要なモデルの部品みたいなものが入った箱作ったりする。
- ビューで使うならインスタンス変数に格納する
Todo.find(params[:id])
みたいに自分で作ったModel
使ってデータベースの値を取得する。
Todo.find(params[:id])
はTodo.new
で作った部品に、取得したデータも含んだものだと思っておけばいいです。※下図参照- ビューで使うならインスタンス変数に格納する
- インスタンス変数はビューで使用可能なのでページを操作によってデータ表示やフォームの送り先を変えられる動的なページを作れる
初心者のうちはデータ取得したり、データを加工する、ビューで表示するためのデータを
@todo
みたいなインスタンス変数を作る!
みたいな感じでいいと思います。コントローラー作成コマンド
以下コマンドで、Controllerを作成します。
とりあえずコントローラーを生成する
$ rails g controller Todosこのコマンドならtodosというコントローラー作成されるので、
todos_controller.rb
というファイルができます。8. コントローラーのアクションは作ったか
app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all end endコントローラーの処理を書いてみました。
Todoテーブルに入っているデータを全件取得(allメソッドを使う)してインスタンス変数の@todos
に入れています。9. ビューファイルはいるか?
ビューファイルは表示部分です。
htmlっぽい書き方だけどコントローラーなどで処理したインスタンス変数などを使って静的なページでなく動的なページを作ることができます。さきほど、
@todos
というインスタンス変数をコントローラーのindexアクションに書いたので、これを作って全データを表示してみましょう。app/views/todos/index.html.erb<h1>ToDo一覧</h1> <table> <thead> <tr> <th>タスク</th> </tr> </thead> <tbody> <% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> </tr> <% end %> </tbody> </table>作成した画面
まだデータの登録がないので全件取得しても表示するものがないので、このような表示になっています。
こんな1~9までのような順序でアプリの作成(もしくは追加)をしていくことになります。
では、次にTodoの全件表示しかできない状態なので
次は新規ToDo追加できる機能を追加します新規追加機能の作り方
次に新規登録できる機能を作ります。
今度は1~9まの順序で機能を追加していきましょう。1.どんなもの作るか
ToDoテーブルにデータを新規追加する機能
テキストフィールドに入力した文字をボタン押すとデータベースに登録2.データベースに保存するデータは何か
テーブル名:Todo
カラム名:title
デフォルトで作られるカラム(ID, 作成日, 更新日)
title
だけデータベースに保存すればいいことになります。3. モデルは作ったか
モデルはTodoつかうので新しく作成の必要なし
4. マイグレーションファイルを生成&データベースに反映
モデルはTodo使う。カラムの追加も必要ないのでマイグレーションファイル作る必要も、データベースに反映する必要もない。
5. ページを表示するのか?データを登録・更新をするのか?
カラム名で
title
をデータベースに登録する処理を追加することになります。ページの表示をしているわけでも、情報の更新をしているわけでもなく、情報の削除でもありません。
こういうデータの登録処理は
POST
で行いましょう。6. ルーティングを決める
- メソッド:
POST
- ルーティング:
/tasks
- コントローラー&アクション
tasks#create
ルーティングを確認
実はすでに追加してしまっていたのですが、今回使うルーティングは↓の二つ目です。
routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' endページを表示しないで新規登録を行うのでPOSTですね。
7. 処理を行うコントローラーは作ったか
新しいモデルを作ったわけでも、わざわざ新しいコントローラーをつくる必要もありません。
todo
テーブルのToDoのデータ追加処理を追加するのでコントローラーはtodos_controller.rb
に追記すればOKです。8. コントローラーのアクションは作ったか
データ追加処理を記述したアクションを
todos_controller.rb
に追記していきましょう。コントローラーにアクションを追加
create
を追加しました。新規登録を行うためのアクションになります。app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all @todo = Todo.new # フォーム作るために必要 end def create @todo = Todo.create(todo_params) redirect_to todos_path end end
redirect_to todos_path
としているのは、ページを表示しているわけでないのでリダイレクトしないと処理が終わっても処理が固まった感じになってしまうからです。処理としては正常に終了してるけど、↓のように何も起こってない…みたいな感じになる。
正常に登録はできているのでリロードするとちゃんと表示されることになる。
get '/todos', to: 'todos#index', as: 'todos'
のas: 'todos'
と書くと/todos
というURLにtodos
という名前を付けて使えるようにしています。この名前に、
ルーティングの名前(todosなど)
+_path
でtodos_path
のようにするといちいちURLを書く必要がなくなります。注意点として、Formで入力した情報を新規登録や更新する際はセキュリティ対策として
Strong Parameters
を使わないとエラーでますので追加します。Strong Parameters
セキュリティ対策で、入れないとエラーになります。
難しいこと省いて、フォームから受け取ったデータを渡すと、許可したデータだけ返却されるくらい覚えておけばとりあえずは良いと思います。そして追加したのが
todo_params
メソッド。また、private
を使うと、todo_paramsメソッドが外部から使えないようにできるのでセキュリティを高まります。
難しいことはいいので、privateの下にStrong Parameters
いれるんだなくらいで今はいいです。app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all end def create @todo = Todo.create(todo_params) redirect_to todos_path end private def todo_params params.require(:todo).permit(:title) end end9. ビューファイルはいるか?
データベースでのデータの追加処理では使いません。
ただし、送信するためのテキスト入力フォーム
と送信ボタン
をindex.html.erb
に登録したいと思います。テキスト入力フォームと送信ボタン
index.html.erb<h1>ToDo一覧</h1> <table> <thead> <tr> <th>タスク</th> </tr> </thead> <tbody> <% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> </tr> <% end %> </tbody> </table> <h2>ToDo追加</h2> <%= form_with model: @todo do |f| %> <div><%= f.label :title %></div> <div><%= f.text_field :title %></div> <div><%= f.submit %></div> <% end %>フォームを作るために
form_with
を使っています。
また、使うmodelを指定する必要があるので@todo
を指定してます。
コントローラーのindexアクションで追加します。
f.label
のようにすると「Title」というラベル(文字表示)され、
f.text_field
は文字を入力するフォームを作ることができる。
f.submit
は送信ボタン。
todos_controller
のindex
にフォームを作るための部品を用意するため@todo = Todo.new
を追加app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all @todo = Todo.new # フォーム作るために必要 end end登録機能完成
データベースに登録する機能が完成しました!
今度は更新と削除
次は更新機能を実装します。
必要機能
- 情報更新ページ(今回はeditアクションとedit.html.erbにて)
- ルーティングを追加する
- editアクションで編集する情報を表示する todos_controller.rb
- ビューで見た目を作る edit.html.erb
- 情報更新処理
- updateアクションで情報の更新処理 todos_controller.rb
更新情報編集ページ用ルーティング追加
routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo' end
get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo'
を追加しました。
todos_controller.rb
のedit
アクションを使用します。ルーティングの名前はedit_todo
とでもしておきます。更新情報編集ページ用コントローラー
editアクションは、情報編集画面(/todos/:id/edit)にアクセスすると処理されるアクションで、ビューで見た目を作るときに必要なデータの取得します。
editアクションを情報を追加します。
todos_controller.rbdef edit @todo = Todo.find(params[:id]) end
@todo
はインスタンス変数。@
とつけることでビューでもデータを使えるようになります。find
はデータベースのid
カラムからデータを探し出すときに使います。
- idはtodoを追加したときに自動的に採番されていきます。データベースの中身は下の表みたいになる。
- 編集画面は(/todos/:id/edit)のようにルーティングで設定した。
- 例えばURLが
http://sample.com/todos/1/edit
だとするとparams[:id]
は1
http://sample.com/todos/3/edit
だとするとparams[:id]
は3
http://sample.com/todos/1/edit
だとすると@todo = Todo.find(1)
と書いているのと同じになる試しに登録したデータベースの内容が↓になります。
id title created_at updated_at 1 テスト 2019-10-18 04:05:36.776003 2019-10-18 04:05:36.776003 2 買い物 2019-10-18 04:05:43.090180 2019-10-18 04:05:43.090180 3 勉強 2019-10-18 04:05:51.098753 2019-10-18 04:05:51.098753
@todo = Todo.find(params[:id])
でid
が1
だったら取得するデータをbyebugというデバッグツールで確認してみると
#<Todo id: 1, title: "テスト", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 04:05:36">
というデータが取れます。
ここで注意点
教えていて勘違いしている方が多かったので補足。
今回
title
カラムのデータを取得したいわけです。
@todo = Todo.find(params[:id])
をするとtitle
のデータだけ
を取得しているのではありません。意図的にデータを絞り込まなければ、今回の場合
id
,title
,created_at
,updated_at
1レコード(1行)分のデータ取得されます。
@todo = Todo.find(params[:id])
idは1とすると正:
#<Todo id: 1, title: "テスト", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 04:05:36">
誤:
#<Todo title: "テスト">
自分が必要だと思っているデータだけ取得できてると勘違いしている??ように思えましたので補足。
こらへんはSQLを理解していないから起こることなのかとも思います。
気を付けましょう。更新処理アクション追加
ルーティングとコントローラーに追記していきます。
resources使うともう少しシンプルに書けますが、今回は原理を知るためしっかりと書いていきます。routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo' patch '/todos/:id', to: 'todos#update' # 更新処理用ルーティング追加 endapp/controllers/todos_controller.rbdef update @todo = Todo.find(params[:id]) @todo.update(todo_params) redirect_to todos_path endresouses使えるならこんな感じでシンプルに書ける
<-- resousesを使えるときなら --> <h1>ToDo編集</h1> <%= form_with model: @todo do |f| %> <div><%= f.label :title %></div> <div><%= f.text_field :title %></div> <div><%= f.submit %></div> <% end %> <%= link_to '戻る', todos_path %>今回原理をよく理解するためにこのようにURLとメソッドもちゃんと書きました。
#{ }
は文字列として変数を埋め込めるRubyの記法です。
変更するToDoデータのid
を指定すれば同じことを行えます。edit.html.erb<h1>ToDo編集</h1> <%= form_with(model: @todo, url: "/todos/#{@todo.id}", method: :patch) do |f| %> <div><%= f.label :title %></div> <div><%= f.text_field :title %></div> <div><%= f.submit %></div> <% end %> <%= link_to '戻る', todos_path %>編集ページへのリンクを作る
変更箇所付近だけ抜粋。
link_to
と~~~~_path
を使ってやるとこんな風に簡単に作れる。index.html.erb<% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> <td><%= link_to '編集', edit_todo_path(todo) %></td> </tr> <% end %><td><%= link_to '編集', edit_todo_path(todo.id) %></td>ついでに、ちゃんと省略して書かないとこういう書き方になる。
todo
→todo.id
Railsがいい感じで判断してくれるのでこのようになりますが、原理は覚えておくほうがいいと思います。
htmlだと↓のようなものが作られる
<a href="/todos/1/edit">編集</a>
削除機能実装
削除機能はわかってしまえば他と大して変わらないので楽です。
1. ルーティング追加
2. コントローラーにdestroy
アクション追加
3. ビューに削除ボタンのリンク追加routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo' patch '/todos/:id', to: 'todos#update' delete '/todos/:id', to: 'todos#destroy' end
@todo = Todo.find(params[:id])
で削除する項目(idで指定された項目)を@todoに入れる- destroyメソッドで削除する
- 元のページに戻る
app/controllers/todos_controller.rbdef destroy @todo = Todo.find(params[:id]) @todo.destroy redirect_to todos_path end
<td><%= link_to '削除', "/todos/#{todo.id}", method: :delete %></td>
を追加。
method: :delete
でメソッドを指定。(指定しないとデフォルトのGETになります。)
ルーティングでdeleteに設定したURLはdelete '/todos/:id', to: 'todos#destroy'
なので対応させるために"/todos/#{todo.id}"
このように書きました。index.html.erb<% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> <td><%= link_to '編集', edit_todo_path(todo) %></td> <td><%= link_to '削除', "/todos/#{todo.id}", method: :delete %></td> </tr> <% end %>削除機能が完成しました。
見た目は最悪ですが、ToDo登録・更新・削除機能が完成しました。
- 投稿日:2019-11-15T23:50:36+09:00
とりあえずこれだけ。Rails ToDoアプリ作成【CRUD】勉強会 #1
初心者用Railsアプリ練習帳
勉強会を開き、Rails初学者に日頃教えています。初心者向け勉強回資料用もかねて作成。
ある程度基礎を学習していると、読みながら真似はできるけど、いまいち全体の知識がつながっていないので自分で作ることができない…そんな状態になりがちでした。
対象者
- Railsでなんでもいいからの教材をやった人
- Rails基礎はなんとなくわかってきた人
- Railsで自分でアプリ作るのはまだ無理そうという人
- Rails以外のフレームワークならわかる人
※ 正確さより概念とアプリ作成の流れを重視します。
コンセプト
他にも言っている方がいますが、勉強会で教えている経験から初学者が同じ教材を何周もするのは上達が遅いように感じています。
知識はあるけど自分で一から作るのは無理そう。
エラーが出て対処出来ないということになりがちでした。
こういう場合、実際に手を動かし、自分の頭で考えて簡単なアプリを作る必要があるけど、
- いきなりだと何を作ればいいのかわからない…
- 一連のアプリを作っていく順番がわからない…
- エラーでても対処できない…
- 簡単なのから作ってみればいいと言うが何が簡単なものなのか分からない…
- 何個もアプリ自分で作るといいらしいが何を作れば練習やスキル向上なるか分からない…
そんな人が未経験からプログラミングをやる人には何人もいました。
現状の私の考えですが、世の中にある教材は見た目とか完成度ないと売れないし、強い人達からツッコまれるので分かりやすさより精確さが必要とされる教材になってしまうようにおもいます。なので、後で身につければ良いこともあれもこれも付け足しており、本当にコレだけ!
という事項が初心者には見えにくくなっている気がします。それをうけて今回、ムダなものとにかく剥ぎまくって、コアな部分だけを数多く練習すればみんな出来るようになるのではないかと考え、練習問題を作りました。
今後も感想とかもらって継続的にブラッシュアップしていければと思っています。手順
- とりあえずToDoアプリをなぞって作ってみる(少しだけでも勉強した人が前提) ←今回はココ
- 超シンプルアプリを量産。手を動かして覚える!概念つかむ!
- データベースから欲しいデータを取得する!千本ノックのようにやりまくります!
2,3は個人的に開く勉強会で解説します。
とりあえずToDoアプリをなぞって作ってみる
環境構築はローカルでもいいですけど、WindowsやMacで違うので初心者はcloud9で作っていいと思います。
構築方法は今回省略以下のコマンドでアプリを作成。プロジェクト名になります。なんでもいいです。
今回、私はrails-todo1
にしました$ rails new rails-todo1沢山のファイルが作られる。終わったらプロジェクトのディレクトリに移動
$ cd rails-todo1サーバーを起動させて表示させてみる
$ rails sこんな画面が出たらOK
これから中身を作っていくので一応流れを確認
今回新規作成で一から作っていきますが、だいたいこんな感じで作ると考えていただければよいかと思います。
新しいアプリ(ページ)を作る手順
全体の流れで考えること
- 作るページはどんなページで作る?
- データベースに保存するデータは何か?
- モデルは作ったか?
- マイグレーションファイルを生成&データベースに反映
generate modelコマンドするとmigrationファイルも作られる。
カラム(データベースの列)を追加したり、データベースの中身を変えたいときにもマイグレーションファイル使う
rails db:migrate
でマイグレーションファイルの内容をデータベースに反映ページを表示するのか?データを登録・更新をするのか?
表示するならGET
データの登録ならPOST
データの更新ならPATCH (PUT)
データの削除ならDELETE
ルーティングを決める
URLにパラメータをいれる必要があるのか?(例えば各ユーザーの情報ページを表示するならparams[:id]のように書く。ならばURLに〇〇〇/:idのようにidを含める必要がある)
URLにパラメータいらない(新規登録や全データ表示など)
処理を行うコントローラーは作ったか
コントローラーのアクションは作ったか
GET?POST?PATCH?DELETE?
かを確認してつくるビューファイルはいるか?
さっそく簡単なアプリ作成していく
1.どんなもの作るか
2.データベースに保存するデータは何か
- テーブル名:Todo
- カラム名:title
- デフォルトで作られるカラム(ID, 作成日, 更新日)
3. モデルは作ったか
今回は、まだ何もつくってないので当然必要。
今回作るToDoアプリでは、Todoテーブルのデータ取得・新規登録・データ更新・データ削除
を行うのでModelを1つ作成しますというわけで、モデル作成。
$ rails generate model Todo title:string2.で書いたように必要なテーブルが
Todo
必要なカラムがtitle
なので、このように書いています。①
rails generate model Todo
でモデルファイルが作られる
②title:string
部分は追加したいカラム名と型。
この部分は書かなくても手動で記述すれば作れるのですが、コマンドでこのように打ったほうが楽ですdb/migrate/~~~~~~~~~_create_todos.rbclass CreateTodos < ActiveRecord::Migration[5.2] def change create_table :todos do |t| t.string :title t.timestamps end end end補足:モデルとは
普通はデータベースからデータを取得するときには
SQL
というものを書かないといけなくて初心者にはそれなりに複雑。
Railsではmodelというものを使うとSQL書かなくてもデータ取得できて便利。4. マイグレーションファイルを生成&データベースに反映
今回新規作成なのでモデルをつくるとmigrationファイルも作られるのでコマンドでmigrationファイルを作る必要ありません。
マイグレーションファイルの内容をデータベースに反映させるコマンド打つ
$ rails db:migrate補足:migrationファイル作る必要あるときは
migrationファイルとは:
データベースのテーブルの作成、変更を記述するためのファイルで、rails db:migrate
を実行することでデータベースに反映させる。
【コマンド詳細】カラムの追加。今回は実行必要なし
$ rails generate migration add_password_digest_to_users password_digest:stringこんな感じのコマンド入れてカラムの追加とかやります。
今回は当然新規必要なし
5. ページを表示するのか?データを登録・更新をするのか?
→ 表示するならGET
→ データの登録ならPOST
→ データの更新ならPATCH (PUT)
→ データの削除ならDELETE
Todoを追加するための機能を持つページを「表示」します。
ということでGET
メソッドです6. ルーティングを決める
ルーティングは
どのURLにアクセスすると、どのコントローラーとアクションを使うのか定義するものです。Railsには
resources
という便利機能があってルーティング書くのを便利にできる機能があるんですが、今回は原理を理解するために今回は一つ一つルーティングを記述していきましょう。config/routes.rbget '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create'ルーティングを確認する
コンソールに↓を入力するとルーティングを確認できる。
$ rails routesPrefix Verb URI Pattern Controller#Action todos GET /todos(.:format) todos#index POST /todos(.:format) todos#createもしくは
/rails/info/routes
をアドレスバーにつけると少し見やすい画面になる
http://sample.com/rails/info/routes
みたいな感じ
他タブで開いておくといちいちコマンド入れなくても確認できるので便利!現状では
/todos
にGETを設定してtodoコントローラー
のindexアクション
の処理を使えと指定している補足:_pathについて
$ rails routes
だとPrefix
がtodos
という名前になっていてURLに名前を付けて便利に使えるようになっている。これに_path
を追加すると使えます。
todos
+_path
でtodos_path
です。
todos_path
と書くと/todos
と書いたのと同じことになります。
結構使いますので覚えておくといいです。7. 処理を行うコントローラーは作ったか
コントローラーは大雑把に
ルーティング
とモデル
とビュー
をつなぐ役割、データ返す役割をしているものです。コントローラーは何をするか雑に説明
細かく言うとわかりにくくなるので大雑把に。
- ルーティングで該当のURLにマッチしたら、コントローラーのアクションに処理が飛んでくる。そして処理を行う。
- その際に
params
のデータもらうこともありTodo.new
使ってビューファイルで記述するform_with
(form_forも含む)で使うフォームでデータを送信するために必要なモデルの部品みたいなものが入った箱作ったりする。
- ビューで使うならインスタンス変数に格納する
Todo.find(params[:id])
みたいに自分で作ったModel
使ってデータベースの値を取得する。
Todo.find(params[:id])
はTodo.new
で作った部品に、取得したデータも含んだものだと思っておけばいいです。※下図参照- ビューで使うならインスタンス変数に格納する
- インスタンス変数はビューで使用可能なのでページを操作によってデータ表示やフォームの送り先を変えられる動的なページを作れる
初心者のうちはデータ取得したり、データを加工する、ビューで表示するためのデータを
@todo
みたいなインスタンス変数を作る!
みたいな感じでいいと思います。コントローラー作成コマンド
以下コマンドで、Controllerを作成します。
とりあえずコントローラーを生成する
$ rails g controller Todosこのコマンドならtodosというコントローラー作成されるので、
todos_controller.rb
というファイルができます。8. コントローラーのアクションは作ったか
app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all end endコントローラーの処理を書いてみました。
Todoテーブルに入っているデータを全件取得(allメソッドを使う)してインスタンス変数の@todos
に入れています。9. ビューファイルはいるか?
ビューファイルは表示部分です。
htmlっぽい書き方だけどコントローラーなどで処理したインスタンス変数などを使って静的なページでなく動的なページを作ることができます。さきほど、
@todos
というインスタンス変数をコントローラーのindexアクションに書いたので、これを作って全データを表示してみましょう。app/views/todos/index.html.erb<h1>ToDo一覧</h1> <table> <thead> <tr> <th>タスク</th> </tr> </thead> <tbody> <% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> </tr> <% end %> </tbody> </table>作成した画面
まだデータの登録がないので全件取得しても表示するものがないので、このような表示になっています。
こんな1~9までのような順序でアプリの作成(もしくは追加)をしていくことになります。
では、次にTodoの全件表示しかできない状態なので
次は新規ToDo追加できる機能を追加します新規追加機能の作り方
次に新規登録できる機能を作ります。
今度は1~9まの順序で機能を追加していきましょう。1.どんなもの作るか
ToDoテーブルにデータを新規追加する機能
テキストフィールドに入力した文字をボタン押すとデータベースに登録2.データベースに保存するデータは何か
テーブル名:Todo
カラム名:title
デフォルトで作られるカラム(ID, 作成日, 更新日)
title
だけデータベースに保存すればいいことになります。3. モデルは作ったか
モデルはTodoつかうので新しく作成の必要なし
4. マイグレーションファイルを生成&データベースに反映
モデルはTodo使う。カラムの追加も必要ないのでマイグレーションファイル作る必要も、データベースに反映する必要もない。
5. ページを表示するのか?データを登録・更新をするのか?
カラム名で
title
をデータベースに登録する処理を追加することになります。ページの表示をしているわけでも、情報の更新をしているわけでもなく、情報の削除でもありません。
こういうデータの登録処理は
POST
で行いましょう。6. ルーティングを決める
- メソッド:
POST
- ルーティング:
/tasks
- コントローラー&アクション
tasks#create
ルーティングを確認
実はすでに追加してしまっていたのですが、今回使うルーティングは↓の二つ目です。
routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' endページを表示しないで新規登録を行うのでPOSTですね。
7. 処理を行うコントローラーは作ったか
新しいモデルを作ったわけでも、わざわざ新しいコントローラーをつくる必要もありません。
todo
テーブルのToDoのデータ追加処理を追加するのでコントローラーはtodos_controller.rb
に追記すればOKです。8. コントローラーのアクションは作ったか
データ追加処理を記述したアクションを
todos_controller.rb
に追記していきましょう。コントローラーにアクションを追加
create
を追加しました。新規登録を行うためのアクションになります。app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all @todo = Todo.new # フォーム作るために必要 end def create @todo = Todo.create(todo_params) redirect_to todos_path end end
redirect_to todos_path
としているのは、ページを表示しているわけでないのでリダイレクトしないと処理が終わっても処理が固まった感じになってしまうからです。処理としては正常に終了してるけど、↓のように何も起こってない…みたいな感じになる。
正常に登録はできているのでリロードするとちゃんと表示されることになる。
get '/todos', to: 'todos#index', as: 'todos'
のas: 'todos'
と書くと/todos
というURLにtodos
という名前を付けて使えるようにしています。この名前に、
ルーティングの名前(todosなど)
+_path
でtodos_path
のようにするといちいちURLを書く必要がなくなります。注意点として、Formで入力した情報を新規登録や更新する際はセキュリティ対策として
Strong Parameters
を使わないとエラーでますので追加します。Strong Parameters
セキュリティ対策で、入れないとエラーになります。
難しいこと省いて、フォームから受け取ったデータを渡すと、許可したデータだけ返却されるくらい覚えておけばとりあえずは良いと思います。そして追加したのが
todo_params
メソッド。また、private
を使うと、todo_paramsメソッドが外部から使えないようにできるのでセキュリティを高まります。
難しいことはいいので、privateの下にStrong Parameters
いれるんだなくらいで今はいいです。app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all end def create @todo = Todo.create(todo_params) redirect_to todos_path end private def todo_params params.require(:todo).permit(:title) end end9. ビューファイルはいるか?
データベースでのデータの追加処理では使いません。
ただし、送信するためのテキスト入力フォーム
と送信ボタン
をindex.html.erb
に登録したいと思います。テキスト入力フォームと送信ボタン
index.html.erb<h1>ToDo一覧</h1> <table> <thead> <tr> <th>タスク</th> </tr> </thead> <tbody> <% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> </tr> <% end %> </tbody> </table> <h2>ToDo追加</h2> <%= form_with model: @todo do |f| %> <div><%= f.label :title %></div> <div><%= f.text_field :title %></div> <div><%= f.submit %></div> <% end %>フォームを作るために
form_with
を使っています。
また、使うmodelを指定する必要があるので@todo
を指定してます。
コントローラーのindexアクションで追加します。
f.label
のようにすると「Title」というラベル(文字表示)され、
f.text_field
は文字を入力するフォームを作ることができる。
f.submit
は送信ボタン。
todos_controller
のindex
にフォームを作るための部品を用意するため@todo = Todo.new
を追加app/controllers/todos_controller.rbclass TodosController < ApplicationController def index @todos = Todo.all @todo = Todo.new # フォーム作るために必要 end end登録機能完成
データベースに登録する機能が完成しました!
今度は更新と削除
次は更新機能を実装します。
必要機能
- 情報更新ページ(今回はeditアクションとedit.html.erbにて)
- ルーティングを追加する
- editアクションで編集する情報を表示する todos_controller.rb
- ビューで見た目を作る edit.html.erb
- 情報更新処理
- updateアクションで情報の更新処理 todos_controller.rb
更新情報編集ページ用ルーティング追加
routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo' end
get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo'
を追加しました。
todos_controller.rb
のedit
アクションを使用します。ルーティングの名前はedit_todo
とでもしておきます。更新情報編集ページ用コントローラー
editアクションは、情報編集画面(/todos/:id/edit)にアクセスすると処理されるアクションで、ビューで見た目を作るときに必要なデータの取得します。
editアクションを情報を追加します。
todos_controller.rbdef edit @todo = Todo.find(params[:id]) end
@todo
はインスタンス変数。@
とつけることでビューでもデータを使えるようになります。find
はデータベースのid
カラムからデータを探し出すときに使います。
- idはtodoを追加したときに自動的に採番されていきます。データベースの中身は下の表みたいになる。
- 編集画面は(/todos/:id/edit)のようにルーティングで設定した。
- 例えばURLが
http://sample.com/todos/1/edit
だとするとparams[:id]
は1
http://sample.com/todos/3/edit
だとするとparams[:id]
は3
http://sample.com/todos/1/edit
だとすると@todo = Todo.find(1)
と書いているのと同じになる試しに登録したデータベースの内容が↓になります。
id title created_at updated_at 1 テスト 2019-10-18 04:05:36.776003 2019-10-18 04:05:36.776003 2 買い物 2019-10-18 04:05:43.090180 2019-10-18 04:05:43.090180 3 勉強 2019-10-18 04:05:51.098753 2019-10-18 04:05:51.098753
@todo = Todo.find(params[:id])
でid
が1
だったら取得するデータをbyebugというデバッグツールで確認してみると
#<Todo id: 1, title: "テスト", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 04:05:36">
というデータが取れます。
ここで注意点
教えていて勘違いしている方が多かったので補足。
今回
title
カラムのデータを取得したいわけです。
@todo = Todo.find(params[:id])
をするとtitle
のデータだけ
を取得しているのではありません。意図的にデータを絞り込まなければ、今回の場合
id
,title
,created_at
,updated_at
1レコード(1行)分のデータ取得されます。
@todo = Todo.find(params[:id])
idは1とすると正:
#<Todo id: 1, title: "テスト", created_at: "2019-10-18 04:05:36", updated_at: "2019-10-18 04:05:36">
誤:
#<Todo title: "テスト">
自分が必要だと思っているデータだけ取得できてると勘違いしている??ように思えましたので補足。
こらへんはSQLを理解していないから起こることなのかとも思います。
気を付けましょう。更新処理アクション追加
ルーティングとコントローラーに追記していきます。
resources使うともう少しシンプルに書けますが、今回は原理を知るためしっかりと書いていきます。routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo' patch '/todos/:id', to: 'todos#update' # 更新処理用ルーティング追加 endapp/controllers/todos_controller.rbdef update @todo = Todo.find(params[:id]) @todo.update(todo_params) redirect_to todos_path endresouses使えるならこんな感じでシンプルに書ける
<-- resousesを使えるときなら --> <h1>ToDo編集</h1> <%= form_with model: @todo do |f| %> <div><%= f.label :title %></div> <div><%= f.text_field :title %></div> <div><%= f.submit %></div> <% end %> <%= link_to '戻る', todos_path %>今回原理をよく理解するためにこのようにURLとメソッドもちゃんと書きました。
#{ }
は文字列として変数を埋め込めるRubyの記法です。
変更するToDoデータのid
を指定すれば同じことを行えます。edit.html.erb<h1>ToDo編集</h1> <%= form_with(model: @todo, url: "/todos/#{@todo.id}", method: :patch) do |f| %> <div><%= f.label :title %></div> <div><%= f.text_field :title %></div> <div><%= f.submit %></div> <% end %> <%= link_to '戻る', todos_path %>編集ページへのリンクを作る
変更箇所付近だけ抜粋。
link_to
と~~~~_path
を使ってやるとこんな風に簡単に作れる。index.html.erb<% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> <td><%= link_to '編集', edit_todo_path(todo) %></td> </tr> <% end %><td><%= link_to '編集', edit_todo_path(todo.id) %></td>ついでに、ちゃんと省略して書かないとこういう書き方になる。
todo
→todo.id
Railsがいい感じで判断してくれるのでこのようになりますが、原理は覚えておくほうがいいと思います。
htmlだと↓のようなものが作られる
<a href="/todos/1/edit">編集</a>
削除機能実装
削除機能はわかってしまえば他と大して変わらないので楽です。
1. ルーティング追加
2. コントローラーにdestroy
アクション追加
3. ビューに削除ボタンのリンク追加routes.rbRails.application.routes.draw do get '/todos', to: 'todos#index', as: 'todos' post '/todos', to: 'todos#create' get '/todos/:id/edit', to: 'todos#edit', as: 'edit_todo' patch '/todos/:id', to: 'todos#update' delete '/todos/:id', to: 'todos#destroy' end
@todo = Todo.find(params[:id])
で削除する項目(idで指定された項目)を@todoに入れる- destroyメソッドで削除する
- 元のページに戻る
app/controllers/todos_controller.rbdef destroy @todo = Todo.find(params[:id]) @todo.destroy redirect_to todos_path end
<td><%= link_to '削除', "/todos/#{todo.id}", method: :delete %></td>
を追加。
method: :delete
でメソッドを指定。(指定しないとデフォルトのGETになります。)
ルーティングでdeleteに設定したURLはdelete '/todos/:id', to: 'todos#destroy'
なので対応させるために"/todos/#{todo.id}"
このように書きました。index.html.erb<% @todos.each do |todo| %> <tr> <td><%= todo.title %></td> <td><%= link_to '編集', edit_todo_path(todo) %></td> <td><%= link_to '削除', "/todos/#{todo.id}", method: :delete %></td> </tr> <% end %>削除機能が完成しました。
見た目は最悪ですが、ToDo登録・更新・削除機能が完成しました。
- 投稿日:2019-11-15T22:49:24+09:00
ReactアプリをFirebaseにデプロイしたらBlankページしか表示されない
はじめに
Reactでポートフォリオを作成してHerokuで公開していました。しかしHerokuには無料枠があり、常時起こし続けているわけにもいきません。
そこでFirebaseにデプロイをしてHostingをしてもらおうと企みました。デプロイまでは順調だったのですが、指定されたURL開いてもなぜかBlankページしか表示されませんでした。
環境
- Mac OS X EL Capitan 10.14.5
- react@16.11.0
- react-dom@16.11.0
FirebaseでHostingする方法はQiitaの以下の記事を参考にしました。
(初心者向け)Firebase HostingへReactプロジェクトを公開する手順課題
デプロイしたアプリのコンソールを確認すると、以下のようなエラーが出ていました。
SyntaxError: Unexpected token '<'
しかしエラー内で指定されているファイルはReactでビルドを行なった煩雑なものであり、読んでも全くわかりません。解決策
どうやら直前にGitHub Pagesにデプロイしようとして中断したのが悪さをしていそうです。
package.json{ "homepage": "https://[username].github.io/[work]/", }この部分をFirebaseのデプロイ時に取得したURLに書き換えて再度ビルドしました。
package.json{ "homepage": "https://[プロジェクトID].firebaseapp.com/", }ビルドが終わったら再度デプロイしてみます。
うまくサイトが表示された!
終わりに
ポートフォリオサイトはFirebaseで公開中です。
ポートフォリオあれ? デプロイできたのに真っ白で何も表示されないぞ? と困ったら1度pacage.jsonの中身を覗いて見ると謎が解けるかもしれません。
参考記事
Stack Overflow
Create-React-App build - “Uncaught SyntaxError: Unexpected token <”
- 投稿日:2019-11-15T18:28:54+09:00
reactNative
reactNative
React Native とは
ReactはFacebookが開発したJavaScriptのフレームワークであり、React Nativeはそれをモバイルで使えるようにしたものである。
React Nativeを使用することでネイティブに描画されるiOSとAndroidのアプリを作ることができる。1つのコードで、両方のプラットフォームで動くものが作れる。さらに、JavaやObjective-Cのライブラリを自分で書いてReact Native自体を拡張することもできる。WebでReactを使っているなら、ターゲットがモバイルに代わるだけなので簡単に使い始めることができる。
React同様、React NativeもJavaScriptと、JSX (なんとなくHtml風に書けるJavaScriptの拡張構文) と呼ばれるXMLライクなマークアップを使って記述される。内部では、ネイティブのレンダリングAPIが呼び出されるので、WebViewではなく、ちゃんとしたモバイルUIコンポーネントが描画される。もちろん、プラットフォームの機能であるカメラや位置情報といったものも利用できる。
※参考
https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50React Nativeのここがすごいよ
他の多くのクロスプラットフォームを謳う開発方法(CordovaやIonic)と違い、WebViewではなくネイティブで描画されるのが大きな利点となる。
これらの開発方法ではネイティブでの表現(UI/UX)を再現するために多大な努力をしているが、完全な再現とまでは行っていないし、最先端からは一歩遅れた表現(UI/UX)になってしまう。また、パフォーマンスもあまり良くない。一方でReact NativeではネイティブなUIが使われるし、また、メインのUIスレッドとは別に動くからパフォーマンスも高く維持される。
React NativeのアップデートサイクルはReactと同じで、propsやstateが変化したときにビューが再描画される。開発の方法はほとんどReactと変わらないので、Reactを使ったことのある人にとって学習コストが低いのも大きい。同様に、開発メンバーも集めやすい。また、以下で述べるように普通のモバイル開発に比べて開発がしやすいのも大きなポイントである。
リストと欠点
React Nativeにおける最も大きな危険性はおそらく、React Nativeがまだ発展段階にあるということだ。2015年の3月にiOS用としてリリースされ、同年9月にAndroid対応が発表されたばかりである。
まだネット上にも情報が多いとはいえないし、ドキュメントも整備されていない部分がある。ただし、足りないAPIは自分で作ることができるので、完全な「詰み」になることはないだろう。react Nativeが成熟していないため、二の足を踏む企業もあるニュース
https://project.nikkeibp.co.jp/idg/atcl/idg/14/481709/082000458/React Nativeの学習コスト
React NativeはFacebookが中心となって開発しているネイティブアプリのためのフレームワークです。特徴としては、Web技術、知識、HTML、CSS、JavaScript、Reactなどを使って、iOS、Androidのアプリを開発できます。
またReactの思想で「Learn once, Write anywhere」というのがあり、「一度学んだら、どこでも書ける」的な意味を持っており、WebでReactを触っていれば、React Nativeを使って、ネイティブアプリの開発もできます。
つまり、WEB開発経験者であればそのまで学習コスト高くないのも特徴になりますReact Nativeの導入実績
https://bagelee.com/programming/react-native/react-native-apps-example/
より簡単なのはReact NativeそれともiOS / Android?
JavaScriptは、Java、Objective-C、Swiftとは対照的に、学習しやすくてデバッグも簡単です。しかし、この手軽さにはデメリットもあります。 JavaScriptは堅牢な言語ではなく、書いたコードに多くのエラーが隠れていても気づきにくいことがあります。
一方、Objective-C / Swift / Javaは、多くの潜在的なエラーを、コード実行前に取り除くことができる「コンパイル時の型チェック」という仕組みを持っているという意味で、堅牢な言語です。
Swiftは明らかに非常にモダンな言語ですが、Objective-CとJavaもモダン化し続けていて、モダンな言語に求められる機能性やパフォーマンスに関しても決して不十分ではありません。しかし、Google検索すればすぐ分かるように、JavaScriptにはさまざまな欠陥があります。
よって、学習そのものはReact Nativeの方が簡単です。
しかし、JavaScriptの欠陥が伴うおそれがあります。また、クロスプラットフォームなフレームワークを利用する場合は必ず、いわゆる「一度書いたら、どこでもデバッグする必要が出てくる」という問題に対処しなければなりません。
どちらを学習するべきか
もしあなたがアプリ開発を予定していて、いつかアプリ開発者の仕事を探したい場合には、さまざまな理由からiOSまたはAndroidのネイティブ言語を強くおすすめします。
またReact Nativeを学ぶという選択肢もあります。これは興味深いテクノロジーですが、いくつか警告があります。
すべての開発者は、強い型付けの、コンパイラ型の、オブジェクト指向言語を習得する必要があります。そしてJava /Objective-C / Swiftはいずれもこれに最適な選択です。あなたが望むかどうかにかかわらず、いずれとにかくJavaScriptは学ぶことになります。
React Nativeは、AppleとGoogleのどちらかにも正式にサポートされていません。これはつまり、OSやAndroidから新しいアナウンスメントが公表されても、React Nativeで完全には動作しない可能性があることを意味します。たとえば、iOS 10iMessageアプリの新機能が追加されたとアナウンスされた時点では、React Nativeによってそのようなアプリのコードを書く方法はなかったと思います。こういうケースでは、iOSのネイティブアプリの開発方法を習得している必要があるのです。また現在でも、React Nativeを使ってApple Watchアプリを開発できるかどうかさえ定かではありませんが、ネイティブ開発を学んでおけば何の問題もないでしょう。
第三に、プロジェクトの寿命を念頭に置く必要があります。FacebookのParseサービス停止の例を思い出してください。現在のところ、React Nativeは健全に運営されていて、いくつかの主要企業もこれを支持しています。しかし、React Nativeをサポートしていない一方で、 今後もiOSとAndroidを長くサポートすることが予測されるAppleやGoogleとは異なり、Facebookやその他多くの企業にとっては、永久にReact Nativeのサービスを提供し続ける理由はないかもしれません。
どんな言語
・divやspanなどのDOMを使えない
・DOMを使えない替わりに「View」や「Text」などコンポーンネントとして使う
・テキストを扱う場合は「Text」。入れ物として扱う場合は「View」
・CSSは使えないので、替わりにに「CSS in JS式」でスタイリングするサンプル()
import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View } from 'react-native'; export default class App extends Component { render() { return ( <View style={styles.container}> <View style={[styles.base, styles.box1]}> <Text style={styles.text}>I am 2.</Text> </View> <View style={[styles.base, styles.box2]}> <Text style={styles.text}>I am 5.</Text> </View> <View style={[styles.base, styles.box3]}> <Text style={styles.text}>I am 1.</Text> </View> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, }, text: { fontSize: 24, color: 'white', }, base: { justifyContent: 'center', alignItems: 'center', }, box1: { flex: 2, backgroundColor: 'black' }, box2: { flex: 5, backgroundColor: 'red', }, box3: { flex: 1, backgroundColor: 'yellow', }, }); AppRegistry.registerComponent('Native', () => App);・入力項目あり
import React, { Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, TextInput, } from 'react-native'; export default class App extends Component { constructor(props) { super(props); this.state = { text: '', } } _onChangeText = (text) => { this.setState({ text }); } render() { const { text, } = this.state; return ( <View style={styles.container}> <TextInput style={styles.input} onChangeText={this._onChangeText} underlineColorAndroid='transparent' /> <Text>{text}</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, input: { height: 30, width: 200, borderBottomWidth: 1, borderBottomColor: '#008080', } }); AppRegistry.registerComponent('Native', () => App);最後に
ネイティブ開発はまだまだ成熟していない技術て、日に日に進歩をしている元気な技術です
android もjava → kotlin が公式言語(google曰く)になったし
ios も objective-c → swift に変更になり言語も技術もどんどん進化しています現在ほとんどのアプリでandroid ios両方でもリリースが当たり前になってきています。
両方の端末で開発をするとなると技術者不足や工数不足、お金の不足など沢山の問題にぶち当たります
そのため、クロスプラットフォーム技術もどんどん進化して行きます
今回紹介した React Native以外にもkotlin nativeなど似たような技術も存在しています今後も自分の価値をあげるためにも、こういう技術はどんどん取り入れていきましょう
- 投稿日:2019-11-15T17:40:45+09:00
ExpoをEject後、Expoから証明書に必要な鍵キーを取得する方法
- 投稿日:2019-11-15T14:16:10+09:00
いまからはじめるReact
この資料は 11/16(土)開催の勉強会 いまからはじめるReact の資料になります。
React未経験者/初学者向けに チュートリアルを通してReact(およびHooks)について学ぶためのものです。
そのため、サンプルコードには例外処理などが不十分な箇所があります。ご注意ください。Reactとは?
Reactとは Facebookが中心となってオープンソースで開発されている ユーザーインターフェースを構築するためのJavaScriptライブラリです。
(2019/10/30現在、v16.10.2 が公開されています)React – ユーザインターフェース構築のための JavaScript ライブラリ
https://ja.reactjs.org/コンポーネント(部品)を作成し、これらを組み合わせることでSingle Page Applicationのような複雑なユーザーインターフェースを構築できるので、ピュアなJavaScriptやjQueryで実装する場合に比べてコードの見通しがよく、デバッグしやすいものになります。
開発環境の準備
以下のツールが必要です。
- エディタ (VisualStudio Codeがオススメです)
- Node.js (頻繁にバージョンアップするので、nvmなどのバージョン管理ツールを使用することをオススメします)
Reactの開発ではOSを選びません。
Windows/Mac/Linuxどれでも好きな環境で開発できます。
Reactをはじめる前に
Reactを使用する際に頻出する JavaScript (ECMAScript2015) の基本文法について確認します。
変数の宣言 let, const
JavaScriptにおける変数/定数の宣言方法は3つあります。
-var
-let
-const
varの問題点 その1: 巻き上げ
参考: https://developer.mozilla.org/ja/docs/Learn/JavaScript/First_steps/Variables
varの巻き上げ(hoisting)
変数の宣言 (および一般的な宣言) はコードを実行する前に処理されますので、変数はコード内のどこで宣言しても、コードの先頭で宣言したものと等価になります。また、変数を宣言する前に変数を使用することもできます。この動作は、変数の宣言が関数やグローバルのコードの先頭に移動したように見えるため、"巻き上げ (hoisting)" と呼ばれます。myName = 'Chris'; function logName() { console.log(myName); } logName(); var myName;上記の例で
var
をlet
に変更すると、エラーで失敗します。varの問題点 その2: 変数の上書き
var
を使用するとき、好きなだけ同じ変数を何度でも宣言することができます、しかしlet
ではできません。var myName = 'Chris'; var myName = 'Bob';上記の例で
var
をlet
に変更すると、エラーで失敗します。これらの問題点は潜在的なバグの要因になりかねません。
Reactの開発においてvar
が必要になることはありませんので、使用しないこと!const
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/const
const 宣言は、値への読み取り専用の参照を作ります。その値が不変ということではなく、その変数識別子が再代入できないというだけです。
const number = 42; try { number = 99; } catch(err) { console.log(err); // expected output: TypeError: invalid assignment to const `number' // Note - error messages will vary depending on browser } console.log(number); // expected output: 42objectのプロパティなどは変更できます。
const obj = { number: 42, }; try { obj.number = 99; } catch(err) { console.log(err); } console.log(obj.number); // => 99JavaScriptでは型がないため、変数にどのような値が格納されるのか制限できません。
変数を定義する際はconst
で宣言することで、意図しない値が格納されることを防げます。
ループのカウンタなど、どうしても再代入が必要な変数のみlet
を使用するのがオススメです。arrow function
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/Arrow_functions
アロー関数式は、より短く記述できる、通常の function 式の代替構文です。
2 つの理由から、アロー関数が導入されました。1 つ目の理由は関数を短く書きたいということで、2 つ目の理由は this を束縛したくない、ということです。
アロー関数は以下のように使用します。
const materials = [ 'Hydrogen', 'Helium', 'Lithium', 'Beryllium' ]; console.log(materials.map(material => material.length)); // expected output: Array [8, 6, 7, 9]構文
状況によってカッコを省略できます。
カッコの有無ではなく、=>
を見てアロー関数かどうかを判断してください。(param1, param2, …, paramN) => { statements } (param1, param2, …, paramN) => expression // 上記の式は、次の式と同等です: => { return expression; } // 引数が 1 つしかない場合、丸括弧 () の使用は任意です: (singleParam) => { statements } singleParam => { statements } // 引数がない場合、丸括弧を書かねばいけません: () => { statements }スプレッド構文
...
スプレッド構文 - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax
スプレッド構文を使うと、関数呼び出しでは 0 個以上の引数として、Array リテラルでは 0 個以上の要素として、Object リテラルでは 0 個以上の key-value のペアとして、Array や String などの iterable オブジェクトをその場で展開します。// 配列の展開 const arr1 = [1, 2, 3]; const arr2 = [4, 5, ...arr1]; console.log(arr2); // => [4, 5, 1, 2, 3]; // オブジェクトの展開 (ECMAScript 2018以降) const obj1 = { firstName: 'kazunori', familyName: 'kimura' }; const obj2 = { ...obj1, age: 40 }; console.log(obj2); // => { firstName: 'kazunori', familyName: 'kimura', age: 40 } // 関数の引数 const sum = (...args) => { // 引数が args という配列に格納される let value = 0; args.forEach(arg => value += arg); return value; }; console.log(sum(1, 3, 5, 7)); // => 16 // 関数の呼び出し const multi = (a, b) => { return a * b; } const arr = [3, 5, 7]; console.log(multi(...arr)); // => 15分割代入 (Destructuring assignment)
分割代入 - JavaScript | MDN https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
分割代入 (Destructuring assignment) 構文は、配列から値を取り出して、あるいはオブジェクトからプロパティを取り出して別個の変数に代入することを可能にする JavaScript の式です。配列の分割代入
const [one, two] = [1, 2, 3, 4]; console.log(one); // => 1 console.log(two); // => 2 const [a, b, c] = [1, 2]; console.log(a); // => 1 console.log(b); // => 2 console.log(c); // => undefined// 既定値の設定 const [a, b = 4, c = 5] = [1, 2]; console.log(a); // => 1 console.log(b); // => 2 console.log(c); // => 5// スプレッド構文との組み合わせ const [a, b, ...arr] = [1, 2, 3, 4, 5]; // [a, b, ...arr,] <= 余剰なカンマはエラーとなる console.log(a); // => 1 console.log(b); // => 2 console.log(arr); // => [3, 4, 5]TIPS: Tupleの代替
分割代入によって他言語にある
Tuple
に似た機能を実装できます。const calc = (a, b) => { return [a + b, a * b]; }; const [sum, multi] = calc(3, 5); console.log(sum); // => 8 console.log(multi); // => 15 // 掛け算の結果だけほしい const [, m] = calc(4, 8); console.log(m); // => 32TIPS: 変数の入れ替え
配列の分割代入を使用すると、変数の値の入れ替えが簡単に行えます。
let a = 1; let b = 2; [a, b] = [b, a]; console.log(a); // => 2 console.log(b); // => 1オブジェクトの分割代入
配列の分割代入とイメージは大差ありません。
const obj = { name: 'kimura', age: 40 }; const { name } = obj; console.log(name); // => kimuraオブジェクトから変数を取り出して、オブジェクトのプロパティとは異なる名前を持つ変数に代入できます
const obj = { name: 'kimura', age: 40 }; const { name: userName } = obj; console.log(userName); // => kimuraTIPS 関数の引数に既定値を設定する
関数の引数にオブジェクトを渡すようにすることで、名前付き引数のような機能を実現できます。
また、既定値を設定することで省略可能な引数を定義できます。const drawRect = ({ width = 100, height = 100, position = { x: 0, y: 0 } } = {}) => { return `x1=${position.x},y1=${position.y},x2=${position.x + width},y2=${position.y + height}`; }; console.log(drawRect()); // => x1=0,y1=0,x2=100,y2=100 console.log(drawRect({ width: 200, position: {x: 50, y: 100} })); // => x1=50,y1=100,x2=250,y2=200
はじめてのReact
やっと本題に入ります。
それでは、Reactのプロジェクトを作成しましょう。create-react-app
Reactのプロジェクトを作成するには
create-react-app
コマンドを使用します。
create-react-app
はnpm
でインストールできます。$ npm install -g create-react-appでは、プロジェクトを作成します。
$ create-react-app todo-app $ cd todo-app $ code .
create-react-app
でプロジェクトを作成すると、すでにいくつかのファイルが生成されており
すぐに実行することが可能です。$ npm start
ブラウザが立ち上がり、Reactのロゴが表示されます。
まずはこのファイルを変更して、Reactの基本を学習します。App.js
App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App;
App
ファンクションが定義されています。
AppファンクションはHTMLのようなものを返却しています。これはJSX
とよばれる JavaScriptの構文の拡張です。
import
はライブラリやファイルの読み込みです。
export
は他のファイルから指定した要素を参照できるようにします。JSXに式を埋め込む
{} 中カッコ
の中に式を埋め込むことで表示内容を動的に変更できます。App.jsimport React from 'react'; import './App.css'; function App() { const message = 'Hello, React!'; return ( <div className="App"> <header className="App-header"> <p> {message} </p> </header> </div> ); } export default App;ファイルを保存すると、自動的にブラウザがリロードされて変更が反映されます。
(以降、App.js内のaタグ, imgタグは不要なので削除しておきます)あらゆる有効な JavaScript の式を JSX 内で中括弧に囲んで使用できます。
以下は配列の中身を書き出す例です。
App.jsimport React from 'react'; import './App.css'; function App() { const libraries = [ 'jQuery', 'React', 'Vue.js' ]; return ( <div className="App"> <header className="App-header"> {libraries.map(item => <p>{item}</p>)} </header> </div> ); } export default App;コンポーネント
それでは、独自のコンポーネントを定義してみます。
分かりやすいようにcomponents
フォルダを作成し、その配下にコンポーネントを作成していきます。$ mkdir src/components $ touch src/components/Message.js
Message.js
はメッセージを表示するコンポーネントです。Message.jsimport React from 'react'; function Message() { return ( <p>Original Message.</p> ); } export default Message;
App.js
にMessage
コンポーネントを配置します。App.jsimport React from 'react'; import './App.css'; import Message from './components/Message'; function App() { return ( <div className="App"> <header className="App-header"> <Message /> </header> </div> ); } export default App;コンポーネント名は常に大文字で始めてください。
コンポーネントに値を渡す: props
App
からMessage
に値を渡して、動的にメッセージを組み立ててみます。
name
というプロパティに文字列を渡します。App.jsimport React from 'react'; import './App.css'; import Message from './components/Message'; function App() { return ( <div className="App"> <header className="App-header"> <Message name="kimura" /> </header> </div> ); } export default App;
Message
にて受け取った値を表示するように修正します。
Reactはコンポーネントを呼び出す際にprops
というobjectに与えられた属性やタグ内の値を渡します。Message.jsimport React from 'react'; function Message(props) { return ( <p>Hello, {props.name}!</p> ); } export default Message;コンポーネントは繰り返し使用できます。
App.jsimport React from 'react'; import './App.css'; import Message from './components/Message'; function App() { return ( <div className="App"> <header className="App-header"> <Message name="kimura" /> <Message name="tanaka" /> <Message name="suzuki" /> </header> </div> ); } export default App;React は柔軟ですが、1 つだけ厳格なルールがあります:
自分自身の props は決して変更してはいけません。では、テキストボックスの入力値を画面に反映させる場合はどのようにすればよいのでしょうか?
ユーザーの入力を扱う: state
コンポーネントで変化する値を扱う場合は
state
を使用します。予め用意した文字列ではなく、テキストボックスに名前を入力して
Message
コンポーネントに渡してみます。
state
の機能を使用するにはuseState
メソッドを使用します。App.jsimport React, { useState } from 'react'; import './App.css'; import Message from './components/Message'; function App() { const [name, setName] = useState(''); const handleTextInput = (e) => { setName(e.target.value); }; return ( <div className="App"> <header className="App-header"> <div className="form"> <input type="text" onChange={handleTextInput} /> </div> <Message name={name} /> </header> </div> ); } export default App;テキストボックスの内容が変わると
handleTextInput
メソッドが呼ばれます。
handleTextInput
でsetName
メソッドにテキストボックスの値を渡します。
name
の値が更新されるとMessage
が再描画されます。子から親に値を渡す
フォームをコンポーネント化することを考えてみましょう。
まずは名前を入力するフォームのコンポーネントを作成します。$ touch src/components/NameForm.js子から親にデータを渡すためには、親から子にコールバック関数を渡します。
子にてprops
に渡されたコールバック関数を実行します。NameForm.jsimport React from 'react'; function NameForm(props) { const handleTextInput = (e) => { props.onChangeName(e.target.value); }; return ( <div className="form"> <input type="text" value={props.name} onChange={handleTextInput} /> </div> ); } export default NameForm;
hendleTextInput
メソッドにてprops
にセットされたonChangeName
を実行しています。
これで、テキストボックスの内容が変わるたびにその値がonChangeName
メソッドを通じて親のコンポーネントに渡されます。App.jsimport React, { useState } from 'react'; import './App.css'; import Message from './components/Message'; import NameForm from './components/NameForm'; function App() { const [name, setName] = useState(""); return ( <div className="App"> <header className="App-header"> <NameForm name={name} onChangeName={value => setName(value)} /> <Message name={name} /> </header> </div> ); } export default App;
onChangeName
で受け取ったvalue
を stateにセットします。
子コンポーネントのテキストボックスの内容が変更されるとstateに反映され、Messageコンポーネントに引き渡されます。Reactではこのようにバケツリレーのようにして親から子に、子から親にデータを渡していきます。
Todoアプリの実装
それでは、もう少し複雑なアプリの開発を通してReactについて解説していきます。
今回はTodoアプリを作成します。
Todoのデータ設計
Todoは以下の項目を持つものとします。
- ID: TodoごとにユニークなIDを持つ
- Content: 内容
- Done: 完了フラグ
- CreatedAt: 作成日時
- UpdatedAt: 更新日時
下準備
App.css
の内容を修正しておきます。App.css.App { padding: 10px; } .theme-selector { padding: 10px; } .theme-selector label { margin-left: 20px; }Todoコンポーネントの作成
では、Todoコンポーネントを実装していきます。
$ touch src/components/Todo.js $ touch src/components/Todo.cssまずはスタイルを定義します。
Todo.css.todo { display: flex; width: 100%; min-height: 60px; align-items: stretch; border: 1px solid #ccc; border-bottom: 0; } .todo:last-child { border-bottom: 1px solid #ccc; } .todo .check { width: 40px; display: flex; justify-content: center; align-items: center; color: #00cc00; font-weight: bold; font-size: xx-large; } .todo .body { flex: 1; display: flex; flex-direction: column; align-items: stretch; } .todo .actions { width: 60px; display: flex; flex-direction: column; justify-content: center; align-items: center; } .todo .body .header { width: 100%; display: flex; flex-direction: row; justify-content: flex-start; align-items: center; } .todo .body .header .date { font-size: x-small; padding: 4px; } .todo .body .content { padding: 4px; } .todo .body textarea { width: calc(100% - 12px); height: 100%; margin: 3px; } .btn { width: 50px; height: 50px; margin: 5px; }つづいて Todoコンポーネント を作成します。
Todo.jsimport React from 'react'; import './Todo.css'; function Todo(props) { return ( <div className="todo"> <div className="check"> {/* Doneがtrueならチェックマークを表示 */} {props.Done && <span>✓</span>} </div> <div className="body"> <div className="header"> <span className="date">CreatedAt: {props.CreatedAt}</span> <span className="date">UpdatedAt: {props.UpdatedAt}</span> </div> {/* contentをそのまま表示 */} <div className="content">{props.Content}</div> </div> <button className="btn">Edit</button> <button className="btn">Delete</button> </div> ); } export default Todo;
props
にデータ設計で定義したTodoの内容がそのままセットされる想定です。
App.js
を修正し、いくつかTodoを表示してみます。App.jsimport React, { useState } from 'react'; import './App.css'; import Todo from './components/Todo'; function App() { const [todos, setTodos] = useState([ { ID: 1, Content: 'hoge', Done: true, CreatedAt: (new Date()).toISOString(), UpdatedAt: (new Date()).toISOString(), }, ]); return ( <div className="App"> {todos.map(item => <Todo key={item.ID} {...item} />)} </div> ); } export default App;
toISOString
は日付をUTCの文字列 (ISO8601形式) に変換します。参考: Date.prototype.toISOString() - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOStringコンポーネントを
map
メソッドなどで複数登録する場合、Reactが個々のコンポーネントを区別できるようにkey
プロパティを指定する必要があります。この状態で表示内容を確認してください。 hoge という項目が一つ表示されているはずです。
Todoの追加
Todoを追加する機能を実装していきます。
TodoForm
コンポーネントを作成します。$ touch src/components/TodoForm.jsまた、TodoのIDを重複なく採番するために、
uuid
というパッケージをインストールします。$ npm install --save uuid完了フラグとTodoの内容を入力するフォームを作成します。
Saveボタンが押されたら入力された内容を親コンポーネントに引き渡します。TodoForm.jsimport React, { useState } from 'react'; import './Todo.css'; function TodoForm(props = { Done: false, Content: '', onSave: () => {} }) { const [done, setDone] = useState(!!props.Done); const [content, setContent] = useState(props.Content); const handleSave = () => { const data = { Done: done, Content: content, }; props.onSave(data); // フォームの初期化 setDone(false); setContent(''); }; return ( <div className="todo"> <div className="check"> <input type="checkbox" checked={done} onChange={e => setDone(e.target.checked)} /> </div> <div className="body"> <textarea value={content} onChange={e => setContent(e.target.value)} /> </div> <button className="btn" onClick={handleSave}>Save</button> </div> ); } export default TodoForm;
App
にTodoForm
を追加します。
TodoForm
のSave
ボタンが押されたらその結果をstate
の配列に追加します。App.jsimport React, { useState } from 'react'; import uuid from 'uuid'; import './App.css'; import Todo from './components/Todo'; import TodoForm from './components/TodoForm'; function App() { const [todos, setTodos] = useState([ { ID: 1, Content: 'hoge', Done: true, CreatedAt: (new Date()).toISOString(), UpdatedAt: (new Date()).toISOString(), }, ]); const handleCreate = data => { // IDを採番 data.ID = uuid.v4(); // 現在日時を取得 const now = (new Date()).toISOString(); data.CreatedAt = now; data.UpdatedAt = now; // 末尾に追加 setTodos([...todos, data]); }; return ( <div className="App"> <TodoForm onSave={handleCreate} /> {todos.map(item => <Todo key={item.ID} {...item} />)} </div> ); } export default App;
handleCreate
メソッドではフォームから受け取った値に対して ID と作成日時、更新日時の項目を追加した上で
stateのTodo配列の末尾に追加しています。ここまで実装して、Todoが追加されることを確認します。
Todoを削除する
TodoコンポーネントにあるDeleteボタンをクリックすると該当のTodoが削除されるように実装していきます。
App.js// ... 省略 ... const handleDelete = id => { // IDが一致する項目のindexを取得 const index = todos.findIndex(item => item.ID === id); if (index >= 0) { // 新しい配列を生成 const newList = [...todos]; // 配列から該当要素を削除 newList.splice(index, 1); // stateに反映 setTodos(newList); } }; return ( <div className="App"> <TodoForm onSave={handleCreate} /> {todos.map(item => ( <Todo key={item.ID} {...item} onDelete={handleDelete} />) )} </div> ); } export default App;
handleDelete
を定義し、Todo
コンポーネントに渡します。
handleDelete
メソッドはTodoのIDを受け取って、該当するIDの項目をstateの配列から削除しています。Todo.jsimport React from 'react'; import './Todo.css'; function Todo(props) { return ( <div className="todo"> <div className="check"> {/* Doneがtrueならチェックマークを表示 */} {props.Done && <span>✓</span>} </div> <div className="body"> <div className="header"> <span className="date">CreatedAt: {props.CreatedAt}</span> <span className="date">UpdatedAt: {props.UpdatedAt}</span> </div> {/* contentをそのまま表示 */} <div className="content">{props.Content}</div> </div> <button className="btn">Edit</button> <button className="btn" onClick={() => props.onDelete(props.ID)}>Delete</button> </div> ); } export default Todo;Deleteボタンのクリック時に
App
から渡されたonDelete
メソッドを実行します。
その際、TodoのIDを引数に渡します。削除処理が正常に動くことを確認します。
Todoを更新する
更新処理は少し複雑です。
ひとつずつ実装していきます。編集モードの切り替え
まず、TodoコンポーネントのEditボタンをクリックすると、該当Todoが編集モードに切り替わるように実装します。
Todo.jsimport React, { useState } from 'react'; import TodoForm from './TodoForm'; import './Todo.css'; function Todo(props) { const [edit, setEdit] = useState(false); if (edit) { return ( <TodoForm {...props} onSave={() => {})} /> ); } return ( <div className="todo"> <div className="check"> {props.Done && <span>✓</span>} </div> <div className="body"> <div className="header"> <span className="date">CreatedAt: {props.CreatedAt}</span> <span className="date">UpdatedAt: {props.UpdatedAt}</span> </div> <div className="content">{props.Content}</div> </div> <button className="btn" onClick={() => setEdit(true)}>Edit</button> <button className="btn" onClick={() => props.onDelete(props.ID)}>Delete</button> </div> ); } export default Todo;編集モードのフラグをstateで管理します。
初期値はfalse
としておきます。編集モードの場合は従来のTodoコンポーネントの変わりに
TodoForm
コンポーネントを表示するようにします。
TodoForm
にはTodo
コンポーネントが受け取ったprops
を展開してセットします。
とりあえずonSave
には空の関数を渡しておきます。Editボタンをクリックしたら
edit
の値をtrue
に変更するように実装します。この状態で、Editボタンをクリックしたら編集モードに切り替わることを確認します。
編集モードのキャンセル
TodoForm
コンポーネントにキャンセルボタンを追加し、キャンセルボタンがクリックされたら編集モードが解除されるように実装します。まずは
TodoForm
にキャンセルボタンを実装します。TodoForm.jsimport React, { useState } from 'react'; import './Todo.css'; function TodoForm(props = { Done: false, Content: '' }) { const [done, setDone] = useState(!!props.Done); const [content, setContent] = useState(props.Content || ''); const handleSave = () => { const data = { Done: done, Content: content, }; props.onSave(data); setDone(false); setContent(''); }; return ( <div className="todo"> <div className="check"> <input type="checkbox" checked={done} onChange={e => setDone(e.target.checked)} /> </div> <div className="body"> <textarea value={content} onChange={(e) => setContent(e.target.value)} /> </div> <button className="btn" onClick={handleSave}>Save</button> {/* IDが存在する場合はキャンセルボタンを表示 */} {props.ID && ( <button className="btn" onClick={props.onCancel}>Cancel</button> )} </div> ); } export default TodoForm;最上位にある登録フォームにはキャンセルボタンが不要なので、
ID
の有無で登録か編集かを判定します。
TodoForm
ではTodo
から渡されたprops
にあるonCancel
を実行します。
onCancel
ではTodo
コンポーネントのstateedit
を更新するよう実装します。Todo.jsimport React, { useState } from 'react'; import TodoForm from './TodoForm'; import './Todo.css'; function Todo(props) { const [edit, setEdit] = useState(false); if (edit) { return ( <TodoForm {...props} onSave={() => {})} onCancel={() => setEdit(false)} /> ); } // ...省略...ここまで実装できたら、編集モードと通常モードの切り替えができることを確認します。
更新処理を実装する
仕上げに更新処理を実装します。
App
にTodo
コンポーネントから呼ばれる更新メソッドhandleUpdate
を実装します。
handleUpdate
の引数にはhandleCreate
同様、Todoのデータがobjectで渡される想定です。変更箇所のみ抜粋します
App.js// ... 省略 ... const handleUpdate = data => { const now = (new Date()).toISOString(); data.UpdatedAt = now; setTodos(todos.map(item => { // IDが一致する要素を差し替える if (item.ID === data.ID) { return data; } return item; })); }; // ... 省略 ... <Todo key={item.ID} {...item} onSave={handleUpdate} onDelete={handleDelete} /> // ... 省略 ...更新処理本体となる
handleUpdate
を追加します。
stateのTodo配列のうち、受け取ったデータとIDが一致するものを差し替えて、新しい配列をstateにセットしています。Todoコンポーネントの
onSave
にhandleUpdate
を渡します。Todoコンポーネントでは受け取った
onSave
をさらにTodoForm
に渡しますが、更新が完了したら自身の編集モードを終了するようフラグを更新しておきます。Todo.jsimport React, { useState } from 'react'; import TodoForm from './TodoForm'; import './Todo.css'; function Todo(props) { const [edit, setEdit] = useState(false); // TodoFormに引き渡す更新メソッド const handleUpdate = data => { props.onSave(data); setEdit(false); // 編集モード終了 }; if (edit) { return ( <TodoForm {...props} onSave={handleUpdate} onCancel={() => setEdit(false)} /> ); } // ... 省略 ...
TodoForm
コンポーネントではDone
,Content
だけでなくID
やCreatedAt
などの値も引き渡すように修正します。
面倒なので受け取ったprops
をすべてTodoデータに展開してしまいます。TodoForm.jsimport React, { useState } from 'react'; import './Todo.css'; function TodoForm(props = { Done: false, Content: '' }) { const [done, setDone] = useState(!!props.Done); const [content, setContent] = useState(props.Content || ''); const handleSave = () => { const data = { ...props, // 受け取ったpropsを展開 Done: done, Content: content, }; props.onSave(data); setDone(false); setContent(''); };これで入力した値が
TodoForm
->Todo
->App
に渡るように実装できました。追加、更新、削除が問題なく動作することを確認してください。
WebAPIとの連携
ここまではクライアントサイドの実装のみでしたので、データを保持する手段がなく、リロードすると登録したTodoの内容が消えてしまいます。
ここからはWebAPIと連携することでTodoを保存できるように実装していきます。WebAPIについて
今回はあらかじめ簡単なWebAPIをAWS上に作成しました。
APIの詳細は以下を確認してください。SwaggerHub
https://app.swaggerhub.com/apis/Kazunori-Kimura/TodoAPI/1データの取得
fetch
メソッドを使用して、WebAPIからデータを取得します。
外部から取得したデータを反映するには、副作用フック (Effect Hook) を使用します。副作用フックの利用法 – React (https://ja.reactjs.org/docs/hooks-effect.html)
データの取得、購読の設定、あるいは React コンポーネント内の DOM の手動での変更、といったものはすべて副作用の例です。App.jsimport React, { useState, useEffect } from 'react'; import uuid from 'uuid'; import './App.css'; import Todo from './components/Todo'; import TodoForm from './components/TodoForm'; const url = 'https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/latest/todo'; function App() { const [todos, setTodos] = useState([]); useEffect(() => { const getTodoes = async () => { const response = await fetch(url, { method: 'GET', mode: 'cors', cache: 'no-cache', }); const res = await response.json(); setTodos(res); }; getTodoes(); }, []); // ... 省略 ..
useEffect
の第2引数を空の配列にすると、App
コンポーネントが描画されたときにだけ呼び出されます。補足: fetch
Fetch API - Web API | MDN (https://developer.mozilla.org/ja/docs/Web/API/Fetch_API)
Fetch API には (ネットワーク越しの通信を含む) リソース取得のためのインターフェイスが定義されています。XMLHttpRequest と似たものではありますが、より強力で柔軟な操作が可能です。補足: cors
オリジン間リソース共有 (CORS) - HTTP | MDN (https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
オリジン間リソース共有 (CORS: Cross-Origin Resource Sharing) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。ウェブアプリケーションは、自分とは異なるオリジン (ドメイン、プロトコル、ポート番号) にあるリソースをリクエストするとき、オリジン間 HTTP リクエストを実行します。データの更新
追加/更新/削除でもAPIを呼び出すように修正します。
App.js// ... 省略 ... const handleCreate = data => { const createTodo = async () => { // ID, CreatedAt, UpdatedAtはAPI側で設定される const response = await fetch(url, { method: 'POST', mode: 'cors', headers: { "Content-Type": 'application/json; charset=utf-8', }, body: JSON.stringify(data), }); console.log(response.status); }; createTodo(); }; const handleUpdate = data => { const updateTodo = async () => { const response = await fetch(`${url}/${data.ID}`, { method: 'PUT', mode: 'cors', headers: { "Content-Type": 'application/json; charset=utf-8', }, body: JSON.stringify(data), }); console.log(response.status); }; updateTodo(); }; const handleDelete = id => { const deleteTodo = async () => { const response = await fetch(`${url}/${id}`, { method: 'DELETE', mode: 'cors', }); console.log(response.status); }; deleteTodo(); }; // ... 省略 ...Google Chromeの開発者ツールを表示した状態で実行してみてください。
登録/更新/削除処理は成功するものの、画面には反映されないと思います。
画面をリロードすれば取得処理が実行され、登録/更新/削除した内容が反映されます。都度画面をリロードしなければならないのは不便なので、登録/更新/削除が完了したら自動的に取得処理が実行されるように実装します。
useEffect
の第2引数にstateから取得した変数を与えます。
useEffect
は第2引数の値が変わる度に呼び出されますので、stateを更新すればリストが更新されるようになります。App.jsimport React, { useState, useEffect } from 'react'; import './App.css'; import Todo from './components/Todo'; import TodoForm from './components/TodoForm'; const url = 'https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/latest/todo'; function App() { const [todos, setTodos] = useState([]); const [refresh, setRefresh] = useState(0); // リフレッシュ用のstate useEffect(() => { const getTodoes = async () => { const response = await fetch(url, { method: 'GET', mode: 'cors', cache: 'no-cache', }); const res = await response.json(); // 作成日時順に返す setTodos(res.sort((a, b) => a.CreatedAt < b.CreatedAt ? 1 : -1)); }; getTodoes(); }, [refresh]); // useEffectの第2引数にリフレッシュ用stateをセット const handleCreate = data => { const createTodo = async () => { // ID, CreatedAt, UpdatedAtはAPI側で設定される const response = await fetch(url, { method: 'POST', mode: 'cors', headers: { "Content-Type": 'application/json; charset=utf-8', }, body: JSON.stringify(data), }); console.log(response.status); setRefresh(Date.now()); // リフレッシュ用stateの値を更新 }; createTodo(); }; const handleUpdate = data => { const updateTodo = async () => { const response = await fetch(`${url}/${data.ID}`, { method: 'PUT', mode: 'cors', headers: { "Content-Type": 'application/json; charset=utf-8', }, body: JSON.stringify(data), }); console.log(response.status); setRefresh(Date.now()); // リフレッシュ用stateの値を更新 }; updateTodo(); }; const handleDelete = id => { const deleteTodo = async () => { const response = await fetch(`${url}/${id}`, { method: 'DELETE', mode: 'cors', }); console.log(response.status); setRefresh(Date.now()); // リフレッシュ用stateの値を更新 }; deleteTodo(); }; // ... 省略 ...これで登録/更新/削除するたびにリストが更新されるようになりました。
Contextフック
コンポーネントのデータ管理の基本は
state
とprops
ですが、コンポーネントが多階層になった場合にバケツリレーのごとくデータを渡すのは非常に面倒です。そういったときに使える機能が
Context Hook
になります。例えば、現在の認証済みユーザー・テーマ・優先言語といったデータを共有する場合に有用です。
Contextの使用例
今回はTodoアプリにテーマ選択機能を追加してみます。
App.jsimport React, { useState, useEffect, createContext } from 'react'; // createContextを追加 import './App.css'; import Todo from './components/Todo'; import TodoForm from './components/TodoForm'; const url = 'https://hogehoge.execute-api.ap-northeast-1.amazonaws.com/latest/todo'; // テーマごとのスタイル定義 const Themes = { light: { color: '#000', backgroundColor: '#fff', }, dark: { color: '#fff', backgroundColor: '#000', }, }; // 現在選択されているテーマを共有するContext // exportして他コンポーネントからも参照できるようにする export const ThemeContext = createContext(Themes.light); function App() { const [todos, setTodos] = useState([]); const [refresh, setRefresh] = useState(0); // 現在選択されているテーマを管理するstate const [theme, setTheme] = useState('light'); // ... 省略 ... // テーマの切り替え const handleTheme = (e) => { setTheme(e.target.value); }; return ( <div className="App"> {/* Providerの配下でContextが共有される */} <ThemeContext.Provider value={Themes[theme]}> {/* テーマの選択用ラジオボタン */} <div className="theme-selector"> <label><input type="radio" name="theme" value="light" defaultChecked={theme === 'light'} onChange={handleTheme} />Light</label> <label><input type="radio" name="theme" value="dark" defaultChecked={theme === 'dark'} onChange={handleTheme} />Dark</label> </div> <TodoForm onSave={handleCreate} /> {todos.map(item => ( <Todo key={item.ID} {...item} onSave={handleUpdate} onDelete={handleDelete} />) )} </ThemeContext.Provider> </div> ); } export default App;まず、テーマごとのスタイルを定義します。
Reactではstyle
プロパティに CSSのプロパティ名をkeyに、値をvalueにセットしてあるobjectを渡すとCSSを適用してくれます。
ただbackground-color
などのハイフンを含むプロパティ名の場合、backgroundColor
のようにハイフンを取り除いて次の文字を大文字にする必要があります。
ThemeContext
がテーマを共有するためのContext
になります。
他のコンポーネントから参照するため、export
しておきます。
Context
を共有する範囲をThemeContext.Provider
コンポーネントで括ります。
Provider
のvalue
に現在の値を渡します。
他コンポーネントはexport
されたThemeContext
を介してvalue
にセットされている値を取得することができます。
Context
を参照する側は以下のように実装します。Todo.jsimport React, { useState, useContext } from 'react'; // useContextを追加 import TodoForm from './TodoForm'; import './Todo.css'; import { ThemeContext } from '../App'; // AppからContextをimport function Todo(props) { const [edit, setEdit] = useState(false); const theme = useContext(ThemeContext); // useContextを使用してContextの値を取り出す const handleUpdate = (data) => { setEdit(false); props.onSave(data); }; if (edit) { return ( <TodoForm {...props} onSave={handleUpdate} onCancel={() => setEdit(false)} /> ); } return ( {/* styleにContextから取り出した値(object)をセット */} <div className="todo" style={theme}> <div className="check"> {props.Done && (<span>✓</span>)} </div> <div className="body"> <div className="header"> <span className="date">CreatedAt: {props.CreatedAt}</span> <span className="date">UpdatedAt: {props.UpdatedAt}</span> </div> <div className="content">{props.Content}</div> </div> <button className="btn" onClick={() => setEdit(true)}>Edit</button> <button className="btn" onClick={() => props.onDelete(props.ID)}>Delete</button> </div> ); } export default Todo;TodoForm.jsimport React, { useState, useContext } from 'react'; // useContextを追加 import './Todo.css'; import { ThemeContext } from '../App'; // AppからContextをimport function TodoForm(props = { Done: false, Content: '' }) { const [done, setDone] = useState(!!props.Done); const [content, setContent] = useState(props.Content || ''); const theme = useContext(ThemeContext); // useContextを使用してContextの値を取り出す const handleSave = () => { props.onSave({ ...props, Done: done, Content: content, }); setDone(false); setContent(''); }; return ( {/* styleにContextから取り出した値(object)をセット */} <div className="todo" style={theme}> <div className="check"> <input type="checkbox" checked={done} onChange={e => setDone(e.target.checked)} /> </div> <div className="body"> <textarea value={content} onChange={(e) => setContent(e.target.value)} /> </div> <button className="btn" onClick={handleSave}>Save</button> {props.ID && ( <button className="btn" onClick={props.onCancel}>Cancel</button> )} </div> ); } export default TodoForm;
App.js
のラジオボタンを変更すると各Todo、Formのテーマが切り替わります。サンプルが単純なためあまり威力が実感できないかもしれませんが、コンポーネント数が多かったり、孫・ひ孫のコンポーネントに値を渡したい場合に便利な機能です。
参考
- 投稿日:2019-11-15T08:57:03+09:00
Reactライブラリー開発環境で、"Invalid Hook Call Warning"に対処する
"Invalid Hook Call Warning"の原因と対応方法
Reactライブラリーを開発するため環境構築で"Invalid Hook Call Warning"に対処したので、その原因と対応方法について書きます。
"Invalid Hook Call Warning"の原因とは?
原因は3種類ある
ReactのHooksの書き方や、Reactプロジェクトの構成が原因で発生するエラーです。
Reactの公式ページで詳しく解説されていて具体的には3つの原因があります。
- 関数型コンポーネントの外でhooksを使おうとしている
- コンポーネントのトップレベル以外(if文など)でhooksを使おうとしている
- 2つ以上のReactが存在する(後ほど詳しく)
Reactや、その周辺ライブラリーを使いながらアプリ開発をしていて"Invalid Hook Call Warning"に遭遇するケースはほぼ1や2のケースです。
2つ以上のReactが存在する、とは?
複数のReactパッケージを参照しながら1つのReactアプリを動かそうとしてしまっている状態です。
Reactライブラリーを開発する際に私は以下のような構成を選びました。
Reactライブラリープロジェクト
- React(これがないと始まらない)
- Typescript(バグを減らしてきれいに書くため型が欲しい)
- rollup(アプリ開発はwebpack、ライブラリー開発はrollupらしい)
こちらのサイトの通りに構築しました。
開発コンポーネント確認用プロジェクト
こちらは create-react-app のTypescript使用バージョンで簡単に作りました。
この構成でライブラリーの中でhooksを使い、開発コンポーネント確認用プロジェクトで
npm start
して動きを確認しようとすると"Invalid Hook Call Warning"が発生します。これは開発コンポーネント確認用プロジェクトは、そのプロジェクト内のnode_modulsにあるreactを参照しますし、
Reactライブラリーとして開発中のコンポーネントは、Reactライブラリープロジェクトにあるnode_modulsのreactを参照してしまう訳です。こうして、「2つ以上のReactが存在する」という条件が整ってエラーになります。
npm link による対応方法
私はnpmを使っているので以下の方法で対応しました。
開発コンポーネント確認用プロジェクトで以下のコマンドを実行
cd node_modules/react && npm link cd ../../ cd node_modules/react-dom && npm link cd ../../Reactライブラリープロジェクトで以下のコマンドを実行
npm link react && npm link react-dom npm link再び開発コンポーネント確認用プロジェクトで以下のコマンドを実行
npm link [module-name]困っている人たちがたくさんいるようで、こちらのissueを参考にしました。