フロントエンドの進化に重い腰をあげてキャッチアップしてみた

これは [NTTテクノクロスアドベントカレンダー 2024](https://qiita.com/advent-calendar/2024/ntt-tx) 9日目の記事です。

## はじめに

こんにちは。NTTテクノクロスのにわかフロントエンドおじさんこと中野です。

[昨年度の弊社のアドベントカレンダー](https://qiita.com/advent-calendar/2023/ntt-tx)は残念ながらタイミングが合わずにお休みしてしまいましたが、今年は頑張って書きました。えらい。

さて、今回は、自分用の参照実装として定期的にメンテナンスし続けているWebアプリケーションのフロントエンドを、最近の主流っぽい技術スタックでアップデートしてみたという話をします。

フロントエンドの進化というか栄枯盛衰が激しすぎてなかなかタイムリーに追随するのは難しいのですが、定期的にキャッチアップしておかないと時代に取り残されてしまうのでがんばって素振りしていこう、という気持ちです。「残念だったな！もうすでにそれは周回遅れの古いやつだッ！」みたいな可能性もありそうですが、ひとまず気にしません。

## 概要

### Before

対象アプリケーションのフロントエンドとして採用している主な技術スタックはこんな感じです。

- TypeScript
- React
- **[Recoil](https://recoiljs.org/)**
    - だいぶ前にReduxから移行して幸せになっていたが、残念ながら開発停止してしまったので、他のライブラリに移行したい
- **CSSファイルインポート(?)**
    - 方式名がよくわからないけど`import "./MyComponent.css"`みたいに書くやり方を採用してる
    - それほど困ってはいないが、最近のやり方にアップデートしたい
    - ちなみにクラス名の命名規約としては、BEMを雰囲気で使っている
- **[Reactstrap](https://github.com/reactstrap/reactstrap)**
    - 年に2回リリースされ続けてはいるが、Reactによる色々な警告がで続けているまま結構放置されているので、そろそろ卒業したい

なお、今回はAPIサーバー側はいじりません(ちなみにGoによる実装です)。

### After

これ(↑)をこう(↓)していきます。

- TypeScript
- React
- ~~Recoil~~ → **[Jotai](https://jotai.org/)** / **[TanStack Query](https://tanstack.com/query/latest)**
- ~~CSSファイルインポート(?)~~ → **[Tailwind CSS](https://tailwindcss.com/)**
- ~~Reactstrap~~ → **[shadcn/ui](https://ui.shadcn.com/)**

## 各論

### Recoil → Jotai

https://jotai.org/

#### 選定理由

- 個人的にRecoilの正統継承者(?)だと思っている
- 基本的な使い勝手がRecoilと同じ
- 最小限のAPIで無限の可能性、みたいなところにロマンを感じる
- アクロバティックな`atom`の使い方で様々なケースに対応できる
- Recoilで強いられていた謎の文字列キーを指定しなくてよい

#### 感想

- 今回の中では一番移行が楽な方だった
- 元々Recoilで実装していた箇所は、以下の基本方針に基づいて、JotaiとTanStack Query(後述)に移行先を変えている
    - ビューとしての状態は、Jotaiで管理する
        - セッションストレージ等で永続化したい、複数のコンポーネントから状態を参照したい、みたいなケースで使う
        - サイドバーの開閉状態、リスト情報取得時のソートキーやページ数などのリクエストパラメーターの保持、など
        - (ちなみに、編集画面のフォームの内容などは揮発してもよいローカルステートとみなして元々`useState`を使っている)
    - APIからの結果を保持しておくだけの(加工したり二次情報を導出する必要もない)ものは、TanStack Queryのキャッシュに任せる
        - ユーザー一覧画面のためのユーザー情報リスト、ユーザー編集画面のためのユーザー情報、など
- セッションストレージなどに状態を透過的に永続化できる`atomWithStorage`が便利すぎる
    - Recoilでも一応存在はしたが、あくまで実験的機能であって、`effects_UNSTABLE`という(意図的に)汚いコードを書かされていた
- `atomWithDefault`, `atomWithReset`, `atomWithRefresh`などなど、名前だけでは違いがよくわかりづらい派生がたくさんあって混乱する(いまだに)
- Recoilの謎の文字列キーはあまり気にせずに指定していたが、なくなってみると「なんであんなものを書かされていたんだ...」という気持ちになる

### Recoil → TanStack Query

https://tanstack.com/query/latest

#### 選定理由

- React Queryという名前のときから気になっていた(弱い)

#### 感想

- 今回の中で一番苦労した気がする
- 前述の通り、元々Recoilで実装していた箇所から、APIからの結果を保持しておくだけの(加工したり二次情報を導出する必要もない)ものは、TanStack Queryのキャッシュに任せるようにした
- キャッシュの挙動を理解するまで苦労した
- クエリ系はSuspenseに標準対応しているが、[変更系は未対応](https://github.com/TanStack/query/discussions/967)なので、自前でSuspense対応する必要がありかなり面倒だった
    - 要件によると思うが...
        - 基本的にはギョーム系を想定した参照実装なので、更新しているときにスピナー等で「更新してますよ！」とわかるようにしたいし、スピナーが止まったら更新された新しいデータが表示されていて欲しい
        - 古いデータを表示したまま、裏で更新しておいて次の取得タイミングで新しいデータが表示されればよい、という要件であればあまり苦労しないのかもしれない
- 正直まだよくわかっていないというか、正しく使えているのか自信はないが、期待する挙動をするように手元のコードでは調教できてるっぽい、みたいなふんわりした状態
- TanStack Queryとは直接関係ないしRecoilのときからそうだったけど、こういう状態管理系ライブラリの初期化処理は直接フック関数を使える場所ではないので、APIの呼び出しまわりで良かれと思ってカスタムフック化してあれこれ自前処理をねじ込んでいると、トリッキーな回避構造(婉曲表現)を導入せざるを得なくなってかなりつらい
    - **API呼び出し周りはフック関数にせずに素直にどこからでも呼べる関数にするのが一番幸せになれるっぽい**

### CSSファイルインポート(?) → Tailwind CSS

https://tailwindcss.com/

#### 選定理由

- 数年前からどこもかしこも「Tailwindだよねー」みたいな風潮になっていて気になっていた
- 「`style`属性にひたすらインラインでCSSを書いていた古の時代へ逆行してるのでは」みたいなあるあるなネガティブ意見を持っていたが、[Tailwind CSS実践入門](https://gihyo.jp/book/2024/978-4-297-13943-8)を読んで目から鱗がぽろぽろと落ちて、「これはちょっと試してみねば」という欲が高まっていた
- 実は、今回の一連の移行作業の主な動機は「ギョームでの開発対象にある程度近しいアプリでTailwind CSSを試してみたい」だった
- 昨今は[Panda CSS](https://panda-css.com/)も大人気のようだけど、初志貫徹でTailwind CSSに決めた

#### 感想

- **ああ、うん、もうこれでいいんじゃないですかね...**
- 子コンポーネントに`className`プロパティを伝播させて子側では一番外側の要素にそのクラス名を適用する、というのを基本ルールとすることで、親コンポーネント側からかなり柔軟に親子間のスタイルの調整ができるようになった
    - 子の内部要素にまで親が口出すのはよくないので、それは専用プロパティ(`position`, `direction`とか)で制御した方がよさそう
- 「元が`margin: 4px`なので`m-4`っと...」という初歩的ミスを繰り返した (1あたり4pxなので`m-1`が正しい)
    - もうだいぶ慣れたが、モノによってはpx単位なのでやはりいまだに混乱している
- CSSファイルがなくなってディレクトリでまとめておく必要がなくなり、コンポーネントのTSXファイルだけをフラットにおく構造になってすっきりした (Storybookは使ってないので)
- Reactstrapを完全に引き剥がす前だと、ReactstrapのCSS由来の紛らわしいクラス名が補完候補に出てきてだいぶ混乱した
- 書籍で、Tailwind CSSのメリットの一つとして「便宜的なクラス名をBEMとかで頑張ってつけなくてもよくなる」ということが書いてあって、「いや、別にいつものルールでクラス名つければいいだけでしょ」と思っていたが、いざつけなくてよい世界になってみると開放感がすごい
    - あと、ギョームでコードレビューをしていると「一貫したルールでクラス名をつけるということが意外と難しいんだなぁ」という現実と向き合うことも度々あり、Tailwind CSSな世界の方が相対的に幸せに慣れそうな感じがする

### Reactstrap → shadcn/ui

https://ui.shadcn.com/

#### 選定理由

- Tailwind CSSといえばHeadless UIみたいな情報が多かった
- Headless UI(カテゴリ名)と[Headless UI](https://headlessui.com/)(固有名)があって「うわぁ...」という気持ちになった
- Headless UI(カテゴリ名)の方の比較記事をいくつかみたが、shadcn/uiのアプローチが一番好みだった
    - CUIコマンドで、使いたいコンポーネントのソースコードをローカル上にダウンロードしてそれをカスタマイズして使う、というアプローチ
    - 唐突に[Grails](https://docs.grails.org/latest/)にたとえると「静的スキャフォルド」な感じ

#### 感想

- 「しゃっどしーえぬ(ゆーあい)」と呼んでいるが、正しい発音かどうかはわからない
    - [GitHubのIssue](https://github.com/shadcn-ui/ui/issues/2541)で「FAQに読み方を書かないの？」という提案があったが、無慈悲に自動クローズされていた
- **コンポーネント実装としておそらくモダンなのであろうソースコードを手元で参照しながらいじれるので色々勉強になる**
    - アプリケーションの基本カラーの統一とかはCSS変数を使ってこうやるのかー
    - 前項まででTailwind CSS単体で書いていたボタンのカラー種別なども、variantsを活用するとこんなにスッキリ書けるのかー
    - ダークモードはこうやって実装するといいのかー
        - と学びになりつつも`light-dark()`関数を使ったもっと攻めてるやり方があるっぽい？
            - (参考) [2024 年の Webサイトダークモード対応](https://qiita.com/suzukima/items/97ca8451f2e7a9be2ef8)
    - リッチなUIなどは[Radix UI](https://www.radix-ui.com/)を使っていて、その使い方自体も参考になる
- 夢が広がる
    - え、こんな便利なコンポーネントが？
    - え、そんなリッチなUIが、こんなに簡単に？

## おわりに

実はTailwind CSSとshadcn/uiでコンポーネントをいい感じにする作業は現時点ではまだ道半ばなのですが、「ぼくのかんがえたさいきょうの参照実装」に辿り着くまで楽しみつつ細々とやっていこうと思います。

というわけで、チャンスの神様には前髪しかないらしいですが、フロントエンドの神様にはわりと長めの後ろ髪もあるようで、ちょっとくらいなら出遅れても問題ない、という知見が得られました。たぶん。

明日は @s-sekiguchi による記事を引き続きお楽しみください。