はじめに
弊社では、バックエンドエンジニアがフロントエンドのコードを書くことがよくあります。
それ自体は悪いことではありません。むしろ、業務仕様やデータ構造を理解しているバックエンドエンジニアがフロントエンドまで触れることで、実装の速度が上がる場面も多いです。
特に今後はフロントエンドだけに閉じたエンジニアはAIによって淘汰される可能性が高くなってくると考えており、ドメインについて深い理解を持つエンジニアがバックエンドもフロントエンドも担当することになると考えています。
ただし、バックエンドの設計をそのままフロントエンドに持ち込むと、かえってコードが複雑になります。
たとえば、クリーンアーキテクチャやレイヤードアーキテクチャのような考え方を、フロントエンドにそのまま適用してしまうケースです。
もちろん、関心の分離や依存方向の整理は重要です。
しかし、バックエンドとフロントエンドでは、そもそも守るべきものが違います。
バックエンドでは、DB、永続化、トランザクション、整合性、外部システム連携など、長く維持すべき中心があります。
一方でフロントエンドは、画面、操作、表示要件、ユーザー体験に強く引っ張られます。コードの寿命も影響範囲も違う領域に対して同じ考え方、アーキテクチャで解決できるわけがないのです。
さらに、Reactを使うのであれば、React独自の考え方にも従う必要があります。
Reactには、コンポーネント、Props、State、Hooks、Effect、Contextなど、Reactの思想に基づいた設計の仕組みがあります。
これらを理解せずに、バックエンドの設計だけを持ち込んでも、Reactらしいフロントエンドにはなりません。
Reactを勉強してみて感じるのは、Reactというのはフレームワークというより「思想そのもの」だということです。Laravelなど自由度の高いフレームワークと比較するとReactの考え方にある程度従わないといけない点も違いがある点の一つです。
この記事では、最新の流行を追うというより、バックエンドエンジニアがフロントエンドを書くときに、最低限破綻しないために意識してほしいことを整理します。
バックエンドとフロントエンドでは守る対象が違う
まず前提として、バックエンドとフロントエンドでは関心の中心が違います。
バックエンドでは、業務ルールやデータの整合性が重要です。
たとえば、予約管理システムであれば、同じ時間帯に予約が重複していないか、在庫数を超えていないか、権限のないユーザーが操作していないか、といったことを守る必要があります。
これらは画面が変わっても残り続けるルールです。
そのため、バックエンドでは画面やAPIの都合から切り離して、純粋な業務ロジックとして集約する意味があります。
一方で、フロントエンドは違います。
フロントエンドでは、ユーザーにどう見せるか、どの順番で入力させるか、どのタイミングでバリデーションを出すか、どの状態でボタンを無効化するか、といった画面上の関心が非常に大きくなります。
同じ「予約作成」であっても、管理画面、ユーザー向け画面、スマホ画面、簡易登録画面では、必要な状態や操作の流れが変わります。
つまり、フロントエンドのコードは、純粋な業務ロジックだけではなく、画面単位の関心に強く引っ張られます。
ここを無視して、バックエンドと同じようにロジックだけをきれいに抜き出そうとすると、逆に不自然な設計になります。
フロントエンドは画面単位の集約が入る
バックエンドでは、ロジックを中心に集約することが多いです。
たとえば、予約、ユーザー、注文、在庫といったドメイン単位で責務を分けます。
もちろんフロントエンドでも、データや機能のまとまりは重要です。
しかし、フロントエンドではそれに加えて、画面単位の集約が入ります。
なぜなら、フロントエンドでは「同じ画面で一緒に変わるもの」が多いからです。
たとえば、ある一覧画面には次のような要素があります。
- 一覧の取得
- 検索条件
- ソート条件
- ページネーション
- 選択中の行
- モーダルの開閉状態
- 削除確認
- エラー表示
- ローディング表示
- 空状態の表示
これらは、純粋な業務ロジックだけで見ると別々の関心に見えるかもしれません。 しかし、画面を変更するときには一緒に触ることが多いです。
このとき、バックエンド的な感覚で、検索条件、ページネーション、モーダル制御、エラー処理、表示用ロジックを全部別々のレイヤーに分けると、1つの画面を変更するだけで大量のファイルを横断することになります。
これは保守しやすい設計ではありません。
フロントエンドでは、画面単位でまとまっていたほうが自然なものがあります。
つまり、フロントエンドの設計では、関心の分離だけでなく、画面単位の集約も考える必要があります。
典型的なのは関連画面でしか利用しないコンポーネントです。これらをComponentsのような技術的な分類をしているフォルダにまとめてないでしょうか?関心を意識するようにしましょう。
Reactを使うならReactの思想に従う
フロントエンド設計を考えるときに、もう1つ重要なのが、使っているフレームワークの思想に従うことです。
Reactを使っているなら、Reactの考え方に沿った設計が必要です。
Reactは、単にHTMLをコンポーネント化するための道具ではありません。 UIを状態の関数として扱い、状態の変化に応じて画面を再描画するための仕組みです。
そのため、Reactでは次のような考え方が重要になります。
- UIをコンポーネントとして分割する
- Propsでデータを渡す
- Stateで画面の状態を表現する
- Hooksで状態や副作用を扱う
- Effectを必要最小限にする
- 派生できる値をStateにしない
- コンポーネントを純粋に保つ
- データの流れを一方向に保つ
これらは、Reactにおける基本です。
逆に言えば、Reactの思想を理解せずに、バックエンドの設計だけでフロントエンドを組み立てようとすると、Reactと相性の悪いコードになります。
たとえば、何でもクラスやサービスに寄せたり、ReactのStateで表現すべきものを外部のstoreに逃がしたり、Effectで不要な同期処理を書いたりすると、コードはどんどん分かりづらくなります。
特にHooksによるロジックの切り出しと、不要なEffectsの利用、memo化などの最適化不足(React Compilerで多くは解決できるようになりましたが、、、そもそも知らない人が多いです。)
ReactにはReactの設計作法があります。
バックエンドの設計知識は役に立ちますが、それはReactの思想の上に乗せるべきです。 Reactの思想を無視して、バックエンドの構造を押し込むべきではありません。
HooksはReactにおける設計単位になる
Reactでは、Hooksの使い方が設計に大きく影響します。
Hooksは、単なる便利関数ではありません。 状態、副作用、イベント処理、外部データとの接続などを、コンポーネントの文脈で扱うための仕組みです。
たとえば、一覧画面の処理を考えると、次のようなCustom Hookを作ることがあります。
function useReservationList() {
const [keyword, setKeyword] = useState('')
const [selectedId, setSelectedId] = useState<string | null>(null)
const reservations = useReservationsQuery({ keyword })
return {
keyword,
setKeyword,
selectedId,
setSelectedId,
reservations,
}
}
このようなHookは、バックエンドでいう純粋なドメインロジックとは違います。
検索条件、選択状態、データ取得、ローディング状態など、画面に強く結びついた関心をまとめています。
これは悪いことではありません。
Reactでは、画面に関心を持つロジックをCustom Hookとして切り出すことで、コンポーネントの見通しをよくできます。
ただし、何でもHookにすればよいわけでもありません。
Hookにする意味があるのは、状態や副作用を含む処理、複数のコンポーネントで共有したい処理、コンポーネントから切り出すことで意図が明確になる処理です。
単なる関数でよいものまでHookにする必要はありません。
function formatReservationTime(startAt: string, endAt: string): string {
return `${startAt} - ${endAt}`
}
このような処理は、Reactの状態やライフサイクルに関係しないので、普通の関数で十分です。
Reactでは、普通の関数、コンポーネント、Custom Hookを使い分けることが重要です。
公式ドキュメントを読むことが一番の近道
Reactを書くなら、公式ドキュメントをちゃんと読むべきです。
これは精神論ではありません。
Reactは、ここ数年で推奨される考え方がかなり整理されています。数ある技術的な公式ドキュメントのなかでもクオリティの高い文書が揃っている印象です。
特に、現在のReact公式ドキュメントでは、コンポーネントの純粋性、Stateの設計、Effectの使い方、Custom Hookの考え方などが、かなり実践的に説明されています。
昔の知識のままReactを書くと、不要なEffect、過剰なState、複雑なContext、巨大なコンポーネントを作りがちです。
たとえば、次のような考え方は、Reactを書くうえでかなり重要です。
- Stateは必要最小限にする
- PropsやStateから計算できる値をStateにしない
- ユーザー操作による処理とEffectによる同期処理を分ける
- Effectは外部システムとの同期に使う
- レンダリング中に計算できるものはレンダリング中に計算する
- コンポーネントは可能な限り純粋に保つ
- Custom Hookで状態を持つロジックを再利用する
これらは、バックエンドのアーキテクチャ本を読んでも身につきません。
ReactにはReactの設計思想があります。 そのため、Reactでアプリケーションを書くなら、まずReactの公式ドキュメントを読むべきです。
フロントエンドアーキテクチャを考える前に、Reactが何を推奨しているのかを理解する必要があります。
関心の分離は必要。ただし分離しすぎない
誤解してほしくないのは、フロントエンドでは関心の分離が不要だと言っているわけではない、ということです。
むしろ、関心の分離は必要です。
API通信、画面表示、入力状態、バリデーション、エラー処理、権限制御などがすべて1つのコンポーネントに詰め込まれているコードは、当然つらくなります。
ただし、分離の単位を間違えると、別のつらさが生まれます。
バックエンドでは、業務ロジックを画面や通信の都合から分離することに大きな意味があります。 一方でフロントエンドでは、画面の都合自体が主要な関心です。
そのため、画面に強く依存する処理を、無理に画面から遠ざけすぎる必要はありません。
たとえば、次のようなものは画面に近くてよいです。
- モーダルを開くかどうか
- どのタブを選択しているか
- 入力中のフォーム状態
- 一覧の表示順
- UI上の選択状態
- ボタンのdisabled条件
- 画面固有の表示用ラベル
これらを無理に共通化したり、画面から離したりすると、むしろ読みづらくなります。
フロントエンドで重要なのは、何でも分離することではありません。
一緒に変わるものは近くに置く。 別の理由で変わるものは分ける。
この判断が重要です。
まず状態を分類する
フロントエンド設計で最初に意識すべきなのは、ディレクトリ構成よりも状態管理です。
状態の扱いを間違えると、どれだけきれいなディレクトリ構成にしても破綻します。
状態は大きく次の3つに分けて考えるとよいです。
- サーバー由来の状態
- アプリケーション全体で共有する状態
- 画面やコンポーネントに閉じる状態
この分類をせずに、何でもグローバルな状態管理に入れると、フロントエンドの中に疑似的なDBを作ることになります。
それは避けるべきです。
サーバー由来の状態をグローバル状態に入れない
APIから取得したデータは、基本的にサーバー由来の状態です。
たとえば、ユーザー一覧、商品一覧、予約情報、注文履歴などです。
これらを何でもグローバル状態に入れると、次のような問題が起きます。
- いつ取得されたデータなのか分からない
- どこで更新されたのか分からない
- 再取得のタイミングが分からない
- サーバー上の状態とズレる
- キャッシュの破棄条件が複雑になる
サーバー由来の状態は、フロントエンドが所有している状態ではありません。 本当の正はサーバー側にあります。
そのため、フロントエンドでは「保持する」というより「取得し、キャッシュし、必要に応じて再取得する」と考えるほうが自然です。
SWRやTanStack Queryのようなライブラリは、この問題を扱うためのものです。
もちろん、ライブラリの名前自体が重要なのではありません。 重要なのは、サーバー由来の状態と、フロントエンドが本当に所有する状態を分けて考えることです。
グローバル状態は最小限にする
グローバル状態は便利です。
しかし、便利だからといって多用すると、依存関係が見えなくなります。
どこからでも読める。 どこからでも書ける。 だからこそ、どこで変わったのか分からなくなります。
グローバル状態に置くべきものは、基本的に画面をまたいで保持する必要があるものだけです。
たとえば、次のようなものです。
- 認証中のユーザー情報
- アプリ全体の設定
- Toastや通知
- 画面遷移をまたいで必要な一時状態
- 複数画面にまたがる処理の進行状態
逆に、ある画面の中だけで使う状態は、基本的にその画面の近くに置くべきです。
グローバルに置くと再利用しやすくなるように見えますが、実際には不要な結合を増やすことが多いです。
ローカル状態は画面の近くに置く
フォームの入力値、モーダルの開閉、タブの選択、一覧の選択状態などは、多くの場合ローカル状態です。
こうした状態は、無理に外へ出す必要はありません。
むしろ、画面の近くに置いたほうが読みやすくなります。
const [isOpen, setIsOpen] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
この程度の状態まで、専用のstoreやusecaseに逃がす必要はありません。
画面に閉じている状態は、画面に閉じていてよいです。
フロントエンドでは「状態をどこに置くか」が、そのまま設計になります。
ReactではStateを増やしすぎない
Reactで特に注意したいのは、Stateを増やしすぎないことです。
Reactでは、Stateが変わると再レンダリングが発生します。 そのため、Stateは画面の状態を表現するために重要な仕組みです。
しかし、何でもStateにすると、状態の同期が複雑になります。
たとえば、次のようなコードは避けたいです。
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [fullName, setFullName] = useState('')
fullName が firstName と lastName から計算できるなら、Stateとして持つ必要はありません。
const fullName = `${firstName} ${lastName}`
このように、PropsやStateから計算できる値は、基本的にレンダリング中に計算すればよいです。
Stateを増やすほど、同期すべきものが増えます。 同期すべきものが増えるほど、バグが増えます。
これはReactを書くうえでかなり重要な感覚です。
Effectを濫用しない
Reactでよくある問題が、Effectの濫用です。
useEffect は便利ですが、何でもEffectに書くとコードが分かりづらくなります。
Effectは、基本的に外部システムとReactの状態を同期するための仕組みです。
たとえば、ブラウザAPI、外部ライブラリ、購読、タイマー、外部ストアなどと同期する場合にはEffectが必要になることがあります。
しかし、レンダリング中に計算できるものや、ユーザー操作の中で処理できるものまでEffectに入れる必要はありません。
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
このような処理は、Effectではなくレンダリング中に計算すれば十分です。
const fullName = `${firstName} ${lastName}`
Reactでは、Effectを使う前に「本当に外部システムとの同期なのか」を考えるべきです。
Effectが増えるほど、コンポーネントの挙動は追いづらくなります。
そもそもSWRなどのライブラリを適切に利用することで95%くらいのuseEffectは削除できると考えており、useEffectを書いている場合は全てレビューで確認するくらいの温度感で良いと思います。
ディレクトリ構成はレイヤーより変更単位を優先する
バックエンドでは、次のような分け方をすることがあります。
domain/
application/
infrastructure/
presentation/
これは、バックエンドでは自然な分け方です。
しかし、フロントエンドでこれをそのままやると、変更単位とディレクトリ構成がズレることがあります。
フロントエンドでは、ある画面を変更するときに、コンポーネント、Hooks、型、テスト、Storybook、スタイルなどをまとめて触ることが多いです。
そのため、ファイル種別ごとに遠くへ分けるより、画面や機能の近くに置いたほうが保守しやすいことがあります。
たとえば、次のような構成です。
features/reservation-list/
ReservationListPage.tsx
ReservationListTable.tsx
useReservationList.ts
reservationListTypes.ts
reservationListSchema.ts
ReservationListPage.test.tsx
ReservationListTable.stories.tsx
ここで重要なのは、ファイル名やディレクトリ名そのものではありません。
重要なのは、予約一覧画面を変更するときに見るべきものが近くにあることです。
また、Next.jsなどのファイルベースルーティングを利用している場合はパスごとのディレクトリ構造になるため、パスごとに関連するものをまとめる構造になるのが自然です。
これがコロケーションです。
コロケーションを基本にする
フロントエンドでは、コロケーションを基本にするべきです。
コロケーションとは、一緒に変わるものを近くに置くことです。
たとえば、ある画面でしか使わないコンポーネントを、全体共通の components ディレクトリに置く必要はありません。
ある画面でしか使わないHooksを、全体共通の hooks ディレクトリに置く必要もありません。
ある画面でしか使わない型を、全体共通の types ディレクトリに置く必要もありません。
最初から共通化しすぎると、どの画面のためのコードなのか分からなくなります。
共通化は、実際に複数箇所で使われてから考えれば十分です。
悪い例:
components/
ReservationListTable.tsx
hooks/
useReservationList.ts
types/
reservation.ts
schemas/
reservationSchema.ts
良い例:
features/reservation-list/
ReservationListTable.tsx
useReservationList.ts
reservationListTypes.ts
reservationListSchema.ts
共通化よりも、まずは変更しやすさを優先するべきです。
共通化は早すぎると負債になる
バックエンドエンジニアは、重複を嫌う傾向があります。
もちろん、重複が増えすぎるのは問題です。 しかし、フロントエンドでは早すぎる共通化のほうが問題になることがあります。
なぜなら、画面は似ているように見えて、後から少しずつ違っていくからです。
最初は同じに見えた一覧画面でも、片方には一括操作が必要になり、片方にはステータス表示が必要になり、片方には特殊な権限制御が必要になるかもしれません。
このとき、早い段階で無理に共通コンポーネント化していると、共通コンポーネントに大量のpropsや分岐が増えていきます。
<DataTable
showCheckbox
showStatus
showBulkAction
enableSpecialMode
userType="admin"
layout="compact"
/>
こうなると、共通化したはずのコンポーネントが、逆に複雑さの中心になります。
フロントエンドでは、少しの重複を許容してでも、画面ごとに分かれていたほうが変更しやすい場合があります。
共通化は、重複が見えてからでよいです。
UIコンポーネントと画面固有コンポーネントを分ける
フロントエンドでは、コンポーネントの性質を分けて考えることが重要です。
大きく分けると、次の2種類があります。
- 汎用的なUIコンポーネント
- 画面や業務に依存するコンポーネント
汎用的なUIコンポーネントは、Button、Input、Modal、Table、Badgeのようなものです。 これらは特定の業務知識を持たないほうがよいです。
<Button variant="primary">保存</Button>
一方で、予約一覧、ユーザー詳細、注文作成フォームのようなコンポーネントは、画面や業務に依存します。
<ReservationListTable reservations={reservations} />
この2つを混ぜると、UIコンポーネントが業務知識を持ち始めます。
たとえば、汎用的なTableコンポーネントの中に「予約ステータスがキャンセルなら赤くする」といった処理が入ると、それはもう汎用UIではありません。
UIコンポーネントはできるだけ業務を知らない。 業務に依存する表示は、画面や機能側に置く。
この分離は重要です。
TypeScriptでは型の置き場所より境界を意識する
TypeScriptでは、型定義をどこに置くかで悩むことが多いです。
しかし、本当に重要なのは types.ts をどこに置くかではありません。
そもそも私はtypes.tsがある時点でそのエンジニアの実力を疑うレベルです。型定義は境界を意識すれば自然と置き場所が決まり、専用ファイルを作るべきではないと考えています。
重要なのは、どの境界で型を分けるかです。
特に、APIのレスポンス型をそのまま画面全体に流すのは避けたほうがよいです。
サーバーの都合で作られた型と、画面の都合で使いやすい型は必ずしも一致しません。
たとえば、APIでは次のようなレスポンスが返るとします。
type ReservationResponse = {
id: string
customer_first_name: string
customer_last_name: string
start_at: string
end_at: string
status: 'reserved' | 'cancelled'
}
画面では、次のような形で扱いたいかもしれません。
type ReservationListItem = {
id: string
customerName: string
timeRangeLabel: string
statusLabel: string
isCancelled: boolean
}
この変換処理をどこに置くかの名前は重要ではありません。
重要なのは、APIの都合と画面の都合を混ぜないことです。
サーバーから返ってきた形をそのまま画面全体に広げると、APIの変更が画面全体に波及します。
逆に、画面で扱いやすい形に整えてから使えば、影響範囲を小さくできます。
つまり、TypeScriptで重要なのは型ファイルの分類ではなく、境界の設計です。
API通信は隠してよいが、過剰に抽象化しない
API通信をコンポーネントに直接書き散らすと、コードはすぐに読みにくくなります。
useEffect(() => {
fetch('/api/reservations')
.then((res) => res.json())
.then((data) => setReservations(data))
}, [])
このようなコードが画面ごとに増えていくと、エラー処理、ローディング処理、再取得、キャッシュ、型変換などが散らばります。
そのため、API通信の詳細を隠すこと自体は有効です。
ただし、ここでもバックエンドのRepositoryをそのまま再現する必要はありません。
フロントエンドにおけるAPI通信の整理は、DB永続化の抽象化ではありません。 API呼び出し、エラー処理、データ整形、キャッシュ更新などを、画面から適切に切り離すためのものです。
名前は何でもよいです。
repository でも、api でも、service でも、client でも構いません。
重要なのは、責務が明確であることです。
逆に、名前だけ立派で中身が薄い層を増やすのは避けるべきです。
component
↓
usecase
↓
service
↓
repository
↓
apiClient
このような構成が常に必要なわけではありません。
単純な画面であれば、もっと薄くてよいです。
Usecaseのような層は必要になってから作る
バックエンドでは、ユースケース層を作ることに意味があります。
ユーザー登録、注文確定、予約変更など、アプリケーションとしての操作を表現するためです。
しかし、フロントエンドで何でもユースケース化すると、過剰設計になります。
たとえば、ボタンを押してAPIを呼び、成功したらToastを出すだけの処理に、毎回専用のusecaseファイルを作る必要はありません。
それよりも、画面の近くに処理があったほうが読みやすいことがあります。
一方で、次のような場合は、処理を切り出す意味があります。
- 複数のAPIを組み合わせる
- 複数の画面から同じ操作を使う
- 成功時に複数のキャッシュ更新が必要
- エラー処理が複雑
- 権限や状態によって処理が分岐する
- 画面から見て処理の意図を名前で表したい
つまり、層は最初から作るものではなく、必要な複雑さが出てきたときに作るものです。
フロントエンドでは、構造を先に決めすぎると、変更に弱くなります。
Atomic Designを目的化しない
フロントエンドの設計でよく出てくるものにAtomic Designがあります。
Atomic Design自体が悪いわけではありません。 UIを分解して考えるための方法としては有用です。
しかし、アプリケーション全体のディレクトリ構成としてAtomic Designをそのまま採用すると、迷いやすくなります。
atoms/
molecules/
organisms/
templates/
pages/
この構成では、実装中に次のような悩みが出ます。
- これはmoleculeなのかorganismなのか
- 業務に依存するコンポーネントをどこに置くのか
- 特定画面でしか使わないものも共通っぽい場所に置くのか
- 画面変更時に関連ファイルが遠くならないか
業務アプリケーションでは、見た目の粒度よりも、どの画面で使うのか、どの業務文脈に属するのかのほうが重要になることがあります。
そのため、Atomic Designを知っていることはよいですが、2026年時点でこの構成にはするべきではないです。本質的ではない分類に支払うコストはありません。
理論ではなく業務に向き合いましょう。
大事なのは、分類の美しさではなく、変更しやすさです。
クリーンアーキテクチャを直輸入しない
この記事で一番言いたいのはここです。
クリーンアーキテクチャの考え方自体は有用です。
依存方向を制御する。 詳細を外側に追い出す。 重要なルールを守る。 テストしやすくする。
これらはフロントエンドでも役に立ちます。
しかし、クリーンアーキテクチャの形をそのままフロントエンドに持ち込む必要はありません。
バックエンドでは、中心に守るべきドメインがあります。 その周囲にユースケース、さらに外側にDBやWeb APIやフレームワークがあります。
一方で、フロントエンドでは画面そのものが大きな関心です。
ユーザーがどの画面で、どの順番で、どの状態を見て、どの操作をするか。 これが設計に強く影響します。
そのため、フロントエンドで無理に「純粋なロジックだけ」を中心に置こうとすると、画面の都合が行き場を失います。
結果として、実態としては画面に依存している処理なのに、名前だけusecaseやdomainになっているコードが生まれます。
これは設計ではなく、分類のための分類です。
フロントエンドに必要なのは、クリーンアーキテクチャのディレクトリ構成を真似ることではありません。
必要なのは、変更の影響範囲を小さくすることです。
Reactに逆らったアーキテクチャは長続きしない
Reactを使う以上、Reactの仕組みに逆らった設計は長続きしません。
たとえば、Reactの外側に独自の巨大なアプリケーション層を作り、コンポーネントはそこから値を受け取って表示するだけ、という構成にしたくなることがあります。
一見すると責務が分離されているように見えます。
しかし、その結果としてReactのState、Props、Hooks、Context、レンダリングの考え方と噛み合わなくなるなら、それはよい設計とは言えません。
Reactでは、状態が変わることでUIが変わります。 コンポーネントはPropsとStateをもとにUIを表現します。 Custom Hookは、状態を持つロジックをコンポーネントの文脈で再利用するために使えます。
この仕組みを活かしたうえで、どこまでをコンポーネントに置き、どこからを切り出すかを考えるべきです。
バックエンド的なレイヤー構成を先に決めて、Reactをそこに押し込めるのではありません。
Reactの思想を理解したうえで、必要な境界を作るべきです。
バックエンドの知識は形ではなく目的を持ち込む
バックエンドエンジニアがフロントエンドを書くとき、バックエンドの知識は役に立ちます。
たとえば、次のような考え方はフロントエンドでも重要です。
- 関心の分離
- 依存方向の整理
- 境界の明確化
- 型による安全性
- 副作用の隔離
- テストしやすさ
- 変更影響の局所化
これらは積極的に使うべきです。
ただし、持ち込むべきなのは形ではなく目的です。
バックエンドで domain/application/infrastructure に分けているから、フロントエンドでも同じように分ける。
バックエンドでRepositoryを作っているから、フロントエンドでも必ずRepositoryを作る。
バックエンドでUsecaseを作っているから、フロントエンドでもすべてUsecaseを通す。
これは目的を見失っています。
なぜその層が必要なのか。 何を守りたいのか。 何の変更に強くしたいのか。 どの複雑さを閉じ込めたいのか。
そこを考えるべきです。
そしてReactを使っているなら、それがReactの思想と噛み合っているかも考える必要があります。
フロントエンドで最低限守りたいこと
最後に、フロントエンドを書くときに最低限守りたいことをまとめます。
- Reactの公式ドキュメントを読む
- Reactの思想に沿って設計する
- Hooksを設計単位として正しく使う
- Stateを増やしすぎない
- Effectを濫用しない
- サーバー由来の状態をグローバル状態に入れない
- グローバル状態は画面をまたいで必要なものに限定する
- 画面に閉じる状態は画面の近くに置く
- 一緒に変わるファイルは近くに置く
- 画面単位の集約を認める
- UIコンポーネントに業務知識を入れすぎない
- APIの都合と画面の都合を混ぜない
- 型は置き場所より境界を意識する
- 早すぎる共通化を避ける
- 層は必要になってから作る
- クリーンアーキテクチャの形をそのまま真似しない
これらを守るだけでも、かなり破綻しにくくなります。
フロントエンド設計では、すべてをきれいなレイヤーに分けることよりも、変更の単位を見極めることが重要です。
そしてReactを使うなら、Reactの仕組みを理解したうえで設計することが重要です。
まとめ
フロントエンドは、バックエンドとは関心の持ち方が違います。
バックエンドでは、DBや業務ルールを中心に、長く守るべきロジックを集約します。 一方でフロントエンドでは、画面、操作、表示、ユーザー体験に強く引っ張られます。
そのため、フロントエンドでは純粋なロジックだけで集約するのではなく、画面単位での集約も必要になります。
さらに、Reactを使うのであれば、Reactの思想に則った設計が必要です。
コンポーネント、Props、State、Hooks、Effectといった仕組みを理解せずに、バックエンドのアーキテクチャだけを持ち込んでも、Reactらしい設計にはなりません。
関心の分離は重要です。 ただし、分離しすぎて画面変更のたびに大量のファイルを横断するようでは意味がありません。
一緒に変わるものは近くに置く。 別の理由で変わるものは分ける。 サーバーの都合と画面の都合を混ぜない。 グローバル状態を増やしすぎない。 共通化は必要になってから行う。 Reactの仕組みに逆らわない。
バックエンドの設計知識は、フロントエンドでも役に立ちます。 しかし、持ち込むべきなのはクリーンアーキテクチャの形ではなく、関心を整理し、変更に強くするという目的です。
フロントエンドは、バックエンドの劣化版ではありません。 画面とユーザー操作を中心にした、別の設計対象です。
その前提を持ち、Reactの公式ドキュメントを読み、Reactの思想に沿って設計することが、破綻しないフロントエンドを書くための第一歩だと思います。
