KEITA LOG (original) (raw)
SSH についてまとめました。
この記事は以下の構成になっています。
SSH とは
SSH とは、Secure Shell の略で、ネットワークを通じて他のコンピュータに安全に接続するための仕組み。
主にサーバーに対して遠隔操作を行う時に使われる。
SSH の前は telnet というコマンドが使われていたが、telnet はやりとりする情報をそのまま送信するのに対して、SSH は暗号化して送信するのでより安全である。
SSH で接続する流れ
SSH で接続するには以下の流れを行う。
- ターミナルで ssh コマンドを実行する
- 認証情報を入力する
- リモートサーバーへのアクセスが開始される
1. ターミナルで ssh コマンドを実行する
SSH で接続するには、以下のようにターミナルで ssh コマンドを実行する。
ssh ユーザー名@サーバーアドレス
2. 認証情報を入力する
接続先がパスワード認証の場合、リモートサーバーのユーザーアカウントのパスワードを求められるので、パスワードの入力を行う。
公開鍵認証の場合は、事前に設定した秘密鍵ファイルで認証が行われる。
認証については「SSH の認証方法」で後述。
3. リモートサーバーへのアクセスが開始される
認証が成功すると、リモートサーバーに接続され、コマンドを実行できる状態になる。具体的にはリモートサーバーのシェルにアクセスできる状態である。
SSH 接続を通して、ファイルの編集やシステムの管理などを行うことができる。
SSH の認証方法
SSH での認証方法は主にパスワードと公開鍵・秘密鍵の 2 つの方法がある。
パスワード認証
パスワード認証はその名の通り、パスワードで認証を行う方法。
先ほどの「SSH で接続する流れ」はパスワード認証にあたる。
ただしパスワードはブルートフォース攻撃や盗聴のリスクがあり、セキュリティ的によくない方法なので、公開鍵認証が推奨される。
公開鍵認証
公開鍵認証は公開鍵と秘密鍵のペアを使って認証を行う方法で、パスワードより安全である。
サーバーに公開鍵を登録し、接続時にローカルに保存された秘密鍵を使って認証する仕組み。
SSH 接続だけでなく、SSL/TLS や VPN などでも使われる。
公開鍵認証で SSH 接続する方法はこちらの記事で解説。
コンピュータのデータの単位と、n 進数についてまとめました。
この記事は以下の構成になっています。
- コンピュータのデータの単位
- ビット
- バイト
- n 進数
- 2 進数
- 10 進数
- 16 進数
- 進数の変換
- 2 進数 → 10 進数
- 10 進数 → 2 進数
コンピュータのデータの単位
コンピュータはアプリを動かしたり、動画を見たり、音楽を聞いたりできるが、どんなデータも最終的には 0 と 1 の電気信号で処理されている。
0 はコンピュータの回路がオフ、1 はオンの状態を表す。
この 0 か 1 はビット(bit)という単位であり、8 ビットで 1 バイト(byte)になる。
コンピュータは 単純な 0 と 1 を組み合わせることで複雑なデータも表現することができる。
ビット
ビットはデータの最小単位で、0 か 1 のどちらか 1 つの値を持つ。
バイト
1 バイトは 8 ビットであり、1 バイトで 0〜255 の異なる種類の値を表現できる。
8 ビットは 2 の 8 乗で 256 になり、0~255 の範囲はメモリ管理や演算で扱いやすい数字の範囲になるため、8 ビットが 1 バイトとして扱われるようになった。
n 進数
n 進数とは数値を表現する方法で、進数は使用する数字の数を表す。
人間が基本的に使うのは 10 進数だが、コンピュータでは 2 進数や 16 進数が使われる。
2 進数
2 進数は 0 と 1 だけを扱うもので、コンピュータのデータは最終的には 2 進数で処理される。
2 進数のデータはバイナリデータともいう。
10 進数
10 進数は 0〜9 の数字を扱うもので、日常的に使われる数値の表現。
16 進数
16 進数は 0〜9 の数字と A~F のアルファベットを組み合わせた数値の表現で、メモリアドレスやカラーコードなどで使われる。
16 進数は 10 進数より桁が少なくなるメリットがある。
進数の変換
2 進数 → 10 進数
2 進数を 10 進数に変換するには、1 桁目から順番に各桁を 2 のべき乗で掛け算をして合計を求める。
例えば、1110011 を 10 進数に変換する。
計算式は以下のようになる。
1 × 2^6 + 1 × 2^5 + 1 × 2^4 + 0 × 2^3 + 0 × 2^2 + 1 × 2^1 + 1 × 2^0
= 1 × 64 + 1 × 32 + 1 × 16 + 0 × 8 + 0 × 4 + 1 × 2 + 1 × 1
= 115
10 進数 → 2 進数
10 進数を 2 進数に変換するには、元の数を 2 で割り続けて余りを記録し、最終的に余りを逆順に並べる。
さっきの 115 を 2 進数に変換する。
計算式は以下のようになる。
115 ÷ 2 = 57 余り 1 57 ÷ 2 = 28 余り 1 28 ÷ 2 = 14 余り 0 14 ÷ 2 = 7 余り 0 7 ÷ 2 = 3 余り 1 3 ÷ 2 = 1 余り 1 1 ÷ 2 = 0 余り 1
この余りを逆から並べると 1110011 になる。
Rails の認証機能を実装するための gem である devise のインストール方法、使い方をまとめました。
この記事は以下の構成になっています。
- devise とは
- devise で作れる機能
- devise を使う手順
- gem をインストールする
- devise の設定ファイルをインストールする
- devise の認証モデルを作成する
- マイグレーションを実行する
- その他
- ビューをカスタマイズする
- コントローラをカスタマイズする
- サインアップ、サインイン、サインアウト後のリダイレクトの URL を変更する
- 新規登録時に name を追加する
- パスワードの文字数を変更する
- 管理者を実装する
devise とは
devise は Rails で認証機能を実装するための gem。
公式ドキュメントhttps://github.com/heartcombo/devise
devise を使うことで、一から認証機能を作る必要がなく、セキュリティ的にも安全。
devise で作れる機能
devise によって、以下のような機能を簡単に作れる。
- ユーザー登録、編集、削除 - registerable
- 認証 - database_authenticatable
- パスワードリセット、アカウント復旧 - recoverable
- バリデーション(メールアドレス・パスワード) - validatable
- ログイン状態の保持 - rememberable
- メール認証 - Confirmable
- アカウントロック - Lockable
- ログイン情報の追跡 - Trackable
- セッションのタイムアウト - Timeoutable
- OmniAuth 認証 - Omniauthable
devise ではこれらの機能をregisterable
やdatabase_authenticatable
といったモジュールという単位で管理しており、必要になる機能だけを有効化して使うことができる。
devise を使う手順
devise は以下の手順で使う。
- gem をインストールする
- devise の設定ファイルをインストールする
- devise の認証モデルを作成する
- マイグレーションを実行する
1. gem をインストールする
まずは devise の gem をインストールする。
Gemfile に以下の記述を行い、bundle install
を実行する。
gem 'devise'
2. devise の設定ファイルをインストールする
gem をインストールできたら、以下のコマンドを実行して devise の設定ファイルをインストールする。
rails g devise:install
インストールが成功すると、以下のメッセージがターミナルに表示される。
create config/initializers/devise.rb
create config/locales/devise.en.yml
===============================================================================
Depending on your application's configuration some manual setup may be required:
Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
In production, :host should be set to the actual host of your application.
- Required for all applications. *
Ensure you have defined root_url to something in your config/routes.rb. For example:
root to: "home#index"
- Not required for API-only Applications *
Ensure you have flash messages in app/views/layouts/application.html.erb. For example:
<%= notice %>
<%= alert %>
- Not required for API-only Applications *
You can copy Devise views (for customization) to your app by running:
rails g devise:views
- Not required *
===============================================================================
メッセージでは Devise の設定後に追加で行うべき手動の設定を説明しており、内容は以下の通り。
- default_url_options を設定する(すべてのアプリで必要)
- ルート URL を設定する(API では不要)
- フラッシュメッセージをレイアウトに追加する(API では不要)
- Devise のビューをコピーする(任意)
3. devise の認証モデルを作成する
以下のコマンドを実行すると、devise の認証モデルのモデルファイルとマイグレーションファイルとルーティングが作られる。
rails g devise [モデル名]
例として、User モデルを作る。
rails g devise User
コマンドを実行すると以下のメッセージが表示される。
rails g devise User invoke active_record identical db/migrate/20241112163054_add_devise_to_users.rb unchanged app/models/user.rb route devise_for :users
users テーブルのマイグレーションファイル、User モデルのファイル、users のルーティングが作られている。
user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable end
User モデルに書かれているのがモジュール。使われていないモジュールはコメントアウトされている。
devise_create_users.rb
frozen_string_literal:true
class DeviseCreateUsers < ActiveRecord::Migration[7.1] def change create_table :users do |t|
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
t.string :reset_password_token
t.datetime :reset_password_sent_at
t.datetime :remember_created_at
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
end end
マイグレーションファイルでは、モジュールに必要なカラムが書かれており、使いたいモジュールに合わせてカラムのコメントを外す。
config/routes.rb
Rails.application.routes.draw do devise_for :users end
4. マイグレーションを実行する
rails db:migrate
を実行して、マイグレーションを適用する。
その他
ビューをカスタマイズする
devise に関連する view ファイルを作成するには以下のコマンドを実行する。
rails g devise:views
view ファイルを作成することでカスタマイズすることができる。
コントローラをカスタマイズする
devise に関連するコントローラファイルを作成するには以下のコマンドを実行する。
rails g devise:controllers users
すると、app/controllers/users
ディレクトリにいくつかコントローラファイルが作られる。
app/controllers/users/sessions_controller.rb
であれば以下の内容になっている。
frozen_string_literal:true
class Users::SessionsController < Devise::SessionsController
end
このように Devise のコントローラを継承してコントローラを作成している。
コントローラファイルを作成することでカスタマイズすることができる。
サインアップ、サインイン、サインアウト後のリダイレクトの URL を変更する
サインアップ、サインイン、サインアウトそれぞれのリダイレクトの URL を変更するには以下の手順を行う。
- コントローラを作成する
- メソッドをオーバーライド
- ルーティングを設定する
1. コントローラを作成する
コントローラをカスタマイズするためにrails g devise:controllers users
を実行してコントローラを作成する。
サインアップ後のリダイレクトの URL を変更するには Devise の RegistrationsController を、サインインとサインアウト後のリダイレクトの URL を変更するには Devise の SessionsController をカスタマイズする。
2. メソッドをオーバーライド
サインアップは以下のようにafter_sign_up_path_for
をオーバーライドする。
class Users::RegistrationsController < Devise::RegistrationsController
def after_sign_up_path_for(resource) dashboard_path end end
サインインは以下のようにafter_sign_in_path_for
メソッドをオーバーライドする。
class Users::SessionsController < Devise::SessionsController
def after_sign_in_path_for(resource) dashboard_path end end
サインアウトは以下のようにafter_sign_out_path_for
メソッドをオーバーライドする。
class Users::SessionsController < Devise::SessionsController
def after_sign_out_path_for(resource) root_path end end
3. ルーティングを設定する
config/routes.rb
で、Devise の RegistrationsController と SessionsController をカスタマイズしたコントローラにマッピングする。
devise_for :users, controllers: { registrations: 'users/registrations' sessions: 'users/sessions' }
新規登録時に name を登録する
デフォルトではメールアドレスとパスワードで登録されるので、名前も登録したい場合は以下の手順を行う。
- データベースに name を追加
- ストロングパラメータの設定
1. データベースに name を追加
rails generate migration AddNameToUsers name:string rails db:migrate
2. ストロングパラメータの設定
コントローラのストロングパラメータでユーザー登録時に name を許可するようにする。
class ApplicationController < ActionController::Base before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:name]) end end
パスワードの文字数を変更する
config/initializers/devise.rb
の以下の部分でパスワードの最小文字数と最大文字数を変更することができる。
config.password_length = 6..128
管理者を実装する
devise を使って管理者を実装するには以下の 3 つの方法がある。
- 管理者モデルを作る
- ユーザーモデルに管理者属性を追加する
- enum を使ってユーザーに役割を持たせる
管理者モデルを作る
管理者モデルを作る場合は以下の手順で行う。
- モデルを作成
- モデルの設定
- マイグレーションファイルの作成と実行
- ルーティングの設定
1. モデルを作成
Admin モデルを作成する。
rails g devise Admin
2. モデルの設定
Admin モデルに必要な devise モジュールを追加する。
class Admin < ActiveRecord::Base devise :database_authenticatable, :trackable, :timeoutable, :lockable end
3. マイグレーションファイルの編集と実行
マイグレーションファイルを必要に応じて編集し、マイグレージョンを実行する。
class DeviseCreateAdmins < ActiveRecord::Migration def self.up create_table(:admins) do |t| t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" t.integer :sign_in_count, default: 0 t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip t.integer :failed_attempts, default: 0 t.string :unlock_token t.datetime :locked_at t.timestamps end end
def self.down drop_table :admins end end
rails db:migrate
4. ルーティングの設定
config/routes.rb
に以下を追加する。
devise_for :admins
ユーザーモデルに管理者属性を追加する
ユーザーモデルに管理者属性を追加するには、users テーブルに boolean のadmin
を追加する。
rails g migration add_admin_to_users admin:boolean, default: false
マイグレーションを実行。
rails db:migrate
enum を使ってユーザーに役割を持たせる
enum を使ってユーザーに役割を持たせるには、ユーザーモデルに役割(role)を追加する。
users テーブルにrole
を作成するマイグレーションを作成。
rails g migration AddRoleToUsers role:integer
マイグレーションを実行。
rails db:migrate
User モデルに enum を設定する。
class User < ApplicationRecord enum role: [:user, :vip, :admin] after_initialize :set_default_role, if: :new_record?
def set_default_role self.role ||= :user end end
Rails のアセットパイプラインについてまとめました。
この記事は以下の構成になっています。
- アセットパイプラインとは
- アセットパイプラインが行うこと
- アセットパイプラインの仕組み
- アセットの管理
- CSS
- JavaScript
- 画像
- 開発環境と本番環境での違い
前提として Rails7 のアセットパイプラインについて解説しています。
アセットパイプラインとは
アセットパイプラインは Rails で CSS、JavaScript、画像などのアセットファイルを圧縮したり、まとめたりして効率的に管理するための仕組み。
Rails7 ではデフォルトで導入されており、無効にする場合はrails new
を実行するときに--skip-asset-pipeline
を指定する。
アセットパイプラインが行うこと
アセットパイプラインによって以下のことが行われる。
- キャッシュ管理
- アセットファイルの最適化
- アセットファイルのコンパイル
キャッシュ管理
アセットにハッシュを追加することで、ブラウザが最新のアセットをキャッシュする。
アセットファイルの最適化
CSS や JavaScript ファイルを圧縮し、ファイルサイズを小さくして読み込み速度を向上させる。
アセットファイルのコンパイル
複数のアセットファイルを 1 つに結合して、HTTP リクエストの数を減らし、ページの表示を高速化する。
アセットパイプラインの仕組み
アセットパイプラインは、Rails7 では、importmap-rails
、sprockets
、sprockets-rails
の gem によって実装されており、CSS は Sprockets、JavaScript は Importmap によって管理する。
Sprockets とは 従来のアセットパイプラインの仕組みで、CSS ファイルや画像の管理、最適化を行うライブラリ。app/assets
フォルダ内のファイルをコンパイルし、キャッシュ管理や圧縮も担当する。
JavaScript も Sprockets によって管理されていたが、Rails7 からは Importmap に移行された。
Importmap とは Rails 7 で JavaScript を管理するために導入されているライブラリ。JavaScriptのモジュールをサーバーから直接インポートする仕組みで、Webpackなどのビルドツールを使用せず、ブラウザ側でモジュールを解決できる。
アセットの管理
CSS
CSS はapp/assets/stylesheets
ディレクトリ以下で管理する。デフォルトでapp/assets/stylesheets/application.css
が作られており、このファイルはマニフェストファイルといって、他の CSS ファイルをまとめる役割がある。
require
の部分が他の CSS を読み込むコードで、ディレクティブという。
*= require_tree .
は現在のディレクトリ(app/assets/stylesheets)内のすべての CSS および SCSS ファイルを再帰的に読み込むディレクティブ。
*= require_self
はこのファイル自体に記述したスタイルが他のファイルよりも優先して適用するためのディレクティブ。
そしてapplication.css
自体は、デフォルトのapplication.html.erb
のhead
タグ内の以下のコードによって読み込むことができる。
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
"data-turbo-track": "reload"
については、Turbo が使われている場合は、アセットが更新されているかどうかを Turbo がチェックし、更新されていればアセットをページに読み込むようになるオプション。
JavaScript
JavaScript はapp/javascript
ディレクトリ以下で管理する。デフォルトでマニフェストファイルとしてapp/javascript/application.js
が作られており、このファイルで他の JavaScript ファイルをまとめる。
import '@hotwired/turbo-rails' import 'controllers'
自作の JavaScript ファイルを作るときは、デフォルトでは Importmap が使われているので、app/javascript/custom.js
のようにファイルを作り、config/importmap.rb
に以下のようなコードを書いてファイルを登録する。
pin "custom", to: "custom.js"
そしてこの登録したファイルをapplication.js
でインポートする。
import 'custom'
application.js
はデフォルトのapplication.html.erb
のhead
タグ内の以下のコードによって読み込むことができる。
<%= javascript_importmap_tags %>
画像
画像ファイルはapp/images
ディレクトリ以下で管理する。
画像ファイルにアクセスする場合はビューでimage_tag
を使うだけ。
開発環境と本番環境の違い
アセットパイプラインは開発環境と本番環境で動作に違いがある。
開発環境
開発環境では、アセットはリアルタイムでコンパイルされ、即座にブラウザに反映される。
キャッシュは使われず毎回アセットは再読み込みされて、常に最新の状態になる。
また CSS や JavaScript はデバッグのために、圧縮や最適化は行われず分割された状態になる。
本番環境
本番環境では、常にコンパイルされるとパフォーマンスに影響があるため、明示的にコンパイルする必要がある。
明示的にコンパイルするにはrails assets:precompile
を実行する。
このコマンドを実行すると以下のことが行われる。
- アセットファイルの結合と圧縮
- ファイル名にハッシュを追加
- プリコンパイルされたアセットの配信
- キャッシュ管理
またコンパイルされたファイルはpublic/assets
に配置される。
React のデータフェッチングライブラリである、SWR と TanStack Query(TanStack Query)の基本的なデータ取得についてまとめました。
この記事は以下の構成になっています。
- SWR
- SWR とは
- SWR を使う手順
- SWR のメリット・デメリット
- TanStack Query
- TanStack Query とは
- TanStack Query を使う手順
- TanStack Query のメリット・デメリット
- SWR と TanStack Query の違い
- データフェッチングライブラリを使わずにデータを取得する場合
データの取得について解説していますが、データの更新や複雑な機能については触れていません。
SWR
SWR とは
SWR は React のフレームワークである Next.js を開発している Vercel が提供しているデータフェッチライブラリ。
データを取得する際に、古いデータを即時に表示しながら新しいデータを非同期で取得して再表示する仕組みを提供している。
SWR という名前は Stale-While-Revalidate という HTTP キャッシュの戦略に由来している。
Stale-While-Revalidate とは、Web ページや API のデータを素早く返しながら、バックグラウンドで最新データへの更新を行う HTTP キャッシュ戦略。
SWR を使う手順
SWR は以下の手順で使う。
- パッケージをインストールする
- データを取得する関数を作る
- useSWR をインポートする
- useSWR にデータを取得する関数を渡し、data、error を受け取る
- エラーやローディングの処理を書く
例として、以下の Posts コンポーネントで、JSONPlaceholder から posts のデータを取得する処理を SWR を使って書いていく。
Posts.jsx
export const Posts = () => { return ( ul {data.map((post) => ( li key={post.id}{post.title}li ))} ul ) }
data
の部分はまだ取得していないのでエラーの状態。
1. パッケージをインストールする
SWR を使うには以下のコマンドを実行して npm のパッケージをインストールする必要がある。
npm i swr
2. データを取得する関数を作る
データ取得に使用する API や関数を別で定義する。この関数は後述の useSWR に渡す関数。
例として fetcher という関数を定義する。
const fetcher = async (url) => { const res = await fetch(url) return res.json() }
export const Posts = () => { return ( ul {data.map((post) => ( li key={post.id}{post.title}li ))} ul ) }
fetcher でデータを取得するロジックを切り出していることで、他のコンポーネントでも再利用できる。
また、ここではfetch
を使っているが、もし axios や他のロジックを使いたい時はfetcher
の中身を変えるだけで済む。
3. useSWR をインポートする
swr
から useSWR を インポートする。
import useSWR from 'swr'
useSWR はデータの取得やキャッシュ管理、エラーハンドリングなどを効率的に行うための hooks。
useSWR でデータを取得すると、自動的にキャッシュを保持し、データが変わったタイミングやフォーカスが戻った際に再取得を行って、常に最新のデータを表示することができる。
4. useSWR にデータを取得する関数を渡して、data、error、isLoading を受け取る
コンポーネントのトップレベルで、useSWR を呼び出し、第一引数に URL、 第二引数にデータ取得関数を渡して、data
、error
、isLoading
を受け取る。
第一引数の URL は第二引数のfetcher
に渡される。
import useSWR from 'swr'
const fetcher = async (url) => { const res = await fetch(url) return res.json() }
export const Posts = () => { const { data, error, isLoading } = useSWR( 'https://jsonplaceholder.typicode.com/posts', fetcher )
return ( ul {data?.map((post) => ( li key={post.id}{post.title}li ))} ul ) }
data
はリクエストが成功した時の取得されたデータが格納されるプロパティ。初期状態は undefined。
このdata
はキャッシュされるため、次に同じ URL にアクセスした場合はキャッシュからデータを返すので、2 度目以降のデータ取得が高速になる。
error
はデータの取得時にエラーが発生した場合に、エラーメッセージやエラーオブジェクトが格納される。
isLoading
はデータの取得中かどうかを示すフラグ。リクエストが開始されてからデータが返されるまでの間は true になり、データが取得できた時点で false に切り替わる。
5. エラーやローディングの処理を書く
useSWR から受け取ったerror
やisLoading
を使ってエラーやローディングの処理を書く。
import useSWR from 'swr'
const fetcher = async (url) => { const res = await fetch(url) return res.json() }
export const Posts = () => { const { data, error, isLoading } = useSWR( 'https://jsonplaceholder.typicode.com/posts', fetcher )
if (error) return divFailed to loaddiv
if (isLoading) return divLoading...div
return ( ul {data?.map((post) => ( li key={post.id}{post.title}li ))} ul ) }
SWR のメリット・デメリット
メリット
SWR のメリットは以下。
- 軽量で使いやすい
- 自動でキャッシュを管理し、データの再フェッチなどが標準で使える
- useSWR を使うだけでシンプルに書ける
デメリット
SWR のデメリットは以下。
- 状態管理やミューテーションの機能が豊富ではない
- 複雑なキャッシュ管理が必要な場合、柔軟性に欠けることがある
TanStack Query
TanStack Query とは
TanStack Query(旧 React Query)は React 用のデータフェッチライブラリ。SWR と同様に、非同期データの取得とキャッシュ管理を容易にするが、それに加えてより多くの機能を提供している。
SWR より機能が豊富な分、パッケージのサイズは大きい。
TanStack Query を使う手順
TanStack Query は以下の手順で使う。
- パッケージをインストールする
- TanStack Query を使う準備を行う
- データを取得する関数を作る
- useQuery をインポートする
- useQuery にキーとデータを取得する関数を渡して、data、isPending を受け取る
1. パッケージをインストールする
TanStack Query を使うには、npm のパッケージをインストールする必要がある。
npm i @tanstack/react-query
2. TanStack Query を使う準備を行う
TanStack Query を使うには、index.js で設定を行う必要がある。
設定は以下のように記述する。
index.js
import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
const root = ReactDOM.createRoot(document.getElementById('root')) root.render(
)Context や React Router のように、QueryClinetProvider
で囲んだコンポーネントは TanStack Query の機能を使えるようになる。
3. データを取得する関数を作る
SWR と同様に、データ取得用の関数を定義する。
const fetchPosts = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/posts') if (!response.ok) { throw new Error('Failed to fetch data') } return response.json() }
4. useQuery をインポートする
TanStack Query でデータフェッチングで行うには useQuery をインポートする。
import { useQuery } from '@tanstack/react-query'
useQuery
とは非同期データを簡単に取得・管理できるための TanStack Query の hooks。
5. useQuery に、data、error、isPending を受け取る
コンポーネントのトップレベルで useQuery を呼び出してdata
、error
、isPending
を受け取る。
useQuery を呼び出す際には引数にはqueryKey
とqueryFn
のプロパティを持つオブジェクトを渡す。
queryKey
にはキャッシュを一意に識別するためのキーを指定する必須のプロパティ。
queryFn
にはデータを取得するための関数を指定するプロパティで、Promise を返す関数である必要がある。
import { useQuery } from '@tanstack/react-query'
const fetchPosts = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/posts') if (!response.ok) { throw new Error('Failed to fetch data') } return response.json() }
export const Posts = () => {
const { data, error, isPending } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, })
return ( ul {data?.map((post) => ( li key={post.id}{post.title}li ))} ul ) }
data
は取得したデータが格納され、デフォルトは undefined。
error
はクエリ実行時に発生したエラーオブジェクトを扱うもので、デフォルトは null。
isPending
はキャッシュされたデータがなく、クエリの実行が完了していない状態を表すフラグで、ローディングの処理に使う。
5. エラーやローディングの処理を書く
import { useQuery } from '@tanstack/react-query'
const fetchPosts = async () => { const response = await fetch('https://jsonplaceholder.typicode.com/posts') if (!response.ok) { throw new Error('Failed to fetch data') } return response.json() }
export const Posts = () => {
const { data, error, isPending } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, })
if (isPending) return divLoading...div
if (error) return 'An error has occurred: ' + error.message
return ( ul {data?.map((post) => ( li key={post.id}{post.title}li ))} ul ) }
TanStack Query のメリット・デメリット
メリット
TanStack Query のメリットは以下。
- 非同期データ管理に関する幅広い機能を提供し、大規模アプリケーションに適している
- 状態更新(ミューテーション)やキャッシュの制御が詳細に設定可能
- リトライやエラー処理、データのプリフェッチなど、SWR に比べて柔軟で強力な機能がある
- ページネーションは無限スクロールなども扱える
デメリット
TanStack Query のデメリットは以下。
- SWR に比べてパッケージサイズが少し大きい
- シンプルなアプリケーションにはやや過剰で、学習コストが高い
SWR と TanStack Query の違い
SWR と TanStack Query はどちらもデータフェッチングライブラリだが、主に以下の違いがある。
機能の豊富さ
SWR はシンプルで軽量な設計に重点を置いているのに対し、TanStack Query は多機能で柔軟な操作が可能。特に、キャッシュ管理やデータのミューテーションを扱う場合、TanStack Query の方が適している。
パフォーマンス
SWR は軽量で高速な動作が期待できるが、TanStack Query はより多機能であるため、パッケージサイズが少し大きくなる。
用途
小規模なアプリケーションには、SWR がシンプルで扱いやすい。一方、複雑な状態管理や複数のデータソースを使う大規模なアプリケーションでは、TanStack Query の機能が役立つ。
データフェッチングライブラリを使わずにデータを取得する場合
SWR や TanStack Query のようなデータフェッチングライブラリを使わない場合は、React の hooks である useEffect でデータを取得する。
useEffect の場合は以下のように書く。
import { useEffect, useState } from 'react'
export const Posts = () => { const [data, setData] = useState(null) const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(true)
useEffect(() => { const fetchData = async () => { setIsLoading(true)
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts'
)
if (!response.ok) {
throw new Error('Failed to fetch data')
}
const result = await response.json()
setData(result)
} catch (err) {
setError(err)
} finally {
setIsLoading(false)
}
}
fetchData()
}, [])
if (error) return divError: {error.message}div if (isLoading) return divLoading...div
return ( ul {data?.map((post) => ( li key={post.id}{post.title}li ))} ul ) }
useEffect で書く場合は、以下のようなデメリットがある。
- キャッシュを使えない
- 再フェッチが難しい
- フェッチ、ローディング、エラーハンドリングを自分で実装する必要がある
- データを取得するロジックを使いまわせない
Rails のモデルについて基本的な内容をまとめました。
この記事は以下の構成になっています。
- モデルとは
- モデルファイルの作成方法
- Active Record
- CRUD 処理
- その他の ActiveRecord のメソッド
- バリデーション
- コールバック
- アソシエーション
- enum
- scope
- テーブル結合
- トランザクション
- 生の SQL を使う
モデルとは
モデルとは、MVC アーキテクチャの M にあたるもので、主にデータを操作やビジネスロジックを管理する役割を持つ。
ビジネスロジックとは、アプリの要件を実現するデータ処理のロジックのこと。
モデルはデータベースのテーブルに対応し、レコードの作成、読み込み、更新、削除等の処理などを行う。
モデルファイルの作成方法
モデルのファイルを作成するには以下のコマンドを実行する。
rails g model [モデル名]
rails g model User
rails g model Product name price:integer active:boolean
成功すると、以下の出力のようにモデルファイル以外にもマイグレーションやテストなど複数のファイルが作られる。
rails generate model User invoke active_record create db/migrate/20240213131600_create_users.rb create app/models/user.rb invoke test_unit create test/models/user_test.rb create test/fixtures/users.yml
app/models/user.rb
が作られたモデルファイルで、内容は以下のようになっている。
class User < ApplicationRecord end
User
クラスの定義しか記述されていないが、ApplicationRecord
クラスを継承していることによって、ApplicationRecord
クラスのメソッドを使える。
ApplicationRecord
ApplicationRecord とは全てのモデルの親クラスとして機能するクラス。全てのモデルで共通したい処理や設定はこのクラスに書く。
ApplicationRecord はapp/models/application_record.rb
で定義されており、以下の内容になっている。
class ApplicationRecord < ActiveRecord::Base primary_abstract_class end
ApplicationRecord はさらにActiveRecord::Base
を継承している。
Active Record
Active Record とは、Rails に組み込まれている ORM のことで、ActiveRecord::Base
を継承することで機能する。
ORM とは、オブジェクトリレーショナルマッピングの略で、簡単にいうと Ruby のオブジェクトを使って、SQL を書かずにデータベースを操作するというもの。
CRUD 処理
CRUD 処理は、Create、Read、Update、Delete の頭文字をとったもので、基本的なデータを操作する処理のこと。
Active Record によって自動的に CRUD 処理に関連するメソッドが作られているので、簡単に使うことができる。
Create
Create はデータベースにデータを保存する処理。
Create に関連するメソッドは主にcreate
、save
、new
がある。
create
create
はレコードを作成しデータベースにデータを保存するメソッド。保存が失敗するとfalse
を返す。
User.create(name: 'test', email: 'test@example.com')
create!
とすると保存が失敗した時にfalse
ではなく、例外を返す。
!
による違いはsave
やupdate
などでも同じ。
new
new
はモデルのインスタンスを作成するメソッドで、データの保存は行われない。
user = User.new(name: 'test', email: 'test@example.com')
また、モデルのオブジェクトの属性は以下のような記述の仕方で変更できる。
user.name = 'hoge'
new
で作成したインスタンスをデータベースに保存するにはsave
メソッドを使う。
save
save
はすでにレコードがあれば更新し、なければ作成するメソッド。
user = User.new(name: 'test', email: 'test@example.com') user.save
Read
Read はデータベースのデータを取得する処理。
Read に関連するメソッドは主にall
、first
、find
、find_by
、where
がある。
all
all
は全てのデータを取得するメソッド。
users = User.all
first
first
はデータベースに保存されている最初のデータを取得するメソッド。
first_user = User.first
find
find
は指定した id に一致するデータを取得するメソッド。
user = User.find(1)
find_by
find_by
は指定した条件に一致する最初のデータを取得するメソッド。最初のデータなので 1 つだけ。
test_user = User.find_by(name: 'test')
where
where
は条件に一致するすべてのデータを取得するメソッド。
users = User.where(name: 'test')
Update
Update
はデータベースのデータを更新する処理。
Update
に関連するメソッドは主にupdate
、update_all
がある。
update
update
はデータを更新するメソッド。
user = find(1) user.update(name: 'hoge')
update_all
update_all
は条件に一致した全てのデータを一括で更新するメソッド。
User.where(active: true).update_all(active: false)
Delete
Delete はデータベースのデータを削除する処理。
Delete に関連するメソッドは主にdestroy
、destroy_by
、destroy_all
がある。
destroy
destroy
はデータを削除するメソッド。
user = find(1) user.destroy
destroy_by
destroy_by
は指定した条件に一致するデータを削除するメソッド。
User.destroy_by(name: 'test')
destroy_all
destroy_all
は全てのデータを削除するメソッド。
User.destroy_all
その他の ActiveRecord のメソッド
order
order
は指定したカラムによってデータを並べ替えるメソッド。
:desc
を指定すると降順、:asc
を指定すると昇順になる。
latest_users = User.order(created_at: :desc)
limit
limit
は取得するデータの件数を指定するメソッド。
users = User.all.limit(10)
offset
offset
はデータの取得時に何件目から取得するかを指定するメソッド。
users = User.all.offset(10)
上記の記述で、最初のユーザー 10 件を省いて、11 件目からユーザーを取得する。
count
count
はデータの件数を数えるメソッド。
User.count
avarage
average
はデータの平均値を計算するメソッド。
Product.average('price')
sum
sum
はデータの合計を計算するメソッド。
Product.sum('price')
maximum
maximum
はデータの最大値を取得するメソッド。
Product.maximum('price')
minimum
minimum
はデータの最小値を取得するメソッド。
Product.minimum('price')
バリデーション
バリデーションはデータの保存や更新時に適切な値かどうかチェックする機能。
基本的にvalidates
を使って以下のようにデータのカラムごとに指定する。
class User < ApplicationRecord validates :name, presence: true, length: { maximum: 50 } validates :email, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP } validates :password, presence: true, length: { minimum: 8 } end
例では以下のことをチェックしている。
- 名前が空でないこと
- 名前の文字数が 50 文字以内であること
- メールアドレスが空でないこと
- メールアドレスが一意であること(同じメールアドレスが存在しないこと)
- メールアドレスの形式が正しいこと
- パスワードが空でないこと
- パスワードの文字数が 8 文字以上であること
コールバック
コールバックは、モデルオブジェクトの変更前か変更後に自動的に処理を実行するためのメソッド。
コールバックは以下のように使う。
class User < ApplicationRecord before_save :downcase_email
private
def downcase_email self.email = email.downcase end end
ここではbefore_save
というコールバックを使って、オブジェクトが保存される前にメールアドレスを小文字にするという処理を行なっている。
他にもバリデーションの前後や、オブジェクトの作成前後、レコードの作成前後、オブジェクトの削除前後に実行できるコールバックがある。
アソシエーション
アソシエーションとは、モデル同士の関係のことで、例えばユーザーごとの投稿を扱うといった時に使う。
テーブルに関しても関係を持たせる。ユーザーごとの投稿の場合は、投稿が誰のものか表すために Posts テーブルにuser_id
という外部キーを持たせる。
1 対 1
モデル同士の関係が、例えばユーザーのプロフィールといった場合、1 対 1 の関係になる。1 対 1 の関係を設定したい場合はhas_one
を使う。
プロフィールがユーザーを持っているのではなく、ユーザーがプロフィールを持っていると考えるのが普通なので、この場合は User モデルでhas_one
を以下のように記述する。
class User < ApplicationRecord has_one :profile end
そして Profile モデルには以下のように記述する。
class Profile < ApplicationRecord belongs_to :user end
belongs_to
を使うことで、どのモデルに従属しているかを設定できる。つまり外部キーを持つモデルに記述する。
1 対多
モデル同士の関係が、先ほどの例で挙げたユーザーの投稿といった場合は、1 人のユーザーが複数の投稿をもつことが普通なので、1 対多という関係になる。1 対多の関係を設定したい場合はhas_many
を使う。
class User < ApplicationRecord has_many :posts end
複数なのでモデル名は複数形にする。
class Post < ApplicationRecord belongs_to :user end
Post モデルにはbelongs_to
を設定する。これによってユーザーが持つ複数の投稿と反対に、投稿のユーザーの情報を取得できる。
多対多
モデル同士の関係が複数のデータを持つ場合、多対多の関係になる。
例えば、投稿のいいね機能では、ユーザーは複数の投稿をいいねすることができ、投稿は複数のユーザーからいいねされる。
このような多対多の関係を作るには中間テーブルとそのモデルを作成する。
中間テーブルはそれぞれのテーブルの外部キーを保存して、多対多の関係を作るためのテーブル。
いいね機能では User と Post モデルがあるという前提で、中間テーブルと Like モデルを作る。
以下のコマンドを実行。
rails g model Like user:references post:references
中間テーブルのマイグレーションファイルを作成し、マイグレーションを実行。
class CreateLikes < ActiveRecord::Migration[6.1] def change create_table :likes do |t| t.references :user, null: false, foreign_key: true t.references :post, null: false, foreign_key: true
t.timestamps
end
end end
likes テーブルでuser_id
を使って検索するとそのユーザーがどの投稿にいいねをしたかがわかり、post_id
を使って検索するとその投稿に誰がいいねしているかがわかる。
User、Post、Like それぞれのモデルで、テーブル同士の関係を設定する。
user.rb
class User < ApplicationRecord has_many :likes has_many :liked_posts, through: :likes, source: :post end
post.rb
class Post < ApplicationRecord has_many :likes has_many :liked_users, through: :likes, source: :user end
like.rb
class Like < ApplicationRecord belongs_to :user belongs_to :post end
enum
enum は状態や種別、評価などの一連の値を扱うためのもの。
具体的には、以下のような値。
- ユーザーのステータス
- アクティブ
- 停止
- 削除済み
- ブログの記事の状態
- 下書き
- 未公開
- 公開中
enum は以下のように記述する。
class User < ApplicationRecord enum status: { active: 0, banned: 1, withdrawn: 2 } end
class Article < ApplicationRecord enum status: { draft: 0, unpublished: 1, published: 3 } end
enum を使うことで、状態を数値ではなく意味のあるシンボルで管理することができ、user.active?
のような状態に応じたスコープやメソッドも自動で生成される。また内部的にはシンボルではなく数値で保存されるためデータベースの効率も良い。
enum を使わない場合は、integer か string でstatus: 0
のように管理する必要があるので、どんな状態かがわかりづらくなる。
scope
scope
は SQL のクエリを再利用するためのもの。
例えば、最新のデータを取得するというのはよく使う検索の仕方なので、以下のように書くことで使いまわせるようになる。
scope :latest, -> { order(created_at: :desc) }
Post モデルであれば、Post.latest
とすることで、created_at
の降順で並び替えられたデータを取得できる。
テーブル結合
テーブル結合は複数のテーブルを結合してデータを取得する方法。
Rails ではjoins
やincludes
などを使ってテーブル結合を行う。
以下のようにjoins
を使えば INNER JOIN と同じように、内部結合で関連があるデータのみを取得できる。
User.joins(:posts)
includes
では LEFT OUTER JOIN と同じように、外部結合で関連がない場合でも主テーブルのデータを取得できる。またincludes
は N+1 問題を防ぐ役割もある。
User.includes(:posts)
トランザクション
トランザクションはデータベースのデータを変更する複数の処理を一連の処理としてまとめることで、データの一貫性や整合性を保つ仕組みのこと。
トランザクションの処理は全て成功すれば完了とみなし、1 つでも失敗すれば全ての変更がロールバックされる。
トランザクションは以下のようにActiveRecord::Base.transaction
を使って書く。
ActiveRecord::Base.transaction do
user.save! order.save! payment.save! end
トランザクションの例では、EC サイトのような商品を購入して決済が行われる処理が挙げられる。
商品の購入と決済では、商品の在庫を減らす処理と、口座の金額を減らす処理などが必要になるので、トランザクションでまとめる必要がある。
もしトランザクションを使わずに単体で処理を行うと、どちらかの処理だけが失敗したときに、商品の購入ができているのにお金が支払われていなかったり、商品を購入できていないのに残高が減っているというような状態が起こりうる。
生の SQL を使う
ORM を使わずに、生の SQL を使うこともできる。
生の SQL を使うには、find_by_sql
やconnection.execute
などを使う。
find_by_sql
は生の SQL でデータを取得するときに使う。
users = User.find_by_sql("SELECT * FROM users WHERE status = 'active'")
connection.execute
は SQL を直接データベースに実行して結果を返す。
ActiveRecord::Base.connection.execute("DELETE FROM users WHERE status = 'inactive'")
ただし生の SQL は SQL インジェクションのような攻撃のリスクがあり、 セキュリティ上よくないので、基本的には ORM を使うべき。
どうしても生の SQL を使う場合は以下のように SQL の値の部分にプレースホルダーを使って SQL インジェクションを防ぐ。
users = User.find_by_sql(["SELECT * FROM users WHERE status = ?", 'active'])
JavaScript の非同期処理についてまとめました。
この記事は以下の構成になっています。
- 非同期処理とは
- JavaScript はシングルスレッド
- 非同期処理とブラウザの仕組み
- 非同期処理の流れ
- 非同期処理を制御する
- コールバック関数
- Promise
- async await
非同期処理のポイントは以下。
- 非同期処理は 1 つずつ処理を実行する同期通信とは異なり、他の処理の完了を待たずに実行できる処理
- 非同期処理は一時的にタスクキューに渡されてメインスレッドから切り離されるだけで、同期処理と同じようにメインスレッドで実行されるので、並列で処理が行われるわけではない
- 非同期処理は他の処理の完了を待たずに実行できるが、非同期処理の結果が必要になる場合は処理の完了を待つ必要がある
- コールバック関数も Promise も async・await も非同期処理を扱いやすくする構文であり、構文自体は非同期処理ではない
- 非同期処理そのものは、setTimeout や HTTP リクエスト、ファイルの読み書きなどが当てはまる
非同期処理とは
非同期処理とは、処理の完了を待たずに他の処理を実行できる仕組みのこと。
非同期処理の反対である同期処理はある 1 つの処理が終わるまで他の処理ができない処理のこと。
料理で例えると、同期処理は 1 つの料理を作り進めることしかできないが、非同期処理では 1 つの料理を作りながら、別の料理を作ったり、電子レンジを使ったり、洗い物を片付けたりできるということ。
JavaScript はシングルスレッド
JavaScript は 1 つのスレッド(実行コンテキスト)で 1 つの処理しか実行できないシングルスレッドである。シングルスレッドの反対に、複数のスレッドで複数の処理を実行できるマルチスレッドがある。
非同期処理は他の処理の完了を待たずに実行できるが、同時に複数の処理を行うマルチスレッドというわけではなく、同期処理と同じようにシングルスレッドで実行される。
非同期処理もシングルスレッドで行うようにするのはブラウザの仕組みによって行われる。
非同期処理とブラウザの仕組み
非同期処理の実行には以下のブラウザの仕組みが登場する。
- メインスレッド
- WebAPI
- コンテキスト
- コールスタック
- タスクキュー
- イベントループ
メインスレッド
メインスレッドはざっくり言うと、JavaScript が動作するスレッド。スレッド自体はブラウザが管理するが、その上で JavaScript エンジンがコードの実行や、UI の描画、ユーザーのイベントの処理などを行う。
JavaScript はシングルスレッドであり、処理が 1 つずつ行われるため、重い処理が行われると他の処理ができなくなる可能性がある。
例えば、データを取得する処理で時間がかかる場合に画面が描画されずフリーズしてしまうなど。
そこで他の処理の完了を待たずに処理を実行できる非同期処理を使うことで、メインスレッドが占有されることを防ぐことができる。
ただし非同期処理はメインスレッドで実行されないわけではなく、一時的にメインスレッドから切り離されて、非同期処理以外の処理が完了した後にメインスレッドに戻されて処理が行われる。
WebAPI
WebAPI はブラウザや Node.js が提供する API で、JavaScript エンジンの外部で提供される。
WebAPI の中でも、setTimeout
やfetch
などが非同期処理を提供する API で、非同期処理そのものにあたる。
コンテキスト
コンテキストは JavaScript エンジン内にある、JavaScript の関数やグローバルスコープが実行される環境。
コールスタック
コールスタックは JavaScript エンジン内にある、実行中のコンテキストの履歴を管理するデータ構造。
スタックは最後に追加されたデータが最初に取り出される後入れ先出しのデータ構造。
コールスタックはメインスレッドに存在するので、コールスタックにコンテキストが積まれていれば、メインスレッドが占有されている状態。
タスクキュー
タスクキューは JavaScript エンジン外部にある、実行待ちの非同期処理を管理するキュー。
キューは最初に追加されたデータが最初に取り出される、先入れ先出しのデータ構造。
メインスレッドが空いたときに、タスクキューからタスクが取り出されて実行される。
イベントループ
イベントループは、コールスタックを監視して、コールスタックが空になるとタスクキューからタスクを取り出し、メインスレッドで実行する機能。
イベントループは JavaScript エンジン外部にあり、これによって非同期処理がスムーズに行われる。
非同期処理の流れ
以下の setTimeout を使った簡単な例を使って、非同期処理が実行される流れを確認する。
function task1() { console.log('タスク1') // 同期的に実行される }
function task2() { setTimeout(() => { console.log('タスク2') // 1秒後に実行される非同期タスク }, 1000) }
function task3() { console.log('タスク3') // 同期的に実行される }
task1() task2() task3()
この処理は具体的に以下の流れで行われる。
- タスク 1 がコールスタックにプッシュされる
- タスク 1 がメインスレッドで実行される
- タスク 1 の実行が終わると、コールスタックから取り除かれる
- setTimeout がコールスタックにプッシュされる
- JavaScript エンジンが setTimeout のコールバック関数(タスク 2)を WebAPI に渡し、指定された秒数後にコールバック関数を実行するように設定する
- setTimeout がコールスタックから取り除かれる
- タスク 3 がコールスタックにプッシュされる
- タスク 3 がメインスレッドで実行される
- タスク 3 がコールスタックから取り除かれる
- 1 秒経過すると、setTimeout のコールバック関数(タスク 2)が完了済みのタスクとしてタスクキューに追加される
- イベントループがコールスタックの空きを確認し、タスクキューからコールバック関数(タスク 2)を取り出し、タスク 2 をコールスタックにプッシュする
- タスク 2 がメインスレッドで実行される
- タスク 2 がコールスタックから取り除かれる
このようにタスク 2 が非同期処理なので、タスク 1 の次に実行されるのではなく、一時的に WebAPI に渡され、タスク 3 が先に実行された後にタスク 2 が実行されるようになる。
これは setTimeout に 1 秒後と設定したから、タスク 3 が実行された 1 秒後にタスク 2 が実行されるのではなく、秒数を 0 にしてもタスク 3 の後にタスク 2 が実行される。
非同期処理を制御する
先ほどのコードの例では、非同期処理の実行順序がわかりづらいことや、非同期処理の結果を待って次の処理を行うことができないという問題点がある。
このような場合に非同期処理を制御するための書き方がコールバック関数、Promise、async・await である。
コールバック関数
コールバック関数とは
コールバック関数とは、引数に渡す関数のこと。
コールバック関数そのものは非同期処理とは関係なく、引数として関数を渡すことで、非同期処理の完了の後に処理を実行できる。
コールバック関数の書き方
コールバック関数は以下のように書く。
function task1(callback) { console.log('タスク1') callback() }
function task2(callback) { setTimeout(() => { console.log('タスク2') callback() }, 1000) }
function task3() { console.log('タスク3') }
task1(() => { task2(() => { task3() }) })
この処理は以下の流れで実行される。
- task1 が実行される
- コンソールにタスク 1 が表示される
- task2 が task1 のコールバック関数として渡されるので task2 が実行される
- コンソールにタスク 2 が表示される
- task3 が task2 のコールバック関数として渡されるので task3 が実行される
- コンソールにタスク 3 が表示される
コールバック関数の問題点
処理が増えるとコールバック関数をネストされてコールバック地獄という状態になるので可読性が悪くなる。またエラーが起きた時の処理も複雑になる。
非同期処理を制御する場合、コールバック関数ではなく、Promise か async・await を使うのが一般的。
Promise
Promise とは
Promise は、ES2015 で導入された非同期処理の完了または失敗を表現する JavaScript オブジェクト。Promise を使うことで、非同期処理の成功や失敗時に処理を実行できる。
Promise の書き方
Promise は以下のように書く。
function task(success) { return new Promise((resolve, reject) => { // 非同期処理 setTimeout(() => { if (success) { resolve('処理が成功しました!') } else { reject('処理が失敗しました。') } }, 1000) }) }
task(true) .then((result) => console.log(result)) .catch((error) => console.error(error))
非同期処理が成功するとresolve
が呼び出されてthen
が実行され、失敗するとreject
が呼び出されて catch
が実行される。
以下のように書くこともできるが、この場合 Promise の処理を直接書いてしまい、特定の条件に依存しているため再利用ができない。
const promise = new Promise((resolve, reject) => { // 非同期処理 const success = true // 成功/失敗を決定する条件 if (success) { resolve('処理が成功しました!') } else { reject('処理が失敗しました。') } })
promise .then((result) => console.log(result)) .catch((error) => console.error(error))
そのため、Promise を使うときは、Promise オブジェクトを返す関数を作って、引数の値で Promise の結果を変えられるようにするのが一般的。
Promise の状態
Promise は 3 つの状態がある。
- Pending - 初期状態で、処理がまだ完了していない状態
- Fullfilled - 非同期処理が成功し、
resolve
が呼び出された状態 - Rejected - 非同期処理が失敗し、
reject
が呼び出された状態
Promise チェーン
then
を繋げることで非同期処理の結果を次の処理に渡すことができる。これを Promise チェーンという。
コールバック関数の例では非同期処理を繋げると入れ子になってしまうが、Promise の場合は 以下のように then を繋げて書く。
function task1() { return new Promise((resolve) => { console.log('タスク1') resolve() // 次の処理に進むためのresolve }) }
function task2() { return new Promise((resolve) => { setTimeout(() => { console.log('タスク2') resolve() // 次の処理に進むためのresolve }, 1000) }) }
function task3() { console.log('タスク3') }
task1() .then(() => task2()) .then(() => task3())
then
に渡すコールバック関数で return すると次のthen
に渡すことができる。
Promise の具体例
API からデータを取得する例を Promise で書いてみる。
function fetchData(url) { return fetch(url).then((response) => { if (!response.ok) { throw new Error('ネットワークエラー') } return response.json() }) }
fetchData('https://api.example.com/data') .then((data) => { console.log('データ取得成功:', data) }) .catch((error) => { console.error('エラーが発生しました:', error) })
fetch
はブラウザの非同期 API の 1 つで、Promise を返すので、データを取得する関数を作って Promise オブジェクトを return するように書く。
Promise 問題点
Promise も処理が増えると、then
を繋げる必要があるため、同じような記述が増えて可読性が悪くなる。
async await
async await は ES2017 で導入された Promise をより同期的に書ける構文。
async await の書き方
async・await は以下のように書く。
function task(success) { return new Promise((resolve, reject) => { // 非同期処理 setTimeout(() => { if (success) { resolve('処理が成功しました!') } else { reject('処理が失敗しました。') } }, 1000) }) }
async function executeTask() { try { const result = await task(true) // 非同期処理を待機 console.log(result) // 成功した場合の結果を表示 } catch (error) { console.error(error) // 失敗した場合のエラーメッセージを表示 } }
executeTask()
async は関数に使用するキーワードで、async をつけた関数は Promise オブジェクト を返す非同期関数になる。
await は async 内でのみ使えるキーワードで、Promise の結果が返されるまで待機する。結果が返されると次の処理に進む。
try..catch
を使うことで、成功時の処理と失敗時の処理を分けて、エラーをキャッチできる。
Promise の場合は複数の非同期処理を書くときに then をつなげて書いたが、async・await の場合は以下のように同期的にシンプルに書ける。
function task1() { return new Promise((resolve) => { console.log('タスク1') resolve() }) }
function task2() { return new Promise((resolve) => { setTimeout(() => { console.log('タスク2') resolve() }, 1000) }) }
function task3() { console.log('タスク3') }
async function executeTasks() { await task1() // タスク1の完了を待機 await task2() // タスク2の完了を待機 task3() // タスク3を実行 }
executeTasks()
async await の具体例
Promise 同様、API からデータを取得する処理の例を async・await を使って書いてみる。
async function fetchData() { try { const response = await fetch('https://api.example.com/data') if (!response.ok) { throw new Error('ネットワークエラー') } const data = await response.json() console.log('取得したデータ:', data) } catch (error) { console.error('エラーが発生しました:', error) } }
fetchData()
非同期処理を扱いやすくするとは
Promise も async・await も非同期処理自体ではなく、非同期処理を扱いやすくするための構文である。
非同期処理を扱いやすくするとは、以下のことを意味する。
- 非同期処理の結果を他の処理で使う
- 複数の非同期処理を扱う
- 非同期処理の順番を管理する
- 成功時や失敗時の処理を書く
まとめ
非同期処理についてまとめると以下。
- 非同期処理は他の処理の完了を待たずに実行できる処理
- 同期処理と同じようにシングルスレッドで実行される
- 非同期処理は並列で同時に実行されているわけではなく、一時的にメインスレッドから切り離されて、最終的にメインスレッドに戻されて実行される
- 非同期処理自体は setTimeout などや fetch などの API で行われる
- コールバック関数、Promise、async・await は非同期処理そのものではなく、非同期処理を扱いやすくする構文
React の基本的な概要をまとめました。
この記事は以下の構成になっています。
- React とは
- フレームワークではなくライブラリ
- ページではなくコンポーネントで UI を構築する
- 状態を変更すると再レンダリングが発生する
- UI は命令的ではなく宣言的に記述する
- 実際の DOM を直接変更するのではなく仮想 DOM を変更する
- 関数型プログラミングを取り入れている
- データの流れが単方向データフローである
React とは
React とは、インタラクティブな UI を構築する JavaScript のライブラリ。
React には以下の特徴がある。
- フレームワークではなくライブラリ
- ページではなくコンポーネントで UI を構築する
- 状態を変更すると再レンダリングが発生する
- UI は命令的ではなく宣言的に記述する
- 実際の DOM を直接変更するのではなく仮想 DOM を変更する
- 関数型プログラミングを取り入れている
- データの流れが単方向データフローである
フレームワークではなくライブラリ
Vue.js や Angular と同じようにフレームワークとして扱われることが多いが、React はライブラリなので、ルーティングや状態管理を行う場合は別途でライブラリをインストールする必要がある。
フレームワークを使う場合だとそのフレームワークの構造や機能に合わせる必要があるが、ライブラリであることで他のライブラリと柔軟に組み合わせたり、新しい技術やバージョン変更に対応しやすい。
ページではなくコンポーネントで UI を構築する
React が使われる前のアプリはページごとに HTML を作り、CSS や JavaScript も別のファイルで管理していた。
しかし、React ではページではなく、コンポーネントというデータ、処理、見た目を 1 つのかたまりとしたものを組み合わせてアプリを作る。
コンポーネントは以下のように書き、データは state や props、処理は関数、見た目は JSX にあたる。
import { useState } from 'react'
export const Counter = ({ text }) => {
const [count, setCount] = useState(0)
const countUp = () => { setCount((prev) => prev + 1) }
return (
h3{count}h3
button onClick={countUp}{text}button
) }
state、props、JSX の詳細については後述。
原則コンポーネントは 1 つのファイルにつき 1 つであり、UI をコンポーネントに分けることで再利用性や可読性が高まる。
コンポーネントは 1 つのファイルに書くので、HTML、CSS、JavaScript を全て 1 つのファイルで書くということになる。
コンポーネントの書き方には関数コンポーネントとクラスコンポーネントの 2 種類あるが、現在は関数コンポーネントが主流。
JSX
JSX は JavaScript の中に HTML を書けるもので、コンポーネントの見た目を構成する。
関数コンポーネントであればreturn
以下の HTML のタグが書かれた部分が JSX。
export const Button = () => { return ( ボタン ) }
ファイルの拡張子を.jsx
とすることで、そのファイルが JSX であることを表す。
{}
を使えば JavaScript を記述できるので、 HTML の中 で動的な値を扱うこともできる。
JSX の実態は React.createElement
によって作られたオブジェクトであり、Babel のようなトランスパイルを行うツール によって変換される。
props
先ほどの Button コンポーネントは、ボタンという固定のテキストを持つ button タグを返している。
値が固定だとコンポーネントを使いまわしにくいので、再利用性を高めるために props を使う。
props は親コンポーネントが子コンポーネントに一方通行で渡す読み取り専用の値のこと。
親コンポーネントでは子コンポーネントに渡す値を指定し、子コンポーネントでは値を受け取る記述を行う。
Button コンポーネントで任意のテキストとクリックしたときの処理を props として受け取れるようにする。
export const Button = ({text, onClick}) => { return ( {text} ) }
親コンポーネントで Button コンポーネントを呼び出す際に props で値を渡す。
import { Button } from './Button'
export const Parent = () => {
const handleClick = () => { console.log('clicked') }
return ( ) }
このように props を使うことで、コンポーネントを使い回しやすくする。
state
state は親から渡される props とは異なり、コンポーネント自身が持つデータのこと。
React ではデータのことを状態ともいう。
関数コンポーネントでは state は useState という hooks を使って実装する。
import { useState } from "react"
export const Counter = () => { const [count, setCount] = useState(0)
const countUp = () => { setCount((prev) => prev + 1) }
return ( <>
{count}
+ </> ) }state によってフォームに入力された値や API から取得したデータ、UI の表示の状態などを保持することができる。
状態を変更すると再レンダリングが発生する
props、state が変更される、または親コンポーネントが変更されるとコンポーネントの再レンダリングが行われる。
再レンダリングとはコンポーネントを再描画することであり、関数コンポーネントであれば関数が再実行されることと同じである。
親コンポーネントが変更された時に、その親がもつ全ての子コンポーネントは再レンダリングされる。
React の場合、ページのリロードが発生せず、リロードによる更新が行えないので、再レンダリングによって画面を更新できる。
実際の DOM を直接変更するのではなく仮想 DOM を変更する
再レンダリングが発生すると新しい状態に基づいた仮想 DOM が生成される。
仮想 DOM とは、実際の DOM を直接操作せずに効率的に更新するために仮想的に作られた DOM のコピーのことで、その実態は JavaScript のオブジェクトである。
React では DOM は仮想 DOM を使って 以下の流れで変更される。
- state や props などの状態が変更される
- 再レンダリングが発生する
- React が新しい仮想 DOM を生成する
- 新しい仮想 DOM と変更前の仮想 DOM を比較して差分を計算する
- 差分のみを実際の DOM に反映する
仮想 DOM を使うことで、全体ではなく差分だけを変更することになるので、頻繁に DOM が変更されるアプリではパフォーマンスが良くなる。
UI は命令的ではなく宣言的に記述する
React では JSX、仮想 DOM、再レンダリングによって UI を宣言的に記述する。
宣言的というのは、UI がどう見えるかだけを書く方法。宣言的だと、データに基づいて UI が表示されるので、データが変更されたとしても UI を直接変更することなく再レンダリングと仮想 DOM の仕組みによって自動的に変更される。
UI はデータに依存するが、コードで見るとデータである state や props と、見た目である JSX で分かれているため、可読性が高い。
React が使われる前の JavaScript や jQuery を使った DOM を直接変更して作成する UI の書き方 は命令的という。命令的では、DOM を直接取得して、その DOM に対して状態や表示を変更したり、イベントを設定したりするので、コード量が増えて処理が複雑になりやすい。
Todo リストの Todo 追加機能の例で命令的 UI と宣言的 UI の違いを確認する。
Todo ListTodo List
<script>
const todos = ['Learn JavaScript', 'Learn React', 'Build a project']
function renderTodos() {
const todoList = document.getElementById('todo-list')
todoList.innerHTML = ''
todos.forEach((todo) => {
const li = document.createElement('li')
li.textContent = todo
todoList.appendChild(li)
})
}
function addTodo() {
const input = document.getElementById('todo-input')
const newTodo = input.value.trim()
if (newTodo) {
todos.push(newTodo)
input.value = ''
renderTodos()
}
}
document.getElementById('add-todo').addEventListener('click', addTodo)
renderTodos()
</script>
命令的 UI の場合は、追加ボタンの DOM を取得し、それにイベントリスナーを追加して、クリック時にイベントが発火するようにして、追加時にはフォームの値を取得し、取得した値を使って DOM を新しく生成し、生成した DOM を表示するための Todo リストの DOM を取得して、生成した DOM を Todo リストの DOM に挿入するというコードを書いている。
このように簡単な 1 つの機能を書くだけでも DOM の取得や生成を行ったり、イベントを設定したりしなければならないうえに、処理があちこちに散らばりやすいので、可読性も保守性も悪い。
一方、宣言的 UI の場合は、データと UI を分離して、何らかの処理が行われた時に UI ではなくデータを変更して、そのデータに基づいて UI を表示する。
React の場合はコンポーネントごとにファイルを分けるので、TodoApp、TodoList、TodoItem、AddTodoForm に分ける。
TodoApp.jsx
import { useState } from 'react' import TodoList from './TodoList' import AddTodoForm from './AddTodoForm'
const TodoApp = () => { const [todos, setTodos] = useState([ 'Learn JavaScript', 'Learn React', 'Build a project' ])
const addTodo = (newTodo) => { if (newTodo.trim()) { setTodos([...todos, newTodo]) } }
return (
Todo List
export default TodoApp
TodoList.jsx
import TodoItem from './TodoItem'
const TodoList = ({ todos }) => { return (
-
{todos.map((todo, index) => (
))}
export default TodoList
TodoItem.jsx
const TodoItem = ({ todo }) => { return
export default TodoItem
AddTodoForm.jsx
import { useState } from 'react'
const AddTodoForm = ({ onAddTodo }) => { const [newTodo, setNewTodo] = useState('')
const handleAddClick = () => { onAddTodo(newTodo) setNewTodo('') }
return (
export default AddTodoForm
このように基本的に JSX で UI を作り、入力された todo のデータは state で管理し、そのデータを props として JSX に渡すことで UI を表示している。イベントに関しても DOM を取得する処理を書かずに JSX にそのまま処理を指定できるのでわかりやすく書くことができる。
宣言的に書くことで開発者は状態の管理に集中できて、UI の変更を考える必要がなくなる。
関数型プログラミングを取り入れている
React は関数型プログラミングを取り入れている。
関数型プログラミングとは、処理を関数に隠蔽してコードを整理するプログラミング手法。関数型の反対に当たるのは手続き型、もしくは命令型という、手順通りに命令を記述して処理を実行するプログラミング手法。
React は完全に関数型ではないが、関数型と手続き型が混在する。
関数型では純粋関数、イミュータブルという特徴がある。
純粋関数
純粋関数とは、入力が同じなら常に同じ出力を返す関数のこと。引数が同じなら戻り値が同じになるということである。
反対に純粋ではない関数とは、外部の状態によって実行するたびに戻り値が変わったり、外部の状態を変更してしまうような副作用を持った関数。
React の関数コンポーネントは、純粋関数に基づいて、与えられた props によって JSX を返し、副作用を持たないことが理想である。
純粋関数であることで、外部のコードに影響がないため、動作が予測しやすく、テストが書きやすく、再利用性が高いというメリットがある。
イミュータブル
イミュータブルは不変という意味であり、一度作られたデータが変更できないことである。
プリミティブ型の値は元々イミュータブルな値であるが、配列やオブジェクトは後から変更できるミュータブルな値である。
ミュータブルな値を扱うと状態の変更が予測しづらくなるため、関数型ではオブジェクトや配列は新しくコピーを作成して変更する。
データの流れが単方向データフローである
React では props が親から子への一方通行のデータの流れになっている。このデータの流れを単方向データフローという。
単方向データフローのメリットは、データの流れが一貫するため、状態の変更がわかりやすくなること、コンポーネントが疎結合になって再利用性が増すこと、テストの容易さがある。
もし親から子に渡された props を子で変更したい場合は、親から子に props として変更する関数を渡しておき、その関数を子で実行することで親に props の変更を通知する。
単方向データフローの反対は、Vue.js などで採用されている双方向データフローである。双方向データフローでは、UI と状態が結びつき、UI を変更すれば状態が変更され、状態を変更すれば UI も変更されるという双方向の流れになっている。
双方向データフローでは UI と状態の同期が自動的に行われるので記述が減るというメリットがあるが、データの流れが双方向になるため、状態管理が複雑になり予測しづらいコードになる。
React で SPA を作るときに使うルーティングライブラリである React-Router についてまとめました。
この記事は以下の構成になっています。
- React Router とは
- SPA のルーティングの仕組み
- React Router を使う手順
- その他
- リンクで画面遷移を行う
- 何らかの処理の後に画面遷移を行う
- 前のページに戻る
- 動的ルーティング
- クエリパラメータ
- 存在しない URL にアクセスされた場合
React Router とは
React Router とは、React で SPA を作成する時にページを切り替えるために必要になるルーティングを実装するためのライブラリ。
SPA のルーティングの仕組み
SPA でのルーティングとは、入力された URL によって表示するコンポーネントを JavaScript で切り替えること。
SPA ではページではなくコンポーネントで構成されるので、表示するコンポーネントを切り替えることでページを切り替えるように見せる。
JavaScript で切り替えるので、画面のリロードが行われずに画面が切り替わる。
React Router を使う手順
React Router は基本的に以下の手順で使う。
- パッケージをインストール
- BrowserRouter を設定する
- パスとコンポーネントを紐づける
前提として、React Router のバージョンは 6 で解説する。バージョンによって書き方が変わることもあるので注意。
1. パッケージをインストール
React Router を使うにはreact-router-dom
のパッケージが必要なので、インストールする。
npm i react-router-dom
2. BrowserRouter で囲む
React Router をインストールできたので、次に React アプリでルーティングを使えるようにするためにBrowserRouter
を設定する。
アプリ全体に適用させるので基本的にindex.js
で以下のように書く。
import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import { BrowserRouter } from 'react-router-dom'
const root = ReactDOM.createRoot(document.getElementById('root')) root.render( )
3. パスとコンポーネントを紐づける
アプリ全体でルーティングを使えるようになったので、Routes
とRoute
を使って、URL とコンポーネントを紐づけて具体的なルーティングを設定する。
まず表示したいページコンポーネントを作る。例として Home と About コンポーネントを作る。
Home.jsx
export const Home = () => { return h1Homeh1 }
About.jsx
export const About = () => { return h1Abouth1 }
次に具体的なルーティングを React Router が提供するRoutes
とRoute
を使って App.js に作成する。
コードは以下のようになる。
import { Route, Routes } from 'react-router-dom' import { Home } from './pages/Home' import { About } from './pages/About'
function App() { return ( Routes Route path='/' element={Home } Route path='/about' element={About } Routes ) }
export default App
Routes
はRoute
をまとめるためのもので、Route
でパスとそのパスで表示したいコンポーネントを対応させる。
こうすることで、/
では Home コンポーネントが、/about
では About コンポーネントが表示される。
その他
リンクで画面遷移を行う
HTML の a タグでリンクを実装すると画面が更新されてしまうため、React Router のLink
コンポーネントを使う。
以下のように Home コンポーネントから About コンポーネントに遷移するためのリンクを作成する。
import { Link } from 'react-router-dom'
export const Home = () => { return (
h1Homeh1
Link to='/about'AboutLink
) }
Link
を使うことで、リロードせずに画面遷移ができる。
何らかの処理の後に画面遷移を行う
何らかの処理の後に画面遷移を行う場合は React Router が提供するuseNavigate
を使う。
import { useNavigate } from 'react-router-dom'
export const Home = () => { const navigate = useNavigate()
const handleClick = () => { console.log('clicked') navigate('/about') }
return (
h1Homeh1
button onClick={handleClick}ボタンbutton
) }
前のページに戻る
前のページに戻りたい場合はnavigate
に-1
を渡す。
import { useNavigate } from 'react-router-dom'
export const Home = () => { const navigate = useNavigate()
const handleClick = () => { console.log('clicked') navigate(-1) }
return (
h1Homeh1
button onClick={handleClick}ボタンbutton
) }
動的ルーティング
商品やユーザーごとに表示する内容が違う場合、動的ルーティングというものを使う。
動的ルーティングは URL のパラメータを使って異なるコンテンツを表示するためのルーティング。
例として商品のリストから商品を詳細ページを表示するルーティングとコンポーネントを作成する。
以下のコードで、Home コンポーネントで JSONPlaceHolder から product のデータを取得し、商品リンクの一覧を表示する。商品データの取得の処理に関しての説明は省略。
import axios from 'axios' import { useEffect, useState } from 'react' import { Link } from 'react-router-dom'
export const Home = () => { const [products, setProducts] = useState([])
useEffect(() => { const fetchProducts = async () => { const response = await axios.get( 'https://jsonplaceholder.typicode.com/posts' ) setProducts(response.data) }
fetchProducts()
}, [])
return (
h1Homeh1
ul
{products.map((product) => (
li key={product.id}
Link to={`/product/${product.id}`}{product.title}Link
li
))}
ul
Link to='/about'AboutLink
) }
Link
で商品の id ごとにパスを変えている。
次に App.js で 商品の詳細ページを表示するルーティングを追加する。
import { Route, Routes } from 'react-router-dom' import { Home } from './pages/Home' import { About } from './pages/About' import { ProductDetail } from './pages/ProductDetail'
function App() { return ( <Route path='/' element={} /> <Route path='/about' element={} /> <Route path='/product/:id' element={} /> ) }
export default App
動的ルーティングでは動的に変更する部分、ここではid
に:
をつけて/product/:id
のようにする。
最後に商品の詳細を表示するページコンポーネントを ProductDetail として作成する。
import { useEffect, useState } from 'react' import { useParams } from 'react-router-dom' import axios from 'axios'
export const ProductDetail = () => { const { id } = useParams() const [product, setProduct] = useState({})
useEffect(() => {
const fetchProduct = async () => {
const response = await axios.get(
https://jsonplaceholder.typicode.com/posts/${id}
)
setProduct(response.data)
}
fetchProduct()
}, [id])
return ( div h1{product.title}h1 p{product.body}p div ) }
商品の詳細データを id を使って取得する必要があるので、React Router が提供するuseParams
を使って id を取得する。
取得した id を使って JSONPlaceHolder から商品データを取得して表示している。
クエリパラメータ
クエリパラメータを使いたい場合は React Router が提供するuseSearchParams
を使う。
例として、クエリパラメータを表示する Search コンポーネントを作成する。
import { useSearchParams } from 'react-router-dom'
export const Search = () => { const [searchParams] = useSearchParams() const query = searchParams.get('query')
return (
h1Searchh1
p{query}p
) }
searchParams.get('query')
とすることで、クエリパラメータのキーがquery
の値を取得している。
次に Search コンポーネントを表示するルーティングを追加する。
import { Route, Routes } from 'react-router-dom' import { Home } from './pages/Home' import { About } from './pages/About' import { Search } from './pages/Search'
function App() { return ( Routes Route path='/' element={Home } Route path='/search' element={Search } Route path='/about' element={About } Routes ) }
export default App
このようにすると、例えば/search?query=exmaple
というパスにアクセスした場合に、Search コンポーネントでexample
という文字列が表示される。
存在しない URL にアクセスされた場合
ページが存在しない URL にアクセスされたときは、通常 404 のようなエラーを表示する。
このような場合はエラー時に表示するコンポーネントを作り、ルーティングを追加する。
エラー時に表示するコンポーネントは NotFound とする。
export const NotFound = () => { return h1NotFounth1 }
import { Route, Routes } from 'react-router-dom' import { Home } from './pages/Home' import { About } from './pages/About' import { Search } from './pages/Search' import { ProductDetail } from './pages/ProductDetail' import { NotFound } from './pages/NotFound'
function App() { return ( Routes Route path='/' element={Home } Route path='/search' element={Search } Route path='/about' element={About } Route path='/product/:id' element={ProductDetail } Route path='*' element={NotFound } Routes ) }
export default App
Route
のpath
を*
とすることで、どのパスにも合致しない場合のコンポーネントを表示できる。
React で TypeScript を使うときの型定義についてまとめました。
この記事は以下の構成になっています。
- React 特有の型の指定
- コンポーネントの型
- props の型
- useState の型
- イベントの型
- どこまで型定義するか
React 特有の型の指定
React では基本的に、コンポーネント、props、useState、イベント の型を定義する。
コンポーネントの型
コンポーネントの型はJSX.Element
、FC
、VFC
で定義できる。
JSX.Element
コンポーネントの戻り値に型を指定しない場合、JSX.Element
型を返すコンポーネントになる。
JSX.Element
は JSX を返す関数であることを示す型。
型推論によって省略できるが、明示する場合は以下のように書く。
const User = ({ name, age }): JSX.Element => { return (
p{name}p
p{age}p
) }
コンポーネントの型は基本的に明示しないJSX.Element
を使えば OK なので、以下のように書く。
const User = ({ name, age }) => { return (
p{name}p
p{age}p
) }
FC と VFC
FC
は Function Component の略で、暗黙的に children を受け取る型。VFC
は Void Function Component の略で、children を受け取らない型。
FC
とVFC
はインポートして使う必要がある。
React 17 以前では children を受け取るコンポーネントにはFC
、受け取らないコンポーネントにはVFC
が使われていたが、18 以降は、FC
で children の型も明示的に指定する必要があり、VFC
は非推奨になった。
React 17 以前
import React from 'react'
const User: React.FC = ({ name, age }) => { return (
p{name}p
p{age}p
{children}
) }
import React from 'react'
const User: React.VFC = ({ name, age }) => { return (
p{name}p
p{age}p
) }
React 18 以降
import React from 'react'
const User: React.FC = ({ name, age, children }) => { return (
p{name}p
p{age}p
{children}
) }
props の型
props の型定義は型エイリアスかインターフェースを使う。
型エイリアスの場合
type User = { name: string age: number greet: () => void }
const User = ({ name, age, greet }: User) => { return (
p{name}p
p{age}p
) }
インターフェースの場合
interface User { name: string age: number greet: () => void }
const User = ({ name, age, greet }: User) => { return (
p{name}p
p{age}p
) }
FC
の場合の props の型定義は以下のようにジェネリクスを使って書く。
import React from 'react'
type User = { name: string age: number greet: () => void }
const User: React.FC = ({ name, age, greet }) => { return (
p{name}p
p{age}p
) }
children の型
children の型定義は props の型定義の中でReact.ReactNode
を使う。
import React from 'react'
type User = { children: React.ReactNode }
const User = ({ children }: User) => { return {children} }
useState の型
useState の型を指定する場合は以下のようにジェネリクスを使って書く。
もし初期値から型が明確であれば、型推論を使えるので型定義は必要ない。
プリミティブ型
const [text, setText] = useState('') const [count, setCount] = useState(0) const [count, setCount] = useState(true)
配列
const [numbers, setNumbers] = useState<number[]>([])
オブジェクト
type User = { name: string age: number }
const [user, setUser] = useState({ name: '', age: 0 })
型を指定した場合、プロパティに初期値を設定しないとエラーになるので注意。
配列にオブジェクトを格納する
type User = { name: string age: number }
const [users, setUsers] = useState<User[]>([])
null を含む場合
const [text, setText] = useState<string | null>(null)
イベントの型
React でよく使う onChange や onClick 等のイベントハンドラで、イベントオブジェクトを扱う場合は以下のように型を定義する。
import React, { useState } from 'react'
const ExampleComponent = () => { const [text, setText] = useState('')
const handleChange = (event: React.ChangeEvent) => { setText(event.target.value) }
const handleClick = (event: React.MouseEvent) => { console.log('clicked') }
return ( div input type='text' value={text} onChange={handleChange} button onClick={handleClick}クリックbutton div ) }
export default ExampleComponent
イベントの型はイベントごとに専用のものが用意されている。
例のReact.ChangeEvent<HTMLInputElement>
では、React が提供するReact.ChangeEvent
を使って change イベントを扱う型を使用し、そのジェネリクスでどの HTML 要素に関連しているかを示している。ここでは input を使っているのでHTMLInputElement
としている。
どこまで型定義するか
全ての変数や関数に型を定義するのはコストになり現実的ではないので、単純な値では型定義を省略して型推論を使い、外部から受け取る props や関数の引数など、どんな値を受け取るか明確にしたい場合は型を指定する。