Rails 7.0 + Ruby 3.1でゼロからアプリを作ってみたときにハマったところあれこれ

:christmas_tree: Ruby on Rails Advent Calendar 2021の枠が空いていたので、あとから登録しました :christmas_tree: 

## はじめに

個人的なプロジェクトになりますが、僕が翻訳しているRSpecの入門書「[Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp/)」を2022年前半にRails 7.0バージョンにアップデートしようと考えています。

そこでこの本の中で使っているサンプルアプリケーションをRails 7.0でゼロから作り直してみました。フロントエンド周りを中心に結構考え方が変わっている部分があったので、「ここでハマった！」とか「こういうポイントを押さえておくといいかも」という点をあれこれ書いてみます。


なお、Rails 7.0版のサンプルアプリケーションはまだ公開できる状態ではないので、公開はもうしばらくお待ちください🙏


今回作成したサンプルアプリケーションはこちらで公開しています。

https://github.com/JunichiIto/everydayrails-rspec-jp-2022

### 実行環境

この記事は以下の実行環境で動作確認しました。

- Rails 7.0.0 (revision: 6a0f6c4d70b198ef36c2d512314248dabec6fbbc)
- Ruby 3.1.0

### 注意事項等

- この情報はRails 7.0がリリースされて間もない、2021年12月31日時点の情報です。日が経てば状況が変わりそうな内容もあるので、適宜最新の情報を確認するようにしてください。
- 僕自身も試行錯誤しながらやっている部分があるので「そのやり方は違うよ」とか「もっといい方法があるよ」という場合はコメント欄等で教えてください。

### 既存のRailsアプリをRails 7にアップグレードしようとしている方へ

- 本記事はTurboが話題の中心になっていますが、Rails 7.0で"rails new"するのではなく、既存のRailsアプリをRails 7にアップグレードしたときは明示的にturbo-railsのgemやnpmを組み込まない限り、勝手にTurboが有効になることはありません（引き続きrails-ujsが使われます）。
- Turboを導入せずに既存のRailsアプリをRails 7にアップグレードする場合はそれほどハマるところは多くないと思います。ただ、rails-ujsは非推奨になっているのでどこかのタイミングでTurboに移行する必要が出てくるかもしれません。

それでは以下が本文です！

## rails newするときのオプションが増えた

今回はCSSフレームワークとしてBootstrapを使いたかったので、以下のコマンドでrails newしました。

```
rails new everydayrails-rspec-jp-2022 --css bootstrap
```

cssは以下のオプションが選べるようです。

```
rails new myapp --css [tailwind|bootstrap|bulma|postcss|sass]
```

https://github.com/rails/cssbundling-rails

同じくJS用のオプションもあります。
通常のデフォルトは`importmap`ですが、CSSオプションで`tailwind`以外を有効にした場合のデフォルトは`esbuild`っぽいです。

```
rails new myapp --javascript [importmap|esbuild|rollup|webpack]
```

https://github.com/rails/jsbundling-rails

https://techracho.bpsinc.jp/hachi8833/2021_12_28/114730

## 
Rails本体やthorがRuby 3.1.0にまだ対応していない

Rails 7.0.1やThor 1.2.0がリリースされたので、以下の問題は解消しました。

https://rubyonrails.org/2022/1/6/Rails-7-0-1-has-been-released

https://github.com/rails/thor/releases/tag/v1.2.0


Rails 7.0をRuby 3.1.0で動かそうとするとエラーが起きるため、GitHub上のrailsを使用します。


```ruby:Gemfile 
gem "rails", github: "rails/rails", branch: "7-0-stable"
```

https://gist.github.com/yahonda/2776d8d7b6ea7045359f38c10449937b


thorも"DidYouMean::SPELL_CHECKERS.merge!"のような警告を出すため、以下のコードを使いました。


```ruby:Gemfile 
gem 'thor', github: 'deivid-rodriguez/thor', branch: 'support_latest_did_you_mean'
```

https://github.com/rails/thor/pull/761


ただし、こういった対応はRailsやthorの次バージョンがリリースされれば不要になるはずです。


## bin/dev コマンドでサーバを起動する

Rails 7ではお馴染みの`rails s`コマンドでサーバを起動してもJSやCSSがビルド（コンパイル）されません。代わりに`bin/dev`コマンドで起動するとJSやCSSがビルドされ、なおかつJSやCSSを変更したときに自動的に再ビルドされます。

このとき、foremanが必要になるので`bin/dev`の実行時に"foreman: command not found"のようなエラーが出た場合は `gem install foreman` でforemanをインストールしてください。

MinitestやRSpecのような自動テストを実行するときも、JSやCSSを最新に保つ必要があるので、開発時は常時 `bin/dev` で開発サーバを動かしたままにしておく方が良いです。

### bin/devのからくり

`bin/dev`の中身は以下のようになってます。

```bash:bin/dev 
#!/usr/bin/env bash

if ! command -v foreman &> /dev/null
then
  echo "Installing foreman..."
  gem install foreman
fi

foreman start -f Procfile.dev
```

最後の`foreman start -f Procfile.dev`がポイントです。そして`Procfile.dev`の中身は以下のようになっています。

```Procfile.dev
web: bin/rails server -p 3000
js: yarn build --watch
css: yarn build:css --watch
```

さらに、`build`や`build:css`は`package.json`の中で定義されています。

```javascript:package.json
{
  "name": "app",
  "private": "true",
  "dependencies": {
    "@hotwired/stimulus": "^3.0.1",
    "@hotwired/turbo-rails": "^7.1.0",
    "@popperjs/core": "^2.11.0",
    "bootstrap": "^5.1.3",
    "esbuild": "^0.14.9",
    "sass": "^1.45.1"
  },
  "scripts": {
    "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
    "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"
  }
}
```

ここまでの知識を応用すると、`bin/dev`を使わずにJSやCSSをビルドしたい場合に以下のコマンドを使うことができます。

```
# JSのビルド
yarn build

# CSSのビルド
yarn build:css 
```

**（2022.4.20追記）「Everyday Rails - RSpecによるRailsテスト入門」の読者のみなさんへ**
Everyday Railsのサンプルアプリケーションではjsbundling-rails/cssbundling-railsからimportmap-railsに移行しました。そのため、`bin/dev`ではなく`bin/rails s`でサーバが起動するようになっています。

## Turboがいろいろと厄介

Rails 7.0でデフォルトで導入されるTurboがなかなかのクセモノです。

https://turbo.hotwired.dev/

特別難しい処理を実装したわけでもないのに、以下のような症状が出たときはこのあとに説明するポイントをチェックしてみてください。

- 保存ボタンをクリックしても画面遷移せず、同じ画面が再描画される
- 保存ボタンをクリックすると画面遷移はしたが、処理の完了を知らせるフラッシュメッセージが出ない
- 削除ボタンをクリックしたら「本当に削除しますか？」の確認ダイアログが出ず、いきなり処理が実行される
- ボタンやリンクをクリックするとなぜか404エラーが発生した

### バリデーション失敗時に `status: :unprocessable_entity` を付ける必要がある

Turboを使っている場合、バリデーション失敗時のレスポンスに必ず `status: :unprocessable_entity` を付ける必要があります。これがないとバリデーション失敗時のエラーメッセージが表示されません。

```ruby 
  def create
    @project = current_user.projects.new(project_params)

    respond_to do |format|
      if @project.save
        format.html { redirect_to project_url(@project), notice: "Project was successfully created." }
        format.json { render :show, status: :created, location: @project }
      else
        # status: :unprocessable_entity が必須！！
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @project.errors, status: :unprocessable_entity }
      end
    end
  end
```

https://zenn.dev/satococoa/articles/95122320f1ca87

### link_toの`method:`オプションの書き方が変わった

Turboを使っている場合、`link_to`メソッドの`method:`オプションの書き方が変わりました。

```erb 
<!-- 従来の書き方（rails-ujsを使っている場合） -->
<%= link_to 'Delete', [task.project, task], method: :delete %>

<!-- Turboを使っている場合 -->
<%= link_to 'Delete', [task.project, task], data: { turbo_method: :delete } %>
```

ただし、`link_to`でDELETEリクエストを送信する場合は、次のセクションで述べる注意点に気を付ける必要があります。

### destroyのレスポンスに `status: :see_other` を付ける必要がある

Turboを使っている場合、destroyアクションのレスポンスには必ず `status: :see_other` を付ける必要があります。

```ruby 
  def destroy
    @task.destroy

    respond_to do |format|
      # status: :see_other が必須！！
      format.html { redirect_to @project, notice: "Task was successfully destroyed.", status: :see_other }
      format.json { head :no_content }
    end
  end
```

### ~~`status: :see_other` がないと非常に危険！！~~

:::note info
**turbo-rails 1.3.0以降なら安心？**
2022年9月にリリースされたturbo-rails 1.3.0なら、`link_to`でもDELETEではなく、POSTでリクエストを送信するため、ここに書いたような問題が発生しません。
詳しくは以下の記事を読んでください。
[【2025年版】Railsのdestroyアクションにstatus: :see\_otherを付けるべきか否か？ \#Ruby \- Qiita](https://qiita.com/jnchito/items/a1ef3bac55315acd6d45)
:::


これがないとリダイレクト先のURLにDELETEリクエストで遷移してしまい、404エラーが発生したり、最悪、予期せぬリソースを削除してしまうことがあります😱

たとえばこんなことが起きます。

Taskを削除するためにDeleteボタンをクリックします。（`/projects/3/tasks/10`へDELETEリクエストを送信）

![Screen Shot 2021-12-31 at 16.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/0304c877-189f-081b-ae80-969f1c5b106b.png)

なぜか404エラーが発生します。

![Screen Shot 2021-12-31 at 16.27.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/644576ff-a542-653c-cbe0-c99a2173b514.png)

Chromeのネットワークタブをよく見ると・・・削除に成功した後、リダイレクト先の `/projects/3` にDELETEリクエストで遷移しています。これにより、Taskの親レコードであるid=3のProjectが削除されます（おいおいおい！！）

![Screen Shot 2021-12-31 at 16.27.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/2f6cedfc-1575-fcaa-1f7c-17a561f8ca11.png)

Projectを削除した後は、`/projects`ページにリダイレクトします。ここでもDELETEリクエストを送信します。しかし、そんなルーティングはないので404エラーになります。

![Screen Shot 2021-12-31 at 16.27.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/bc15ebc7-f387-9ac4-9f7e-d040bf320477.png)

そのあとはなぜか`/projects/3`にGETでアクセスしようとしています。しかし、id=3のProjectは削除された後なので、やはり404エラーが発生します。そしてブラウザにはこのエラーが表示されています。

![Screen Shot 2021-12-31 at 16.28.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/98ad57ea-5f96-7e26-0a20-c867c092d104.png)

というわけで、 `status: :see_other` を付けなかったがために、404エラーが起きるだけでなく、消すつもりのなかったProjectまで削除されてしまうという、怖い例を紹介しました。Turboを使う場合は十分に気を付けてください。

ボタンやリンクのクリック時に何か予期せぬ動きに気づいたら、Chromeのネットワークタブを開いておかしなリクエストやレスポンスが発生していないかチェックすると、解決の糸口が見つかるかもしれません。

Turboが動作しているときは"fetch"や"turbo"の文字が見えるはずです。
また、通常のリダイレクトはStatusが302ですが、 `status: :see_other`が付いているときはStatusが303になります。

![Screen Shot 2022-01-02 at 9.00.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/3be4bedc-b5b8-7b27-22fe-05fb025733ac.png)

### 上の問題は `link_to` で削除を実行するときに起きる

なお、上の問題は `link_to` で削除ボタン（削除リンク）を作った場合に発生します。`button_to` の場合は `status: :see_other` を付けなくても正常に動作します。

```erb 
<!-- status: :see_other が必須 -->
<%= link_to 'Delete', [task.project, task], data: { turbo_method: :delete } %>

<!-- status: :see_other がなくても大丈夫 -->
<%= button_to 'Delete', [task.project, task], method: :delete %>
```

Rails 7.0でscaffoldを作ると削除ボタンが `button_to` で作成されます。そのため、destroyアクションには`status: :see_other`が付いていません。参考までにscaffoldで自動生成されたコードを載せておきます。

```erb:app/views/tasks/show.html.erb
<p style="color: green"><%= notice %></p>

<%= render @task %>

<div>
  <%= link_to "Edit this task", edit_task_path(@task) %> |
  <%= link_to "Back to tasks", tasks_path %>
  <%= button_to "Destroy this task", @task, method: :delete %>
</div>
```

```ruby:app/controllers/tasks_controller.rb
  # DELETE /tasks/1 or /tasks/1.json
  def destroy
    @task.destroy

    respond_to do |format|
      format.html { redirect_to tasks_url, notice: "Task was successfully destroyed." }
      format.json { head :no_content }
    end
  end
```

しかし、デザイン上の都合等で`button_to`を`link_to`を変えると、前述の問題が起きるので注意してください。ちなみに `button_to` は `status: :see_other` が付いていても正常に動いてそうなので、scaffoldの時点で  `status: :see_other` が付いている方が嬉しいですね。。

### 確認ダイアログ（`confirm:`オプション）の出し方が変わった

Turboを使っている場合、リンクやボタンをクリックした際の確認ダイアログを出すためには次のように書く必要があります。

```erb 
<!-- link_toの場合 -->
<%= link_to 'Delete', [task.project, task], data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } %>

<!-- button_toの場合 -->
<%= button_to "Delete", [task.project, task], method: :delete, data: { turbo_confirm: "Are you sure?" } %>
```

**（2023-02-15追記）** 
上記の記述はTurbo 7.2 (turbo-rails 1.3)以降の場合です。それ以前は `form: { data: { turbo_confirm: "Are you sure?" } }` のように `form:` で囲む必要がありました（[修正PR](https://github.com/hotwired/turbo/pull/564)）。

なお、GETリクエストで`link_to`を作るときも`turbo_method:`オプションは必要です。

```erb 
<%= link_to 'Cancel', task.project, data: { turbo_method: :get, turbo_confirm: 'Are you sure?' } %>
```

https://gorails.com/episodes/turbo-data-confirm-method-and-disable

**（2022.1.28追記）** 
submitボタンの場合はformタグに`data-turbo-confirm`を書く必要があるようです。
詳しくは以下の記事をご覧ください。

https://techracho.bpsinc.jp/hachi8833/2022_01_27/115075

### disable_withの指定方法が変わった

これまではボタンの二重サブミットを防ぐために`data: { disable_with: '送信中...' }`のような書き方が用意されていましたが、Turboを使う場合はボタンタグの中に2つのテキストを用意し、それをCSSで制御するような形になります。

```erb 
<button class="btn btn-primary">
  <span class="show-when-enabled">Submit</span>
  <span class="show-when-disabled">Submitting...</span>
</button>
```

```css 
button                  .show-when-disabled { display: none; }
button[disabled]        .show-when-disabled { display: initial; }

button                  .show-when-enabled { display: initial; }
button[disabled]        .show-when-enabled { display: none; }
```

上のコードを実際に動かすとこうなります。

![bkOudzu260.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/622bf7e0-ce52-af86-a13b-d0a05242b877.gif)

https://github.com/hotwired/turbo/pull/386

### Turboがイヤでもrails-ujsは非推奨になったので戻りづらい

ここまでTurboの落とし穴をいろいろ紹介してきたので、読者のみなさんの中には「Oh、しばらくrails-ujsのままで行こうかな・・・」と思っている人も多いんじゃないかと思います。しかし、rails-ujsは非推奨になったため、しばらくすると使えなくなるかもしれません。

https://github.com/rails/rails/pull/43112

APIドキュメント上でも`confirm:`オプションや`disable_with:`オプションは非推奨になったことが記載されています。

![Screen Shot 2021-12-31 at 17.16.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/62178331-b190-c75f-d77a-405cd15ef474.png)

https://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-button_tag

### confirmやdisable_with（みたいな機能）とTurboは不可分

Turboは`data: { turbo: false }`オプションを付けることで個別にTurboの使用を無効化することができます。

https://turbo.hotwired.dev/handbook/drive#disabling-turbo-drive-on-specific-links-or-forms

Turboを極力使いたくない人は「なるほど！」と思って次のようなコードを書きたくなるかもしれません。

```erb 
<!-- Turboはイヤだ！昔のやり方がいい！！ -->
<%= link_to 'Delete', [task.project, task], method: :delete, data: { turbo: false } %>
```

しかし、上のコードは意図した通りに動きません。なぜなら`method: :delete`を実現しているのはrails-ujsであり、rails-ujsはRails 7.0ではデフォルトでインストールされないからです。

また、GETリクエストを送るリンクであっても、Turboを無効化した上で確認ダイアログを出す、といったことはできません。なぜなら、Turboを無効化することで、`turbo_confirm:`オプションも無効化されるからです。そのため、以下のようなコードでは確認ダイアログは出せません。

```erb 
<!-- Turboを無効化したいが、確認ダイアログだけは出したい、みたいなことはできない -->
<%= link_to 'Cancel', task.project, data: { turbo: false, turbo_confirm: 'Are you sure?' } %>
```

これは上で紹介した二重サブミットを防止するボタンの表示制御（disable_withみたいな機能）についても同様です。

そのためRails 7.0時代では以下のどちらかを選択することになりそうです。

- Turboを有効化して、Turboが提供するconfirmやdisable_with（みたいな機能）を利用する（が、既存のRailsアプリをアップグレードする場合はかなりのコード変更が発生しそう）
- 非推奨であることを理解しつつ、rails-ujsを使ってレガシーな書き方を使い続ける（が、数年後には使えなくなる可能性もある）

### 他にもいろいろハマりどころがありそうな予感……

今回僕が作ったのはごく簡単なRailsアプリなのでこれぐらいで済みましたが、Turboを導入すると他にもいろいろハマりどころがありそうな予感がしています（特にjQueryやVue.js、Reactを多用している場合など）。

TurbolinksがRails 4.0で導入されたときに「メリットよりもハマるデメリットの方が大きいからオフにする」という話をよく見かけましたが、それと同じ状況が起きそうです（そもそもTurbolinksの進化版がTurboですし）。

ただ、Turbolinksとは異なり、前述の通りTurboをオフにするとconfirmやdisable_withが使えなくなりますし、rails-ujsは非推奨になったので、Turboとはうまく付き合っていかなければいけないかもしれません。

## 
DeviseがTurboに対応していない

:::note info
**DeviseがTurboに正式対応しました**
Turboに正式対応したDevise 4.9.0が2023年2月17日にリリースされました。
以下の記事で導入方法を説明しているので、Deviseを使う場合はこちらの記事を読んでください。
[Devise 4\.9をインストールしてRails 7\.0 （Hotwire/Turbo）に対応する \- Qiita](https://qiita.com/jnchito/items/20fad64ab162b2c49bb9)
:::

上で説明してきたように、Turboを導入するとviewやコントローラの書き方がいろいろと変わってきます。自分でviewやコントローラを書いている場合はコツコツとコードを書き換えればいいのですが、場合によってはgemが提供しているviewやコントローラを使う場合があります。その代表例がDeviseです。

この記事の執筆時点ではDeviseのバージョンはDevise 4.8.1ですが、DeviseはまだTurboに対応できていないことが正式に述べられています。

> 4.8.1 - 2021-12-16
> enhancements
> Add support for Rails 7.0. Please note that Turbo integration is not fully supported by Devise yet.
>
> https://github.com/heartcombo/devise/blob/main/CHANGELOG.md

具体的には次のような問題が起きます。

- バリデーション失敗時のレスポンスに `status: :unprocessable_entity` が付いていないので、ログインに失敗してもエラーメッセージが何も表示されない
- DELETEリクエストのレスポンスに `status: :see_other` が付かないため、ログアウトやアカウント削除のリンクが正常に機能しない（ルーティングエラーが起きたりする）
- アカウント削除のボタンをクリックしても確認ダイアログが表示されない

このため、僕は一時的な回避策として以下のような対応を取りました。

### 
formはいったんTurboを無効化する


**2023.1.4追記：このセクションの記述は無視してください**

`status: :unprocessable_entity` を付けるためにはDeviseのコントローラを修正しなければなりません。ですが、Deviseのコントローラの実装はscaffoldのような単純なものではないので、 `status: :unprocessable_entity` を付けようにもどこに付ければ良いかぱっとわかりません。

そのため、コントローラをいじるのは諦めて `rails generate devise:views` で生成されたviewのformに`data: { turbo: false }`オプションを付けて回りました。

```erb:app/views/devise/sessions/new.html.erb
<!-- Turboを無効化したformの例 -->
<%= form_for(resource, as: resource_name, url: session_path(resource_name), data: { turbo: false }) do |f| %>
```

### 
DELETEリクエストのみ `status: :see_other` を付ける


**2023.1.4追記：このセクションの記述は無視してください**

今回、DELETEリクエストはログアウトとアカウント削除の2つだけだったので、これは自前のコントローラを用意してレスポンスを返す部分をオーバーライドしました。

**【注意！！】** 
以下は応急処置目的で雑に追加したコードです。ユースケースによっては不具合を引き起こす可能性もあるので、実務で導入する場合はDeviseの実装コードを十分確認した上で対処することを推奨します。（というか、DeviseがTurboに正式対応するまでTurboの導入は見送った方がいいかも）

```ruby:config/routes.rb
# registrationsとsessionsのコントローラは自前で用意
devise_for :users, controllers: { registrations: 'registrations', sessions: 'sessions' }
```

```ruby:app/controllers/registrations_controller.rb
class RegistrationsController < Devise::RegistrationsController
  def destroy
    resource.destroy
    Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
    set_flash_message! :notice, :destroyed
    yield resource if block_given?
    # status:とnotice:を追加
    respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name), status: :see_other, notice: find_message(:destroyed) }
  end
end
```

```ruby:app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
  def respond_to_on_destroy
    respond_to do |format|
      format.all { head :no_content }
      # status:とnotice:を追加
      format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name), status: :see_other, notice: find_message(:signed_out) }
    end
  end
end
```

### 
ログアウトリンクをTurboに対応させる


**2023.1.4追記：このセクションの記述は無視してください**

ログアウトのリンクは以下のように `turbo_method: :delete` を使うように変更します。

```erb 
<%= link_to "Sign Out", destroy_user_session_path, data: { turbo_method: :delete } %>
```

### 
アカウント削除ボタンをTurboに対応させる


**2023.1.4追記：このセクションの記述は無視してください**

確認ダイアログが表示されるよう、アカウント削除ボタンを以下のように修正します。

```erb 
<%= button_to "Cancel my account", registration_path(resource_name), method: :delete, form: { data: { turbo_confirm: "Are you sure?" } } %>
```

## Turboを使ってAjax風の更新処理を実装する

ずっとTurboを悪者扱いにしてきましたが、Turboを使うとこんなことができますよ、というポジティブな例を紹介しておきます。

今回実装するのはこんなふうにチェックボックスをクリックすると画面のリロード無しにタスクの完了状態を切り替えることができる機能です。

![rUUASb6ENv.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/791a56a9-2b10-2ea5-b18a-ac1cae3fa402.gif)

### 更新用のルーティングを定義する

まず、タスクの完了状態を切り替えるためのルーティング（`tasks#toggle`）を定義します。

```ruby:config/routes.rb
  resources :projects do
    # 略
    resources :tasks do
      member do
        patch :toggle
      end
    end
  end
```

### partialを作る

次に、Turboを利用するためのpartialを用意します。

```erb:app/views/tasks/_completed.html.erb
<%= turbo_frame_tag task do %>
  <%= form_with model: task, url: [:toggle, task.project, task] do |f| %>
    <label class="<%= task.completed? ? 'completed' : '' %>">
      <%= f.check_box :completed, onchange: "this.form.requestSubmit()" %>
      <%= task.name %>
    </label>
  <% end %>
<% end %>
```

ポイントは以下のとおりです。

- `turbo_frame_tag task do`で囲んだ部分がTurboで差し替え可能なDOMになる。
- `onchange: "this.form.requestSubmit()"`でチェックボックスがON/OFFされたときにsubmitされるようにする。
  - `submit()`ではTurboが動作しないので、`requestSubmit()`にしないといけない点に注意。

なお、上のpartialは以下のようなHTMLを出力します。

```html 
<turbo-frame id="task_17">
  <form action="/projects/3/tasks/17/toggle" accept-charset="UTF-8" method="post">
    <input type="hidden" name="_method" value="patch" autocomplete="off">
    <input type="hidden" name="authenticity_token" value="m-L35ID..." autocomplete="off">
    <label class>
      <input name="task[completed]" type="hidden" value="0" autocomplete="off">
      <input onchange="this.form.requestSubmit()" type="checkbox" value="1" name="task[completed]" id="task_completed">
      原稿を書く
    </label>
  </form>
</turbo-frame>
```

### メインのviewに組み込む

上で作ったpartialをviewに組み込みます。partialの中では`turbo_frame_tag task do`を使っていますが、ここでは通常のpartialのようにHTMLがレンダリングされます。

```erb:app/views/tasks/_task.html.erb
  <!-- 略 -->
  <td>
    <%= render 'tasks/completed', task: task %>
  </td>
  <!-- 略 -->
```

ちなみに以下の青枠部分がpartialの表示部分で、Turboで差し替え可能な部分になります。

![Screen Shot 2021-12-31 at 19.07.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/4e634175-0a13-168a-3174-50928f47c1e9.png)

### コントローラを実装する

コントローラの`tasks#toggle`アクションは次のように実装しました。

```ruby:app/controllers/tasks_controller.rb 
def toggle
  @task.update(completed: !@task.completed)

  render turbo_stream: turbo_stream.replace(
    @task,
    partial: 'completed',
    locals: { task: @task }
  )
end
```

ポイントは以下のとおりです。

- `turbo_stream: turbo_stream.replace`で「Turboを利用したDOMに置き換え」を指定する。
- `replace`メソッドの各引数の意味は以下のとおり。
  - `@task` = Taskオブジェクトが生成する`dom_id`（"task_17"など）に合致するidのturbo-frameを置き換える
  - `partial: 'completed'` = 置き換えに使用するpartial
  - `locals: { task: @task }` = partialに渡すパラメータ

### CSSで表示を整える

Taskが完了するとlabelタグに"completed"のclassが付くので、その場合は取り消し線が入るようにします。

```scss 
.completed {
  text-decoration: line-through;
}
```

### 完成！

これで完成です :tada: 

![rUUASb6ENv.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/791a56a9-2b10-2ea5-b18a-ac1cae3fa402.gif)

なお、この機能の作成にあたっては以下のサイトを参考にさせてもらいました。

https://qiita.com/NaokiIshimura/items/d91b10587881107b5f39

https://techracho.bpsinc.jp/hachi8833/2021_06_09/108495

https://blog.corsego.com/hotwire-turbo-button-to-like-record

## RSpec実行時の警告に対応する

RSpecを実行すると警告がいくつか出たので以下のように対応しました。

### debug.gemを`require: false`する

`js: true`が付いているシステムスペックを実行すると以下のような警告が出ます。

```
$ bundle exec rspec -f progress
...................................................................DEBUGGER: Attaching after process 69034 fork to child process 69055
...

Finished in 3.26 seconds (files took 1.79 seconds to load)
70 examples, 0 failures
```

一瞬「テストに問題があるのかな？」と勘違いしてしまうので、debug.gemを`require: false`にして必要なとき以外は読み込まれないようにしました。

```ruby:Gemfile 
  gem "debug", platforms: %i[ mri mingw x64_mingw ], require: false
```

https://github.com/ruby/debug/issues/394

### Capybaraの警告に対処する

`js: true`が付いているシステムスペックを実行すると以下のような警告が出ます。

```
WARN Selenium [DEPRECATION] [:browser_options] :options as a parameter for driver initialization is deprecated. Use :capabilities with an Array of value capabilities/options if necessary instead.
```

この警告はCapybaraの問題なのですが、masterではすでに対応済みなので、次バージョン（3.36.0の次）がリリースされるまでGitHubのコードを使います。

```ruby:Gemfile 
  gem "capybara", github: 'teamcapybara/capybara'
```

ちなみにこの警告を修正するプルリクエストは僕が出しました（自慢）。

https://github.com/teamcapybara/capybara/pull/2520

### 
VCRの警告に対処する


VCR 6.1.0がリリースされたので、以下の問題は解消しました。

https://github.com/vcr/vcr/blob/v6.1.0/CHANGELOG.md

~~Ruby 3.1.0でVCRを使うと以下のような警告（例外情報）が表示されます。~~

```
/Users/jnito/.rbenv/versions/3.1.0-preview1/bin/bundle: warning: Exception in finalizer #<Proc:0x000000010a01e6f8 /Users/jnito/.rbenv/versions/3.1.0-preview1/lib/ruby/gems/3.1.0/gems/vcr-6.0.0/lib/vcr/library_hooks/webmock.rb:36 (lambda)>
/Users/jnito/.rbenv/versions/3.1.0-preview1/lib/ruby/gems/3.1.0/gems/vcr-6.0.0/lib/vcr/library_hooks/webmock.rb:36:in `block in global_hook_disabled_requests': wrong number of arguments (given 1, expected 0) (ArgumentError)
```

~~こちらもmasterではすでに対応済みなので、次バージョン（6.0.0の次）がリリースされるまでGitHubのコードを使います。~~

```ruby:Gemfile 
  gem 'vcr', github: 'vcr/vcr'
```

https://github.com/vcr/vcr/pull/907

## まとめ

というわけで、この記事ではRails 7.0でゼロから簡単なRailsアプリケーションを作ったときに気づいた点やハマった点をあれこれ書いてみました。

実際にアプリを作る前は「フロントエンド周り以外はそんなに変わってないのでは？」と考えていましたが、そのフロントエンド周りが思った以上に変わっていたので、従来のノウハウがそのままでは活かせない部分が多かったです。

特にコントローラに追加する `status: :unprocessable_entity` や `status: :see_other` オプションなどは、予備知識がまったくなかったのでなぜエラーが出るのかわからず、かなりハマりました。

みなさんもRails 7.0でRailsアプリを作るときはこの記事に挙げたポイントに注意しながら開発していってください。

### 謝辞

「Rails 7、ここがようわからん」と思った点は[ruby-jp](https://ruby-jp.github.io/)で質問していろいろ情報を教えてもらいました。回答してくれたみなさん、どうもありがとうございました！

### PR：「Everyday Rails - RSpecによるRailsテスト入門」をアップデートしました！
冒頭にもちらっと書いたとおり、僕が翻訳している電子書籍「Everyday Rails - RSpecによるRailsテスト入門」を
2022年前半にアップデートする予定です 
アップデートしました！

アップデート版ではRuby 7.0 + Ruby 3.1環境でテストを書く方法を説明しています。
今回のアップデートの詳細は以下のブログで紹介しています。

https://blog.jnito.com/entry/2022/01/17/115109

すでに持っている方は無料でアップデートできますので、Leanpubのサイトから最新版をダウンロードしてください。

https://leanpub.com/everydayrailsrspec-jp