20191115のReactに関する記事は7件です。

Ruby on Rails ToDoアプリ作成【CRUD】勉強会 #1

初心者用Railsアプリ練習帳

勉強会を開き、Rails初学者に日頃教えています。初心者向け勉強回資料用もかねて作成。
ある程度基礎を学習していると、読みながら真似はできるけど、いまいち全体の知識がつながっていないので自分で作ることができない…

そんな状態になりがちでした。

対象者

  • Railsでなんでもいいからの教材をやった人
  • Rails基礎はなんとなくわかってきた人
  • Railsで自分でアプリ作るのはまだ無理そうという人
  • Rails以外のフレームワークならわかる人

※ 正確さより概念とアプリ作成の流れを重視します。

コンセプト

他にも言っている方がいますが、勉強会で教えている経験から初学者が同じ教材を何周もするのは上達が遅いように感じています。

知識はあるけど自分で一から作るのは無理そう。
エラーが出て対処出来ない

ということになりがちでした。

こういう場合、実際に手を動かし、自分の頭で考えて簡単なアプリを作る必要があるけど、

  • いきなりだと何を作ればいいのかわからない…
  • 一連のアプリを作っていく順番がわからない…
  • エラーでても対処できない…
  • 簡単なのから作ってみればいいと言うが何が簡単なものなのか分からない…
  • 何個もアプリ自分で作るといいらしいが何を作れば練習やスキル向上なるか分からない…

そんな人が未経験からプログラミングをやる人には何人もいました。
現状の私の考えですが、世の中にある教材は見た目とか完成度ないと売れないし、強い人達からツッコまれるので分かりやすさより精確さが必要とされる教材になってしまうようにおもいます。

なので、後で身につければ良いこともあれもこれも付け足しており、本当にコレだけ!
という事項が初心者には見えにくくなっている気がします。

それをうけて今回、ムダなものとにかく剥ぎまくって、コアな部分だけを数多く練習すればみんな出来るようになるのではないかと考え、練習問題を作りました。
今後も感想とかもらって継続的にブラッシュアップしていければと思っています。

手順

  1. とりあえずToDoアプリをなぞって作ってみる(少しだけでも勉強した人が前提) ←今回はココ
  2. 超シンプルアプリを量産。手を動かして覚える!概念つかむ!
  3. データベースから欲しいデータを取得する!千本ノックのようにやりまくります!

2,3は個人的に開く勉強会で解説します。

とりあえずToDoアプリをなぞって作ってみる

環境構築はローカルでもいいですけど、WindowsやMacで違うので初心者はcloud9で作っていいと思います。
構築方法は今回省略

以下のコマンドでアプリを作成。プロジェクト名になります。なんでもいいです。
今回、私はrails-todo1にしました

$ rails new rails-todo1

沢山のファイルが作られる。終わったらプロジェクトのディレクトリに移動

$ cd rails-todo1

サーバーを起動させて表示させてみる

$ rails s

welcome_rails.png

こんな画面が出たらOK

これから中身を作っていくので一応流れを確認

今回新規作成で一から作っていきますが、だいたいこんな感じで作ると考えていただければよいかと思います。

新しいアプリ(ページ)を作る手順

全体の流れで考えること

  1. 作るページはどんなページで作る?
  2. データベースに保存するデータは何か?
  3. モデルは作ったか?
  4. マイグレーションファイルを生成&データベースに反映
    • generate modelコマンドするとmigrationファイルも作られる。
    • カラム(データベースの列)を追加したり、データベースの中身を変えたいときにもマイグレーションファイル使う
    • rails db:migrateでマイグレーションファイルの内容をデータベースに反映
  5. ページを表示するのか?データを登録・更新をするのか?

    • 表示するならGET
    • データの登録ならPOST
    • データの更新ならPATCH (PUT)
    • データの削除ならDELETE
  6. ルーティングを決める

    • URLにパラメータをいれる必要があるのか?(例えば各ユーザーの情報ページを表示するならparams[:id]のように書く。ならばURLに〇〇〇/:idのようにidを含める必要がある)
    • URLにパラメータいらない(新規登録や全データ表示など)
  7. 処理を行うコントローラーは作ったか

  8. コントローラーのアクションは作ったか

    • GET?POST?PATCH?DELETE?かを確認してつくる
  9. ビューファイルはいるか?

さっそく簡単なアプリ作成していく

1.どんなもの作るか

↓ なんの飾りもない超シンプルアプリ
image.png

テキストを入力して登録する
todo2.gif

2.データベースに保存するデータは何か

  • テーブル名:Todo
  • カラム名:title
  • デフォルトで作られるカラム(ID, 作成日, 更新日)

3. モデルは作ったか

今回は、まだ何もつくってないので当然必要。
今回作るToDoアプリでは、Todoテーブルのデータ取得・新規登録・データ更新・データ削除を行うのでModelを1つ作成します

というわけで、モデル作成。

$ rails generate model Todo title:string

2.で書いたように必要なテーブルがTodo
必要なカラムがtitleなので、このように書いています。

rails generate model Todoでモデルファイルが作られる
title:string部分は追加したいカラム名と型。
この部分は書かなくても手動で記述すれば作れるのですが、コマンドでこのように打ったほうが楽です

db/migrate/~~~~~~~~~_create_todos.rb
class 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

今回は最初のページが
image.png

Todoを追加するための機能を持つページを「表示」します。
ということでGETメソッドです

6. ルーティングを決める

ルーティングは
どのURLにアクセスすると、どのコントローラーとアクションを使うのか定義するものです。

Railsにはresourcesという便利機能があってルーティング書くのを便利にできる機能があるんですが、今回は原理を理解するために今回は一つ一つルーティングを記述していきましょう。

config/routes.rb
get '/todos', to: 'todos#index', as: 'todos'
post '/todos', to: 'todos#create'

ルーティングを確認する

コンソールに↓を入力するとルーティングを確認できる。

$ rails routes
Prefix  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みたいな感じ
他タブで開いておくといちいちコマンド入れなくても確認できるので便利!

image.png

現状では/todosにGETを設定してtodoコントローラーindexアクションの処理を使えと指定している

補足:_pathについて

$ rails routesだとPrefixtodosという名前になっていてURLに名前を付けて便利に使えるようになっている。これに_pathを追加すると使えます。
todos + _pathtodos_pathです。

todos_pathと書くと/todosと書いたのと同じことになります。
結構使いますので覚えておくといいです。

7. 処理を行うコントローラーは作ったか

コントローラーは大雑把にルーティングモデルビューをつなぐ役割、データ返す役割をしているものです。

mvc_pattern.png

コントローラーは何をするか雑に説明

細かく言うとわかりにくくなるので大雑把に。

  • ルーティングで該当のURLにマッチしたら、コントローラーのアクションに処理が飛んでくる。そして処理を行う。
    • その際にparamsのデータもらうこともあり
  • Todo.new使ってビューファイルで記述するform_with(form_forも含む)で使うフォームでデータを送信するために必要なモデルの部品みたいなものが入った箱作ったりする。
    • ビューで使うならインスタンス変数に格納する
  • Todo.find(params[:id])みたいに自分で作ったModel使ってデータベースの値を取得する。
    • Todo.find(params[:id])Todo.newで作った部品に、取得したデータも含んだものだと思っておけばいいです。※下図参照
    • ビューで使うならインスタンス変数に格納する
  • インスタンス変数はビューで使用可能なのでページを操作によってデータ表示やフォームの送り先を変えられる動的なページを作れる

image.png

初心者のうちはデータ取得したり、データを加工する、ビューで表示するためのデータを@todoみたいなインスタンス変数を作る!
みたいな感じでいいと思います。

コントローラー作成コマンド

以下コマンドで、Controllerを作成します。

とりあえずコントローラーを生成する

$ rails g controller Todos

このコマンドならtodosというコントローラー作成されるので、todos_controller.rbというファイルができます。

8. コントローラーのアクションは作ったか

app/controllers/todos_controller.rb
class 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>

作成した画面

まだデータの登録がないので全件取得しても表示するものがないので、このような表示になっています。
image.png

こんな1~9までのような順序でアプリの作成(もしくは追加)をしていくことになります。

では、次にTodoの全件表示しかできない状態なので
次は新規ToDo追加できる機能を追加します

新規追加機能の作り方

次に新規登録できる機能を作ります。
今度は1~9まの順序で機能を追加していきましょう。

1.どんなもの作るか

ToDoテーブルにデータを新規追加する機能
テキストフィールドに入力した文字をボタン押すとデータベースに登録

image.png

2.データベースに保存するデータは何か

テーブル名:Todo
カラム名:title
デフォルトで作られるカラム(ID, 作成日, 更新日)

titleだけデータベースに保存すればいいことになります。

3. モデルは作ったか

モデルはTodoつかうので新しく作成の必要なし

4. マイグレーションファイルを生成&データベースに反映

モデルはTodo使う。カラムの追加も必要ないのでマイグレーションファイル作る必要も、データベースに反映する必要もない。

5. ページを表示するのか?データを登録・更新をするのか?

カラム名でtitleをデータベースに登録する処理を追加することになります。

ページの表示をしているわけでも、情報の更新をしているわけでもなく、情報の削除でもありません。

こういうデータの登録処理はPOSTで行いましょう。

6. ルーティングを決める

  • メソッド:POST
  • ルーティング:/tasks
  • コントローラー&アクションtasks#create

ルーティングを確認

実はすでに追加してしまっていたのですが、今回使うルーティングは↓の二つ目です。

routes.rb
Rails.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.rb
class 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
としているのは、ページを表示しているわけでないのでリダイレクトしないと処理が終わっても処理が固まった感じになってしまうからです。

処理としては正常に終了してるけど、↓のように何も起こってない…みたいな感じになる。
正常に登録はできているのでリロードするとちゃんと表示されることになる。

todo1.gif

get '/todos', to: 'todos#index', as: 'todos'
as: 'todos'と書くと/todosというURLにtodosという名前を付けて使えるようにしています。

この名前に、ルーティングの名前(todosなど) + _pathtodos_path
のようにするといちいちURLを書く必要がなくなります。

注意点として、Formで入力した情報を新規登録や更新する際はセキュリティ対策としてStrong Parametersを使わないとエラーでますので追加します。

Strong Parameters

セキュリティ対策で、入れないとエラーになります。
難しいこと省いて、フォームから受け取ったデータを渡すと、許可したデータだけ返却されるくらい覚えておけばとりあえずは良いと思います。

そして追加したのがtodo_paramsメソッド。また、privateを使うと、todo_paramsメソッドが外部から使えないようにできるのでセキュリティを高まります。
難しいことはいいので、privateの下にStrong Parametersいれるんだなくらいで今はいいです。

app/controllers/todos_controller.rb
class 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
end

9. ビューファイルはいるか?

データベースでのデータの追加処理では使いません。
ただし、送信するためのテキスト入力フォーム送信ボタン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_controllerindexにフォームを作るための部品を用意するため@todo = Todo.newを追加

app/controllers/todos_controller.rb
class TodosController < ApplicationController
  def index
    @todos = Todo.all
    @todo = Todo.new # フォーム作るために必要
  end
end

image.png

登録機能完成

データベースに登録する機能が完成しました!

todo2.gif

今度は更新と削除

次は更新機能を実装します。

必要機能

  • 情報更新ページ(今回はeditアクションとedit.html.erbにて)
    • ルーティングを追加する
    • editアクションで編集する情報を表示する todos_controller.rb
    • ビューで見た目を作る edit.html.erb
  • 情報更新処理
    • updateアクションで情報の更新処理 todos_controller.rb

更新情報編集ページ用ルーティング追加

routes.rb
Rails.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.rbeditアクションを使用します。ルーティングの名前はedit_todoとでもしておきます。

更新情報編集ページ用コントローラー

editアクションは、情報編集画面(/todos/:id/edit)にアクセスすると処理されるアクションで、ビューで見た目を作るときに必要なデータの取得します。

editアクションを情報を追加します。

todos_controller.rb
  def 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])id1だったら取得するデータを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.rb
Rails.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' # 更新処理用ルーティング追加
end
app/controllers/todos_controller.rb
  def update
    @todo = Todo.find(params[:id])
    @todo.update(todo_params)
    redirect_to todos_path
  end

resouses使えるならこんな感じでシンプルに書ける

<-- 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 %>

image.png

<td><%= link_to '編集', edit_todo_path(todo.id) %></td>

ついでに、ちゃんと省略して書かないとこういう書き方になる。todotodo.id
Railsがいい感じで判断してくれるのでこのようになりますが、原理は覚えておくほうがいいと思います。
htmlだと↓のようなものが作られる
<a href="/todos/1/edit">編集</a>

削除機能実装

削除機能はわかってしまえば他と大して変わらないので楽です。
1. ルーティング追加
2. コントローラーにdestroyアクション追加
3. ビューに削除ボタンのリンク追加

routes.rb
Rails.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.rb
  def 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 %>

削除機能が完成しました。

todo3.gif

見た目は最悪ですが、ToDo登録・更新・削除機能が完成しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

とりあえずこれだけ。Rails ToDoアプリ作成【CRUD】勉強会 #1

初心者用Railsアプリ練習帳

勉強会を開き、Rails初学者に日頃教えています。初心者向け勉強回資料用もかねて作成。
ある程度基礎を学習していると、読みながら真似はできるけど、いまいち全体の知識がつながっていないので自分で作ることができない…

そんな状態になりがちでした。

対象者

  • Railsでなんでもいいからの教材をやった人
  • Rails基礎はなんとなくわかってきた人
  • Railsで自分でアプリ作るのはまだ無理そうという人
  • Rails以外のフレームワークならわかる人

※ 正確さより概念とアプリ作成の流れを重視します。

コンセプト

他にも言っている方がいますが、勉強会で教えている経験から初学者が同じ教材を何周もするのは上達が遅いように感じています。

知識はあるけど自分で一から作るのは無理そう。
エラーが出て対処出来ない

ということになりがちでした。

こういう場合、実際に手を動かし、自分の頭で考えて簡単なアプリを作る必要があるけど、

  • いきなりだと何を作ればいいのかわからない…
  • 一連のアプリを作っていく順番がわからない…
  • エラーでても対処できない…
  • 簡単なのから作ってみればいいと言うが何が簡単なものなのか分からない…
  • 何個もアプリ自分で作るといいらしいが何を作れば練習やスキル向上なるか分からない…

そんな人が未経験からプログラミングをやる人には何人もいました。
現状の私の考えですが、世の中にある教材は見た目とか完成度ないと売れないし、強い人達からツッコまれるので分かりやすさより精確さが必要とされる教材になってしまうようにおもいます。

なので、後で身につければ良いこともあれもこれも付け足しており、本当にコレだけ!
という事項が初心者には見えにくくなっている気がします。

それをうけて今回、ムダなものとにかく剥ぎまくって、コアな部分だけを数多く練習すればみんな出来るようになるのではないかと考え、練習問題を作りました。
今後も感想とかもらって継続的にブラッシュアップしていければと思っています。

手順

  1. とりあえずToDoアプリをなぞって作ってみる(少しだけでも勉強した人が前提) ←今回はココ
  2. 超シンプルアプリを量産。手を動かして覚える!概念つかむ!
  3. データベースから欲しいデータを取得する!千本ノックのようにやりまくります!

2,3は個人的に開く勉強会で解説します。

とりあえずToDoアプリをなぞって作ってみる

環境構築はローカルでもいいですけど、WindowsやMacで違うので初心者はcloud9で作っていいと思います。
構築方法は今回省略

以下のコマンドでアプリを作成。プロジェクト名になります。なんでもいいです。
今回、私はrails-todo1にしました

$ rails new rails-todo1

沢山のファイルが作られる。終わったらプロジェクトのディレクトリに移動

$ cd rails-todo1

サーバーを起動させて表示させてみる

$ rails s

welcome_rails.png

こんな画面が出たらOK

これから中身を作っていくので一応流れを確認

今回新規作成で一から作っていきますが、だいたいこんな感じで作ると考えていただければよいかと思います。

新しいアプリ(ページ)を作る手順

全体の流れで考えること

  1. 作るページはどんなページで作る?
  2. データベースに保存するデータは何か?
  3. モデルは作ったか?
  4. マイグレーションファイルを生成&データベースに反映
    • generate modelコマンドするとmigrationファイルも作られる。
    • カラム(データベースの列)を追加したり、データベースの中身を変えたいときにもマイグレーションファイル使う
    • rails db:migrateでマイグレーションファイルの内容をデータベースに反映
  5. ページを表示するのか?データを登録・更新をするのか?

    • 表示するならGET
    • データの登録ならPOST
    • データの更新ならPATCH (PUT)
    • データの削除ならDELETE
  6. ルーティングを決める

    • URLにパラメータをいれる必要があるのか?(例えば各ユーザーの情報ページを表示するならparams[:id]のように書く。ならばURLに〇〇〇/:idのようにidを含める必要がある)
    • URLにパラメータいらない(新規登録や全データ表示など)
  7. 処理を行うコントローラーは作ったか

  8. コントローラーのアクションは作ったか

    • GET?POST?PATCH?DELETE?かを確認してつくる
  9. ビューファイルはいるか?

さっそく簡単なアプリ作成していく

1.どんなもの作るか

↓ なんの飾りもない超シンプルアプリ
image.png

テキストを入力して登録する
todo2.gif

2.データベースに保存するデータは何か

  • テーブル名:Todo
  • カラム名:title
  • デフォルトで作られるカラム(ID, 作成日, 更新日)

3. モデルは作ったか

今回は、まだ何もつくってないので当然必要。
今回作るToDoアプリでは、Todoテーブルのデータ取得・新規登録・データ更新・データ削除を行うのでModelを1つ作成します

というわけで、モデル作成。

$ rails generate model Todo title:string

2.で書いたように必要なテーブルがTodo
必要なカラムがtitleなので、このように書いています。

rails generate model Todoでモデルファイルが作られる
title:string部分は追加したいカラム名と型。
この部分は書かなくても手動で記述すれば作れるのですが、コマンドでこのように打ったほうが楽です

db/migrate/~~~~~~~~~_create_todos.rb
class 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

今回は最初のページが
image.png

Todoを追加するための機能を持つページを「表示」します。
ということでGETメソッドです

6. ルーティングを決める

ルーティングは
どのURLにアクセスすると、どのコントローラーとアクションを使うのか定義するものです。

Railsにはresourcesという便利機能があってルーティング書くのを便利にできる機能があるんですが、今回は原理を理解するために今回は一つ一つルーティングを記述していきましょう。

config/routes.rb
get '/todos', to: 'todos#index', as: 'todos'
post '/todos', to: 'todos#create'

ルーティングを確認する

コンソールに↓を入力するとルーティングを確認できる。

$ rails routes
Prefix  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みたいな感じ
他タブで開いておくといちいちコマンド入れなくても確認できるので便利!

image.png

現状では/todosにGETを設定してtodoコントローラーindexアクションの処理を使えと指定している

補足:_pathについて

$ rails routesだとPrefixtodosという名前になっていてURLに名前を付けて便利に使えるようになっている。これに_pathを追加すると使えます。
todos + _pathtodos_pathです。

todos_pathと書くと/todosと書いたのと同じことになります。
結構使いますので覚えておくといいです。

7. 処理を行うコントローラーは作ったか

コントローラーは大雑把にルーティングモデルビューをつなぐ役割、データ返す役割をしているものです。

mvc_pattern.png

コントローラーは何をするか雑に説明

細かく言うとわかりにくくなるので大雑把に。

  • ルーティングで該当のURLにマッチしたら、コントローラーのアクションに処理が飛んでくる。そして処理を行う。
    • その際にparamsのデータもらうこともあり
  • Todo.new使ってビューファイルで記述するform_with(form_forも含む)で使うフォームでデータを送信するために必要なモデルの部品みたいなものが入った箱作ったりする。
    • ビューで使うならインスタンス変数に格納する
  • Todo.find(params[:id])みたいに自分で作ったModel使ってデータベースの値を取得する。
    • Todo.find(params[:id])Todo.newで作った部品に、取得したデータも含んだものだと思っておけばいいです。※下図参照
    • ビューで使うならインスタンス変数に格納する
  • インスタンス変数はビューで使用可能なのでページを操作によってデータ表示やフォームの送り先を変えられる動的なページを作れる

image.png

初心者のうちはデータ取得したり、データを加工する、ビューで表示するためのデータを@todoみたいなインスタンス変数を作る!
みたいな感じでいいと思います。

コントローラー作成コマンド

以下コマンドで、Controllerを作成します。

とりあえずコントローラーを生成する

$ rails g controller Todos

このコマンドならtodosというコントローラー作成されるので、todos_controller.rbというファイルができます。

8. コントローラーのアクションは作ったか

app/controllers/todos_controller.rb
class 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>

作成した画面

まだデータの登録がないので全件取得しても表示するものがないので、このような表示になっています。
image.png

こんな1~9までのような順序でアプリの作成(もしくは追加)をしていくことになります。

では、次にTodoの全件表示しかできない状態なので
次は新規ToDo追加できる機能を追加します

新規追加機能の作り方

次に新規登録できる機能を作ります。
今度は1~9まの順序で機能を追加していきましょう。

1.どんなもの作るか

ToDoテーブルにデータを新規追加する機能
テキストフィールドに入力した文字をボタン押すとデータベースに登録

image.png

2.データベースに保存するデータは何か

テーブル名:Todo
カラム名:title
デフォルトで作られるカラム(ID, 作成日, 更新日)

titleだけデータベースに保存すればいいことになります。

3. モデルは作ったか

モデルはTodoつかうので新しく作成の必要なし

4. マイグレーションファイルを生成&データベースに反映

モデルはTodo使う。カラムの追加も必要ないのでマイグレーションファイル作る必要も、データベースに反映する必要もない。

5. ページを表示するのか?データを登録・更新をするのか?

カラム名でtitleをデータベースに登録する処理を追加することになります。

ページの表示をしているわけでも、情報の更新をしているわけでもなく、情報の削除でもありません。

こういうデータの登録処理はPOSTで行いましょう。

6. ルーティングを決める

  • メソッド:POST
  • ルーティング:/tasks
  • コントローラー&アクションtasks#create

ルーティングを確認

実はすでに追加してしまっていたのですが、今回使うルーティングは↓の二つ目です。

routes.rb
Rails.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.rb
class 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
としているのは、ページを表示しているわけでないのでリダイレクトしないと処理が終わっても処理が固まった感じになってしまうからです。

処理としては正常に終了してるけど、↓のように何も起こってない…みたいな感じになる。
正常に登録はできているのでリロードするとちゃんと表示されることになる。

todo1.gif

get '/todos', to: 'todos#index', as: 'todos'
as: 'todos'と書くと/todosというURLにtodosという名前を付けて使えるようにしています。

この名前に、ルーティングの名前(todosなど) + _pathtodos_path
のようにするといちいちURLを書く必要がなくなります。

注意点として、Formで入力した情報を新規登録や更新する際はセキュリティ対策としてStrong Parametersを使わないとエラーでますので追加します。

Strong Parameters

セキュリティ対策で、入れないとエラーになります。
難しいこと省いて、フォームから受け取ったデータを渡すと、許可したデータだけ返却されるくらい覚えておけばとりあえずは良いと思います。

そして追加したのがtodo_paramsメソッド。また、privateを使うと、todo_paramsメソッドが外部から使えないようにできるのでセキュリティを高まります。
難しいことはいいので、privateの下にStrong Parametersいれるんだなくらいで今はいいです。

app/controllers/todos_controller.rb
class 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
end

9. ビューファイルはいるか?

データベースでのデータの追加処理では使いません。
ただし、送信するためのテキスト入力フォーム送信ボタン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_controllerindexにフォームを作るための部品を用意するため@todo = Todo.newを追加

app/controllers/todos_controller.rb
class TodosController < ApplicationController
  def index
    @todos = Todo.all
    @todo = Todo.new # フォーム作るために必要
  end
end

image.png

登録機能完成

データベースに登録する機能が完成しました!

todo2.gif

今度は更新と削除

次は更新機能を実装します。

必要機能

  • 情報更新ページ(今回はeditアクションとedit.html.erbにて)
    • ルーティングを追加する
    • editアクションで編集する情報を表示する todos_controller.rb
    • ビューで見た目を作る edit.html.erb
  • 情報更新処理
    • updateアクションで情報の更新処理 todos_controller.rb

更新情報編集ページ用ルーティング追加

routes.rb
Rails.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.rbeditアクションを使用します。ルーティングの名前はedit_todoとでもしておきます。

更新情報編集ページ用コントローラー

editアクションは、情報編集画面(/todos/:id/edit)にアクセスすると処理されるアクションで、ビューで見た目を作るときに必要なデータの取得します。

editアクションを情報を追加します。

todos_controller.rb
  def 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])id1だったら取得するデータを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.rb
Rails.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' # 更新処理用ルーティング追加
end
app/controllers/todos_controller.rb
  def update
    @todo = Todo.find(params[:id])
    @todo.update(todo_params)
    redirect_to todos_path
  end

resouses使えるならこんな感じでシンプルに書ける

<-- 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 %>

image.png

<td><%= link_to '編集', edit_todo_path(todo.id) %></td>

ついでに、ちゃんと省略して書かないとこういう書き方になる。todotodo.id
Railsがいい感じで判断してくれるのでこのようになりますが、原理は覚えておくほうがいいと思います。
htmlだと↓のようなものが作られる
<a href="/todos/1/edit">編集</a>

削除機能実装

削除機能はわかってしまえば他と大して変わらないので楽です。
1. ルーティング追加
2. コントローラーにdestroyアクション追加
3. ビューに削除ボタンのリンク追加

routes.rb
Rails.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.rb
  def 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 %>

削除機能が完成しました。

todo3.gif

見た目は最悪ですが、ToDo登録・更新・削除機能が完成しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactアプリをFirebaseにデプロイしたらBlankページしか表示されない

はじめに

Reactでポートフォリオを作成してHerokuで公開していました。しかしHerokuには無料枠があり、常時起こし続けているわけにもいきません。
そこでFirebaseにデプロイをしてHostingをしてもらおうと企みました。

デプロイまでは順調だったのですが、指定されたURL開いてもなぜかBlankページしか表示されませんでした。

スクリーンショット 2019-11-13 21.08.39.png

環境

  • Mac OS X EL Capitan 10.14.5
  • react@16.11.0
  • react-dom@16.11.0

FirebaseでHostingする方法はQiitaの以下の記事を参考にしました。
(初心者向け)Firebase HostingへReactプロジェクトを公開する手順

課題

デプロイしたアプリのコンソールを確認すると、以下のようなエラーが出ていました。

スクリーンショット 2019-11-13 21.08.39.png

SyntaxError: Unexpected token '<'
しかしエラー内で指定されているファイルはReactでビルドを行なった煩雑なものであり、読んでも全くわかりません。

解決策

どうやら直前にGitHub Pagesにデプロイしようとして中断したのが悪さをしていそうです。

package.json
{
  "homepage": "https://[username].github.io/[work]/",
}

この部分をFirebaseのデプロイ時に取得したURLに書き換えて再度ビルドしました。

package.json
{
  "homepage": "https://[プロジェクトID].firebaseapp.com/",
}

ビルドが終わったら再度デプロイしてみます。

image.png

うまくサイトが表示された!

終わりに

ポートフォリオサイトはFirebaseで公開中です。
ポートフォリオ

あれ? デプロイできたのに真っ白で何も表示されないぞ? と困ったら1度pacage.jsonの中身を覗いて見ると謎が解けるかもしれません。

参考記事

Stack Overflow
Create-React-App build - “Uncaught SyntaxError: Unexpected token <”

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

reactNative

reactNative

image.png

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/78b3295ff3f37969ab50

React 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を学ぶという選択肢もあります。これは興味深いテクノロジーですが、いくつか警告があります。

  1. すべての開発者は、強い型付けの、コンパイラ型の、オブジェクト指向言語を習得する必要があります。そしてJava /Objective-C / Swiftはいずれもこれに最適な選択です。あなたが望むかどうかにかかわらず、いずれとにかくJavaScriptは学ぶことになります。

  2. React Nativeは、AppleとGoogleのどちらかにも正式にサポートされていません。これはつまり、OSやAndroidから新しいアナウンスメントが公表されても、React Nativeで完全には動作しない可能性があることを意味します。たとえば、iOS 10iMessageアプリの新機能が追加されたとアナウンスされた時点では、React Nativeによってそのようなアプリのコードを書く方法はなかったと思います。こういうケースでは、iOSのネイティブアプリの開発方法を習得している必要があるのです。また現在でも、React Nativeを使ってApple Watchアプリを開発できるかどうかさえ定かではありませんが、ネイティブ開発を学んでおけば何の問題もないでしょう。

  3. 第三に、プロジェクトの寿命を念頭に置く必要があります。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);

image.png

・入力項目あり

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);

image.png

最後に

ネイティブ開発はまだまだ成熟していない技術て、日に日に進歩をしている元気な技術です
android もjava → kotlin が公式言語(google曰く)になったし
ios も objective-c → swift に変更になり言語も技術もどんどん進化しています

現在ほとんどのアプリでandroid ios両方でもリリースが当たり前になってきています。
両方の端末で開発をするとなると技術者不足や工数不足、お金の不足など沢山の問題にぶち当たります
そのため、クロスプラットフォーム技術もどんどん進化して行きます
今回紹介した React Native以外にもkotlin nativeなど似たような技術も存在しています

今後も自分の価値をあげるためにも、こういう技術はどんどん取り入れていきましょう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ExpoをEject後、Expoから証明書に必要な鍵キーを取得する方法

Expoからキーを取得する

$ expo fetch:android:keystore

これで、証明書に必要なキーkeystoreを取得できます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

いまからはじめる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;

上記の例で varlet に変更すると、エラーで失敗します。

varの問題点 その2: 変数の上書き

var を使用するとき、好きなだけ同じ変数を何度でも宣言することができます、しかし let ではできません。

var myName = 'Chris';
var myName = 'Bob';

上記の例で varlet に変更すると、エラーで失敗します。

これらの問題点は潜在的なバグの要因になりかねません。
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: 42

objectのプロパティなどは変更できます。

const obj = {
  number: 42,
};

try {
  obj.number = 99;
} catch(err) {
  console.log(err);
}

console.log(obj.number);
// => 99

JavaScriptでは型がないため、変数にどのような値が格納されるのか制限できません。
変数を定義する際は 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); // => 32

TIPS: 変数の入れ替え

配列の分割代入を使用すると、変数の値の入れ替えが簡単に行えます。

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); // => kimura

TIPS 関数の引数に既定値を設定する

関数の引数にオブジェクトを渡すようにすることで、名前付き引数のような機能を実現できます。
また、既定値を設定することで省略可能な引数を定義できます。

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-appnpm でインストールできます。

$ 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.js
import 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.js
import 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.js
import 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.js
import React from 'react';

function Message() {
  return (
    <p>Original Message.</p>
  );
}

export default Message;

App.jsMessage コンポーネントを配置します。

App.js
import 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.js
import 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.js
import React from 'react';

function Message(props) {
  return (
    <p>Hello, {props.name}!</p>
  );
}

export default Message;

コンポーネントは繰り返し使用できます。

App.js
import 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.js
import 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 メソッドが呼ばれます。
handleTextInputsetName メソッドにテキストボックスの値を渡します。
name の値が更新されると Message が再描画されます。

子から親に値を渡す

フォームをコンポーネント化することを考えてみましょう。
まずは名前を入力するフォームのコンポーネントを作成します。

$ touch src/components/NameForm.js

子から親にデータを渡すためには、親から子にコールバック関数を渡します。
子にて props に渡されたコールバック関数を実行します。

NameForm.js
import 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.js
import 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.js
import 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.js
import 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.js
import 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;

AppTodoForm を追加します。
TodoFormSave ボタンが押されたらその結果を state の配列に追加します。

App.js
import 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.js
import 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.js
import 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.js
import 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 コンポーネントのstate edit を更新するよう実装します。

Todo.js
import 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)}
      />
    );
  }

// ...省略...

ここまで実装できたら、編集モードと通常モードの切り替えができることを確認します。

更新処理を実装する

仕上げに更新処理を実装します。

AppTodo コンポーネントから呼ばれる更新メソッド 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コンポーネントの onSavehandleUpdate を渡します。

Todoコンポーネントでは受け取った onSave をさらに TodoForm に渡しますが、更新が完了したら自身の編集モードを終了するようフラグを更新しておきます。

Todo.js
import 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 だけでなく IDCreatedAt などの値も引き渡すように修正します。
面倒なので受け取った props をすべてTodoデータに展開してしまいます。

TodoForm.js
import 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.js
import 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.js
import 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フック

コンポーネントのデータ管理の基本は stateprops ですが、コンポーネントが多階層になった場合にバケツリレーのごとくデータを渡すのは非常に面倒です。

そういったときに使える機能が Context Hook になります。

例えば、現在の認証済みユーザー・テーマ・優先言語といったデータを共有する場合に有用です。

Contextの使用例

今回はTodoアプリにテーマ選択機能を追加してみます。

App.js
import 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コンポーネントで括ります。
Providervalue に現在の値を渡します。
他コンポーネントは export された ThemeContext を介して value にセットされている値を取得することができます。

Context を参照する側は以下のように実装します。

Todo.js
import 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.js
import 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のテーマが切り替わります。

サンプルが単純なためあまり威力が実感できないかもしれませんが、コンポーネント数が多かったり、孫・ひ孫のコンポーネントに値を渡したい場合に便利な機能です。


参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactライブラリー開発環境で、"Invalid Hook Call Warning"に対処する

"Invalid Hook Call Warning"の原因と対応方法

Reactライブラリーを開発するため環境構築で"Invalid Hook Call Warning"に対処したので、その原因と対応方法について書きます。

"Invalid Hook Call Warning"の原因とは?

原因は3種類ある

ReactのHooksの書き方や、Reactプロジェクトの構成が原因で発生するエラーです。

Reactの公式ページで詳しく解説されていて具体的には3つの原因があります。

  1. 関数型コンポーネントの外でhooksを使おうとしている
  2. コンポーネントのトップレベル以外(if文など)でhooksを使おうとしている
  3. 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を参考にしました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む