SSH接続まとめ

SSH についてまとめました。

この記事は以下の構成になっています。

  • SSH とは
  • SSH で接続する流れ
  • SSH の認証方法

SSH とは

SSH とは、Secure Shell の略で、ネットワークを通じて他のコンピュータに安全に接続するための仕組み。

主にサーバーに対して遠隔操作を行う時に使われる。

SSH の前は telnet というコマンドが使われていたが、telnet はやりとりする情報をそのまま送信するのに対して、SSH は暗号化して送信するのでより安全である。

SSH で接続する流れ

SSH で接続するには以下の流れを行う。

  1. ターミナルで ssh コマンドを実行する
  2. 認証情報を入力する
  3. リモートサーバーへのアクセスが開始される

1. ターミナルで ssh コマンドを実行する

SSH で接続するには、以下のようにターミナルで ssh コマンドを実行する。

ssh ユーザー名@サーバーアドレス

2. 認証情報を入力する

接続先がパスワード認証の場合、リモートサーバーのユーザーアカウントのパスワードを求められるので、パスワードの入力を行う。

公開鍵認証の場合は、事前に設定した秘密鍵ファイルで認証が行われる。

認証については「SSH の認証方法」で後述。

3. リモートサーバーへのアクセスが開始される

認証が成功すると、リモートサーバーに接続され、コマンドを実行できる状態になる。具体的にはリモートサーバーのシェルにアクセスできる状態である。

SSH 接続を通して、ファイルの編集やシステムの管理などを行うことができる。

SSH の認証方法

SSH での認証方法は主にパスワードと公開鍵・秘密鍵の 2 つの方法がある。

パスワード認証

パスワード認証はその名の通り、パスワードで認証を行う方法。

先ほどの「SSH で接続する流れ」はパスワード認証にあたる。

ただしパスワードはブルートフォース攻撃や盗聴のリスクがあり、セキュリティ的によくない方法なので、公開鍵認証が推奨される。

公開鍵認証

公開鍵認証は公開鍵と秘密鍵のペアを使って認証を行う方法で、パスワードより安全である。

サーバーに公開鍵を登録し、接続時にローカルに保存された秘密鍵を使って認証する仕組み。

SSH 接続だけでなく、SSL/TLSVPN などでも使われる。

公開鍵認証で SSH 接続する方法はこちらの記事で解説。

コンピュータのデータの単位とn進数まとめ

コンピュータのデータの単位と、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のdeviseの基本的な使い方まとめ

Rails の認証機能を実装するための gem である devise のインストール方法、使い方をまとめました。

この記事は以下の構成になっています。

  • devise とは
  • devise で作れる機能
  • devise を使う手順
    1. gem をインストールする
    2. devise の設定ファイルをインストールする
    3. devise の認証モデルを作成する
    4. マイグレーションを実行する
  • その他
    • ビューをカスタマイズする
    • コントローラをカスタマイズする
    • サインアップ、サインイン、サインアウト後のリダイレクトの 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 ではこれらの機能をregisterabledatabase_authenticatableといったモジュールという単位で管理しており、必要になる機能だけを有効化して使うことができる。

devise を使う手順

devise は以下の手順で使う。

  1. gem をインストールする
  2. devise の設定ファイルをインストールする
  3. devise の認証モデルを作成する
  4. マイグレーションを実行する

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:

  1. 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. *

  2. 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 *

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

     * Not required for API-only Applications *

  4. 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
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  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|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_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
  # before_action :configure_sign_in_params, only: [:create]

  # GET /resource/sign_in
  # def new
  #   super
  # end

  # POST /resource/sign_in
  # def create
  #   super
  # end

  # DELETE /resource/sign_out
  # def destroy
  #   super
  # end

  # protected

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_sign_in_params
  #   devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
  # end
end

このように Devise のコントローラを継承してコントローラを作成している。

コントローラファイルを作成することでカスタマイズすることができる。

サインアップ、サインイン、サインアウト後のリダイレクトの URL を変更する

サインアップ、サインイン、サインアウトそれぞれのリダイレクトの URL を変更するには以下の手順を行う。

  1. コントローラを作成する
  2. メソッドをオーバーライド
  3. ルーティングを設定する

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 を登録する

デフォルトではメールアドレスとパスワードで登録されるので、名前も登録したい場合は以下の手順を行う。

  1. データベースに name を追加
  2. ストロングパラメータの設定

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. モデルを作成
  2. モデルの設定
  3. マイグレーションファイルの作成と実行
  4. ルーティングの設定
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

Rails7のアセットパイプラインまとめ

Rails のアセットパイプラインについてまとめました。

この記事は以下の構成になっています。

  • アセットパイプラインとは
  • アセットパイプラインが行うこと
  • アセットパイプラインの仕組み
  • アセットの管理
  • 開発環境と本番環境での違い

前提として Rails7 のアセットパイプラインについて解説しています。

アセットパイプラインとは

アセットパイプラインは RailsCSSJavaScript、画像などのアセットファイルを圧縮したり、まとめたりして効率的に管理するための仕組み。

Rails7 ではデフォルトで導入されており、無効にする場合はrails newを実行するときに--skip-asset-pipelineを指定する。

アセットパイプラインが行うこと

アセットパイプラインによって以下のことが行われる。

  • キャッシュ管理
  • アセットファイルの最適化
  • アセットファイルのコンパイル

キャッシュ管理

アセットにハッシュを追加することで、ブラウザが最新のアセットをキャッシュする。

アセットファイルの最適化

CSSJavaScript ファイルを圧縮し、ファイルサイズを小さくして読み込み速度を向上させる。

アセットファイルのコンパイル

複数のアセットファイルを 1 つに結合して、HTTP リクエストの数を減らし、ページの表示を高速化する。

アセットパイプラインの仕組み

アセットパイプラインは、Rails7 では、importmap-railssprocketssprockets-railsの gem によって実装されており、CSS は Sprockets、JavaScript は Importmap によって管理する。

Sprockets とは 従来のアセットパイプラインの仕組みで、CSS ファイルや画像の管理、最適化を行うライブラリ。app/assetsフォルダ内のファイルをコンパイルし、キャッシュ管理や圧縮も担当する。

JavaScript も Sprockets によって管理されていたが、Rails7 からは Importmap に移行された。

Importmap とは Rails 7 で JavaScript を管理するために導入されているライブラリ。JavaScriptのモジュールをサーバーから直接インポートする仕組みで、Webpackなどのビルドツールを使用せず、ブラウザ側でモジュールを解決できる。

アセットの管理

CSS

CSSapp/assets/stylesheetsディレクトリ以下で管理する。デフォルトでapp/assets/stylesheets/application.cssが作られており、このファイルはマニフェストファイルといって、他の CSS ファイルをまとめる役割がある。

/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */

requireの部分が他の CSS を読み込むコードで、ディレクティブという。

*= require_tree .は現在のディレクトリ(app/assets/stylesheets)内のすべての CSS および SCSS ファイルを再帰的に読み込むディレクティブ。

*= require_selfはこのファイル自体に記述したスタイルが他のファイルよりも優先して適用するためのディレクティブ。

そしてapplication.css自体は、デフォルトのapplication.html.erbheadタグ内の以下のコードによって読み込むことができる。

<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

"data-turbo-track": "reload"については、Turbo が使われている場合は、アセットが更新されているかどうかを Turbo がチェックし、更新されていればアセットをページに読み込むようになるオプション。

JavaScript

JavaScriptapp/javascriptディレクトリ以下で管理する。デフォルトでマニフェストファイルとしてapp/javascript/application.jsが作られており、このファイルで他の JavaScript ファイルをまとめる。

// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
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.erbheadタグ内の以下のコードによって読み込むことができる。

<%= javascript_importmap_tags %>

画像

画像ファイルはapp/imagesディレクトリ以下で管理する。

画像ファイルにアクセスする場合はビューでimage_tagを使うだけ。

開発環境と本番環境の違い

アセットパイプラインは開発環境と本番環境で動作に違いがある。

開発環境

開発環境では、アセットはリアルタイムでコンパイルされ、即座にブラウザに反映される。

キャッシュは使われず毎回アセットは再読み込みされて、常に最新の状態になる。

また CSSJavaScriptデバッグのために、圧縮や最適化は行われず分割された状態になる。

本番環境

本番環境では、常にコンパイルされるとパフォーマンスに影響があるため、明示的にコンパイルする必要がある。

明示的にコンパイルするにはrails assets:precompileを実行する。

このコマンドを実行すると以下のことが行われる。

  • アセットファイルの結合と圧縮
  • ファイル名にハッシュを追加
  • プリコンパイルされたアセットの配信
  • キャッシュ管理

またコンパイルされたファイルはpublic/assetsに配置される。

Reactのデータフェッチングライブラリ SWRとTanstack Queryの基礎まとめ

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 は以下の手順で使う。

  1. パッケージをインストールする
  2. データを取得する関数を作る
  3. useSWR をインポートする
  4. useSWR にデータを取得する関数を渡し、data、error を受け取る
  5. エラーやローディングの処理を書く

例として、以下の 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、 第二引数にデータ取得関数を渡して、dataerrorisLoadingを受け取る。

第一引数の 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 から受け取ったerrorisLoadingを使ってエラーやローディングの処理を書く。

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 <div>Failed to load</div>

  // ローディング中の表示
  if (isLoading) return <div>Loading...</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 は以下の手順で使う。

  1. パッケージをインストールする
  2. TanStack Query を使う準備を行う
  3. データを取得する関数を作る
  4. useQuery をインポートする
  5. 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'

// QueryClinetインスタンスを作成
const queryClient = new QueryClient()

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
  // アプリ全体でTanStack Queryの機能を使えるようにする
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>
)

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 を呼び出してdataerrorisPendingを受け取る。

useQuery を呼び出す際には引数にはqueryKeyqueryFnのプロパティを持つオブジェクトを渡す。

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 = () => {
  // useQueryでデータを取得
  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 = () => {
  // useQueryでデータを取得
  const { data, error, isPending } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  if (isPending) return <div>Loading...</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で実行
  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 <div>Error: {error.message}</div>
  if (isLoading) return <div>Loading...</div>

  return (
    <ul>
      {data?.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

useEffect で書く場合は、以下のようなデメリットがある。

  • キャッシュを使えない
  • 再フェッチが難しい
  • フェッチ、ローディング、エラーハンドリングを自分で実装する必要がある
  • データを取得するロジックを使いまわせない

Rails モデルまとめ

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 に関連するメソッドは主にcreatesavenewがある。

create

createはレコードを作成しデータベースにデータを保存するメソッド。保存が失敗するとfalseを返す。

User.create(name: 'test', email: 'test@example.com')

create!とすると保存が失敗した時にfalseではなく、例外を返す。

!による違いはsaveupdateなどでも同じ。

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 に関連するメソッドは主にallfirstfindfind_bywhereがある。

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に関連するメソッドは主にupdateupdate_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 に関連するメソッドは主にdestroydestroy_bydestroy_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

scopeSQL のクエリを再利用するためのもの。

例えば、最新のデータを取得するというのはよく使う検索の仕方なので、以下のように書くことで使いまわせるようになる。

scope :latest, -> { order(created_at: :desc) }

Post モデルであれば、Post.latestとすることで、created_atの降順で並び替えられたデータを取得できる。

テーブル結合

テーブル結合は複数のテーブルを結合してデータを取得する方法。

Rails ではjoinsincludesなどを使ってテーブル結合を行う。

以下のように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_sqlconnection.executeなどを使う。

find_by_sqlは生の SQL でデータを取得するときに使う。

users = User.find_by_sql("SELECT * FROM users WHERE status = 'active'")

connection.executeSQL を直接データベースに実行して結果を返す。

ActiveRecord::Base.connection.execute("DELETE FROM users WHERE status = 'inactive'")

ただし生の SQLSQL インジェクションのような攻撃のリスクがあり、 セキュリティ上よくないので、基本的には ORM を使うべき。

どうしても生の SQL を使う場合は以下のように SQL の値の部分にプレースホルダーを使って SQL インジェクションを防ぐ。

users = User.find_by_sql(["SELECT * FROM users WHERE status = ?", 'active'])

JavaScript 非同期処理まとめ

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 の中でも、setTimeoutfetchなどが非同期処理を提供する 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 がコールスタックにプッシュされる
  2. タスク 1 がメインスレッドで実行される
  3. タスク 1 の実行が終わると、コールスタックから取り除かれる
  4. setTimeout がコールスタックにプッシュされる
  5. JavaScript エンジンが setTimeout のコールバック関数(タスク 2)を WebAPI に渡し、指定された秒数後にコールバック関数を実行するように設定する
  6. setTimeout がコールスタックから取り除かれる
  7. タスク 3 がコールスタックにプッシュされる
  8. タスク 3 がメインスレッドで実行される
  9. タスク 3 がコールスタックから取り除かれる
  10. 1 秒経過すると、setTimeout のコールバック関数(タスク 2)が完了済みのタスクとしてタスクキューに追加される
  11. イベントループがコールスタックの空きを確認し、タスクキューからコールバック関数(タスク 2)を取り出し、タスク 2 をコールスタックにプッシュする
  12. タスク 2 がメインスレッドで実行される
  13. タスク 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()
  })
})

この処理は以下の流れで実行される。

  1. task1 が実行される
  2. コンソールにタスク 1 が表示される
  3. task2 が task1 のコールバック関数として渡されるので task2 が実行される
  4. コンソールにタスク 2 が表示される
  5. task3 が task2 のコールバック関数として渡されるので task3 が実行される
  6. コンソールにタスク 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 の基本的な概要をまとめました。

この記事は以下の構成になっています。

React とは

React とは、インタラクティブな UI を構築する JavaScript のライブラリ。

React には以下の特徴がある。

フレームワークではなくライブラリ

Vue.js や Angular と同じようにフレームワークとして扱われることが多いが、React はライブラリなので、ルーティングや状態管理を行う場合は別途でライブラリをインストールする必要がある。

フレームワークを使う場合だとそのフレームワークの構造や機能に合わせる必要があるが、ライブラリであることで他のライブラリと柔軟に組み合わせたり、新しい技術やバージョン変更に対応しやすい。

ページではなくコンポーネントで UI を構築する

React が使われる前のアプリはページごとに HTML を作り、CSSJavaScript も別のファイルで管理していた。

しかし、React ではページではなく、コンポーネントというデータ、処理、見た目を 1 つのかたまりとしたものを組み合わせてアプリを作る。

コンポーネントは以下のように書き、データは state や props、処理は関数、見た目は JSX にあたる。

import { useState } from 'react'

// コンポーネント
export const Counter = ({ text }) => {
  // state
  const [count, setCount] = useState(0)

  // 関数
  const countUp = () => {
    setCount((prev) => prev + 1)
  }

  // JSX
  return (
    <>
      <h3>{count}</h3>
      <button onClick={countUp}>{text}</button>
    </>
  )
}

state、props、JSX の詳細については後述。

原則コンポーネントは 1 つのファイルにつき 1 つであり、UI をコンポーネントに分けることで再利用性や可読性が高まる。

コンポーネントは 1 つのファイルに書くので、HTML、CSSJavaScript を全て 1 つのファイルで書くということになる。

コンポーネントの書き方には関数コンポーネントとクラスコンポーネントの 2 種類あるが、現在は関数コンポーネントが主流。

JSX

JSX は JavaScript の中に HTML を書けるもので、コンポーネントの見た目を構成する。

関数コンポーネントであればreturn以下の HTML のタグが書かれた部分が JSX。

export const Button = () => {
  return (
    <button>ボタン</button>
  )
}

ファイルの拡張子を.jsxとすることで、そのファイルが JSX であることを表す。

{}を使えば JavaScript を記述できるので、 HTML の中 で動的な値を扱うこともできる。

JSX の実態は React.createElement によって作られたオブジェクトであり、Babel のようなトランスパイルを行うツール によって変換される。

props

先ほどの Button コンポーネントは、ボタンという固定のテキストを持つ button タグを返している。

値が固定だとコンポーネントを使いまわしにくいので、再利用性を高めるために props を使う。

props は親コンポーネントが子コンポーネントに一方通行で渡す読み取り専用の値のこと。

コンポーネントでは子コンポーネントに渡す値を指定し、子コンポーネントでは値を受け取る記述を行う。

Button コンポーネントで任意のテキストとクリックしたときの処理を props として受け取れるようにする。

export const Button = ({text, onClick}) => {
  return (
    <button onClick={onClick}>{text}</button>
  )
}

コンポーネントで Button コンポーネントを呼び出す際に props で値を渡す。

import { Button } from './Button'

export const Parent = () => {

  const handleClick = () => {
    console.log('clicked')
  }

  return (
    <Button text='追加' onClick={handleClick} />
    <Button text='削除' onClick={handleClick} />
  )
}

このように 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 (
    <>
      <h3>{count}</h3>
      <button onClick={countUp}>+</button>
    </>
  )
}

state によってフォームに入力された値や API から取得したデータ、UI の表示の状態などを保持することができる。

状態を変更すると再レンダリングが発生する

props、state が変更される、または親コンポーネントが変更されるとコンポーネントの再レンダリングが行われる。

レンダリングとはコンポーネントを再描画することであり、関数コンポーネントであれば関数が再実行されることと同じである。

コンポーネントが変更された時に、その親がもつ全ての子コンポーネントは再レンダリングされる。

React の場合、ページのリロードが発生せず、リロードによる更新が行えないので、再レンダリングによって画面を更新できる。

実際の DOM を直接変更するのではなく仮想 DOM を変更する

レンダリングが発生すると新しい状態に基づいた仮想 DOM が生成される。

仮想 DOM とは、実際の DOM を直接操作せずに効率的に更新するために仮想的に作られた DOM のコピーのことで、その実態は JavaScript のオブジェクトである。

React では DOM は仮想 DOM を使って 以下の流れで変更される。

  1. state や props などの状態が変更される
  2. レンダリングが発生する
  3. React が新しい仮想 DOM を生成する
  4. 新しい仮想 DOM と変更前の仮想 DOM を比較して差分を計算する
  5. 差分のみを実際の DOM に反映する

仮想 DOM を使うことで、全体ではなく差分だけを変更することになるので、頻繁に DOM が変更されるアプリではパフォーマンスが良くなる。

UI は命令的ではなく宣言的に記述する

React では JSX、仮想 DOM、再レンダリングによって UI を宣言的に記述する。

宣言的というのは、UI がどう見えるかだけを書く方法。宣言的だと、データに基づいて UI が表示されるので、データが変更されたとしても UI を直接変更することなく再レンダリングと仮想 DOM の仕組みによって自動的に変更される。

UI はデータに依存するが、コードで見るとデータである state や props と、見た目である JSX で分かれているため、可読性が高い。

React が使われる前の JavaScriptjQuery を使った DOM を直接変更して作成する UI の書き方 は命令的という。命令的では、DOM を直接取得して、その DOM に対して状態や表示を変更したり、イベントを設定したりするので、コード量が増えて処理が複雑になりやすい。

Todo リストの Todo 追加機能の例で命令的 UI と宣言的 UI の違いを確認する。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Todo List</title>
  </head>
  <body>
    <h1>Todo List</h1>
    <ul id="todo-list"></ul>
    <input type="text" id="todo-input" placeholder="todoを追加する" />
    <button id="add-todo">追加</button>

    <script>
      // Todoデータ
      const todos = ['Learn JavaScript', 'Learn React', 'Build a project']

      // Todoリストを表示する関数
      function renderTodos() {
        const todoList = document.getElementById('todo-list')
        todoList.innerHTML = '' // 既存のリストをクリア

        todos.forEach((todo) => {
          const li = document.createElement('li')
          li.textContent = todo
          todoList.appendChild(li)
        })
      }

      // Todoを追加する関数
      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>
  </body>
</html>

命令的 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 (
    <div>
      <h1>Todo List</h1>
      <TodoList todos={todos} />
      <AddTodoForm onAddTodo={addTodo} />
    </div>
  )
}

export default TodoApp

TodoList.jsx

import TodoItem from './TodoItem'

const TodoList = ({ todos }) => {
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem key={index} todo={todo} />
      ))}
    </ul>
  )
}

export default TodoList

TodoItem.jsx

const TodoItem = ({ todo }) => {
  return <li>{todo}</li>
}

export default TodoItem

AddTodoForm.jsx

import { useState } from 'react'

const AddTodoForm = ({ onAddTodo }) => {
  const [newTodo, setNewTodo] = useState('')

  const handleAddClick = () => {
    onAddTodo(newTodo)
    setNewTodo('')
  }

  return (
    <div>
      <input
        type='text'
        value={newTodo}
        onChange={(e) => setNewTodo(e.target.value)}
        placeholder='todoを追加する'
      />
      <button onClick={handleAddClick}>追加</button>
    </div>
  )
}

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 Routerの基礎まとめ

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 は基本的に以下の手順で使う。

  1. パッケージをインストール
  2. BrowserRouter を設定する
  3. パスとコンポーネントを紐づける

前提として、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(
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

3. パスとコンポーネントを紐づける

アプリ全体でルーティングを使えるようになったので、RoutesRouteを使って、URL とコンポーネントを紐づけて具体的なルーティングを設定する。

まず表示したいページコンポーネントを作る。例として Home と About コンポーネントを作る。

Home.jsx

export const Home = () => {
  return <h1>Home</h1>
}

About.jsx

export const About = () => {
  return <h1>About</h1>
}

次に具体的なルーティングを React Router が提供するRoutesRouteを使って 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

RoutesRouteをまとめるためのもので、Routeでパスとそのパスで表示したいコンポーネントを対応させる。

こうすることで、/では Home コンポーネントが、/aboutでは About コンポーネントが表示される。

その他

リンクで画面遷移を行う

HTML の a タグでリンクを実装すると画面が更新されてしまうため、React Router のLinkコンポーネントを使う。

以下のように Home コンポーネントから About コンポーネントに遷移するためのリンクを作成する。

import { Link } from 'react-router-dom'

export const Home = () => {
  return (
    <>
      <h1>Home</h1>
      <Link to='/about'>About</Link>
    </>
  )
}

Linkを使うことで、リロードせずに画面遷移ができる。

何らかの処理の後に画面遷移を行う

何らかの処理の後に画面遷移を行う場合は React Router が提供するuseNavigateを使う。

import { useNavigate } from 'react-router-dom'

export const Home = () => {
  const navigate = useNavigate()

  const handleClick = () => {
    console.log('clicked')
    navigate('/about')
  }

  return (
    <>
      <h1>Home</h1>
      <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 (
    <>
      <h1>Home</h1>
      <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 (
    <>
      <h1>Home</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <Link to={`/product/${product.id}`}>{product.title}</Link>
          </li>
        ))}
      </ul>
      <Link to='/about'>About</Link>
    </>
  )
}

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 (
    <Routes>
      <Route path='/' element={<Home />} />
      <Route path='/about' element={<About />} />
      <Route path='/product/:id' element={<ProductDetail />} />
    </Routes>
  )
}

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 (
    <>
      <h1>Search</h1>
      <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 <h1>NotFount</h1>
}
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

Routepath*とすることで、どのパスにも合致しない場合のコンポーネントを表示できる。

React での TypeScript の型定義まとめ

React で TypeScript を使うときの型定義についてまとめました。

この記事は以下の構成になっています。

  • React 特有の型の指定
  • どこまで型定義するか

React 特有の型の指定

React では基本的に、コンポーネント、props、useState、イベント の型を定義する。

コンポーネントの型

コンポーネントの型はJSX.ElementFCVFCで定義できる。

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 を受け取らない型。

FCVFCはインポートして使う必要がある。

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'

// children を受け取ることを明示する必要がある
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<User> = ({ 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<string>('')
const [count, setCount] = useState<number>(0)
const [count, setCount] = useState<boolean>(true)

配列

const [numbers, setNumbers] = useState<number[]>([])

オブジェクト

type User = {
  name: string
  age: number
}

const [user, setUser] = useState<User>({ 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<string>('')

  // onChangeイベントハンドラ
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setText(event.target.value)
  }

  // onClickイベントハンドラ
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    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 や関数の引数など、どんな値を受け取るか明確にしたい場合は型を指定する。