はじめに 前回の記事では疎結合にすると影響範囲を限定しやすい、みたいな話を書きましたが、正直それだけだと「じゃあどう分けるん?」ってなると思うんですよね。 今回は、そんな「動くけどなんか嫌なコード」を題材に、関心の分離の視点でどう整理するかをBefore / Afterでまとめてみます。 今回は、 実装としてはちゃんと動く状態 からどう設計を整理するかをBefore / Afterでまとめてみます! どうでもいいんですが、ブログAIが生成すると私っぽくならないのでところどころ使いつつも、結局ほぼ自分で書いてます🙈 関心の分離 一般的な「関心の分離」はレイヤー単位(UI / ロジック / データ)で分ける話ですが、今回はそれをReactのコンポーネント単位に落として考えています。 本質は同じで、「変更理由ごとに責務を分ける」という考え方です。 Before:悪くないけど責務が多い import { useState } from "react" import useSWR from "swr" // AIやりがち。async/awaitで書いて欲しい... const fetcher = (url: string) => fetch(url).then(res => res.json()) export const UserList = () => { const { data, error, isLoading, mutate } = useSWR( '/api/users', fetcher ) //これもAIやりがち。errorでも空配列なるやんやめてくれっていつも言ってる🥹 //正常系の空配列?異常系の空配列?必殺エラーの握りつぶし。 const users = data ?? [] const handleDelete = async (id: string) => { try { await fetch(`/api/users/${id}`, { method: "DELETE" }) mutate() } catch (e) { console.error(e) } } if (isLoading) return <Loading /> if (error) return <Error /> if (users.length === 0) return <Empty /> return ( <div> <h1>ユーザー一覧</h1> <ul> {users.map(user => ( <li key={user.id}> {user.name} <button onClick={() => handleDelete(user.id)}>削除</button> </li> ))} </ul> </div> ) } AIいつもこれやってくるっての今日も出てきたのでコメントしつつそのまま使いました🥹 考察 これ、普通に動くしシンプルなので別に長くもないし、読みにくくもないそのままでもいいんですが責務の観点からって考察してみます。 データ取得 削除処理 状態管理 UI 全部ここでやってるんですよね。 単一責任の法則から逸脱している感じはします。 困りそうなこと 別画面でも同じ「ユーザー削除」やりたくなったらどうなるでしょ?? fetch書いて、再取得書いて、エラーハンドリング書いて、とここでやってることまた全部書くことになる可能性があります。 他にも修正をしたい時でも例えば、削除処理変えたいだけなのに、場合によってはUIまで触ることになったりして、変更の影響範囲が読めなくなるんですよね。 After:責務を分ける hooks(データの責務) import useSWR from "swr" const fetcher = (url: string) => fetch(url).then(res => res.json()) export const useUsers = () => { return useSWR( '/api/users', fetcher ) } UI(表示の責務) export const Page = () => { const { data, error, mutate } = useUsers() const handleDelete = async (id: string) => { try { await fetch(`/api/users/${id}`, { method: "DELETE" }) mutate() // 再取得 } catch (e) { console.error(e) alert("削除に失敗しました") } } if (error) return <Error /> if (!data) return <Loading /> // returnの中で三項演算子で書くべき分離でだとは思いつつ、、 if (data.length === 0) return <Empty /> return ( <div> <h1>ユーザー一覧</h1> <UserList users={users} onDelete={handleDelete} /> </div> ) } 表示コンポーネント type Props = { users: { id: string; name: string }[]; onDelete: (id: string) => void; } export const UserList = ({ users, onDelete }: Props) => { return ( <ul> {users.map(user => ( <li key={user.id}> {user.name} <button onClick={() => onDelete(user.id)}>削除</button> </li> ))} </ul> ) } ただ、より変更に強く、さらに再利用性を高めようと思ったら私は以下のようにするかな、、、と思います🤔 type Props = { users: User[]; } export const UserList = ({ users }: Props) => { return ( <ul> {users.map(user => ( <UserItem key={user.id} user={user} /> ))} </ul> ) } 一覧とアイテムを分離してしまいます。 type Props = { user: User; } export const UserItem = ({ user }: Props) => { const { mutate } = useUsers() const deleteUser = async () => { try { await fetch(`/api/users/${user.id}`, { method: "DELETE" }) mutate() } catch (e) { console.error(e) alert("ユーザーの削除に失敗しました") } } return ( <li key={user.id}> {user.name} <button onClick={deleteUser}>削除</button> </li> ) } こうすることで変更の影響範囲が最小になり「削除ボタンの挙動を変えたい」ときは UserItem だけを見ればいいのでシンプルになりますよね! 職場でイケてないってディスられたけど思想(vs クリーンアーキテクチャ)が違うだけで絶対こっちの方がコンポーネント指向でモダンだと思ってる。。。 UserList は「並べるだけ」、 UserItem は「自分自身の振る舞い」に責任を持つと責務も明確になり関心が綺麗に分離している(関心の分離)と私は感じます。 もし、削除に加えて更新する必要出てきたら、UserItem内に更新処理書いてイベントトリガーになるボタンをおけばいいので簡単ですよね、、! UserItemに責務を閉じておくことで「更新だけ変えたい」というときも他の部分に影響を与えずに済みます。 (UpdateButton/DeleteButton分けたくなるけど) 責務が混ざっていると「更新したいだけなのに一覧や取得ロジックまで触る」みたいなことが起きがち Before / After の違い 観点 Before After データ取得 UIが持つ hooksに分離 削除処理 UIコンポーネントで持つ 責務に応じて配置 整合性 UIが担保 hooksが担保 UIの責務 ロジック混在 表示に集中 再利用性 ほぼ無理 hooksで使い回せる テスト UIと密結合でだるい 分けてテストできる UIの純粋性 外部通信に依存 propsベースでシンプル 補足:コードが長いときの判断 自分はだいたい150行超えたあたりで「これ責務混ざってない?」って疑うようにしてます。というか長いなこれて感じちゃいます! ただ、長い=悪ではないです。 1つのユースケースに集中してるならOK 変更理由が複数あるなら微妙かも 分けたくなるときの注意点 コードが長いと分けたくなります。 でもここでやりがちなのが とりあえずコンポーネント分割 props増える 構造崩れる 逆に分かりづらくなる ってこともあるので注意が必要です。 責務はそのままに、意味のある単位で分けるを意識するといいかもしれません。 UIのセクションで分ける(Form / Table) ロジックはまとめる(hooks) まとめ データの正しさをどこが持つかで考えることも大事で、これが分離しちゃうとバグの温床にもなりうると思います。 ロジックはhooksにまとめると良いですが、ロジックの量にもよるしコードジャンプが増えると可読性が落ちるので分けすぎもNGです。 const [ hoge, setHoge ] = useState<Hoge | null>(null); このためだけにhooksにする必要はないですしね、、 数行のためにカスタムhook作ってやりすぎって指摘されたことありますw UIとロジックを両方持てるのがコンポーネントの良さでもあるので、なんでも分ければいいってもんじゃないですよね、、 おわりに 設計って正しいかじゃなくて 変更に耐えられるかなので ここで見ると一気に判断しやすくなるのではないかと思います!!! 思想や好みも絶対にあるし、そもそも正解はひとつじゃないし、バランスみたいなものもあるし、これくらいなら分けなくても、、の「これくらい」が個々の匙加減なので、観点としては持っておいて絶対はないって意識することも大事かなと思います。