Writer: otsubo 更新日:2024/11/20
こんにちは、デジナーレ福岡オフィスの大坪です。
皆さんはReactで開発を進めていくときにコンポーネントの汎用性について考えたことはありませんか?
Reactの汎用的なコンポーネント設計は、実装する上での開発効率と保守性を大きく左右する大事な要素となっています。適切な設計なしでは、プロジェクトの規模が大きくなるにつれて保守が困難になり、開発速度の低下を招くことになります。
ではどのように設計されたコンポーネントが汎用性の高いものになっていくのでしょうか?実例を交えて解説していきます。
汎用とは
汎用(はんよう):いろいろの方面に広く用いること。「同一規格の部品を—する」
『デジタル大辞泉』(小学館)より
つまり、汎用性の高いコンポーネントとは、様々なユースケースに対応できる柔軟性を持たせたコンポーネントを指します。
汎用性のあるコンポーネントのメリット
1.コードの再利用性が高まり、開発効率が向上
2.保守性が向上し、バグの修正が容易になる
3.チーム開発における共通認識が形成しやすい
4.将来の機能拡張に柔軟に対応可能
5.テストがしやすくなる
汎用的なコンポーネントの設計
ここでは、テーブルコンポーネントを例にコンポーネントの再利用性を取り上げていきます。
汎用的ではないテーブルコンポーネントの例
では、実際にあなたがプロジェクトで、テーブルコンポーネントを用いて実装を行うことになったとします。
このようなテーブルコンポーネントが既に用意されていました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const Table = ({ headerData, bodyData }: TableProps) => { return ( <table> <thead > <tr> {headerData.map(header => <th key={header.id}>{header.label}</th>)} </tr> </thead> <tbody> <tr > {bodyData.map({body=> ( <td key={body.id}>{body.item}</td>)} </tr> ))} </tbody> </table> ); }; |
これを見てどう思いますか?
上のサンプルではCSSは当てておりませんが、実際は当たっているとして、headerとbodyのデータさえ入れてしまえば簡単にテーブルが作成できて問題なさそうですよね?
しかし、例えば別のページでテーブルコンポーネントを再利用したい、そこではソート機能も付けたい、もしくはテーブルも横から縦位置にしたいとなった時、このコンポーネントはそのまま使えるでしょうか?
このような設計では、新しい要件が出るたびに新しくコンポーネントを実装するか、既存のコンポーネントを修正する必要が出てきます。特に前者のようにコンポーネントが必要になる度に新しく実装していたら、似たようなコードがどんどん増えていき、再利用性が高まるどころか、チーム開発だった場合混乱が生じてしまうでしょう。限定的な使い方でしか再利用できない状態になっています。
コンポーネントの適切な分割
コンポーネントは、小さな粒度で設計する必要があります。例えば子供のころに一度は目にしたであろう「レゴブロック」は、小さな部品がたくさんあります。この小さな部品を組み合わせて、家を作ったり、恐竜や城を作ったりするのですが、全てはいくつかのパターンに分類された同じ小さな部品の塊でできています。
先ほどのテーブルコンポーネントはレゴブロックで組み立てたものを接着剤で固めたようなものでした。これでは柔軟な組み替えができません。
では、小さな粒度で設計するとどうなるのでしょうか。
画像のようにコンポーネントの分割を意識して設計してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
// HeaderCell.tsx const HeaderCell = ({ label }: HeaderCellProps) => ( <th style={{ padding: '8px', ... }} > {label} </th> ) // HeaderRow.tsx const HeaderRow = ({ children }: HeaderRowProps) => ( <tr style={{ height: '40px', ... }} > {children} </tr> ) // TableCell.tsx const TableCell = ({ value }: TableCellProps) => ( <td style={{ padding: '8px' }} > {value} </td> ) // TableRow.tsx const TableRow = ({ children }: TableRowProps) => ( <tr style={{ height: '40px' }} > {children} </tr> ) |
このように各コンポーネントでは最低限のデザインのみを担保している状態です。この小さなコンポーネントを組み合わせることで、さらに大きなコンポーネントを柔軟に実装できます。
コンポーネントの設計では、以下の点に注意を払う必要があります
1.レイアウトに関する最小限のデザインのみを指定する
2.ロジックはコンポーネント内に書かず、propsで渡す
3.propsは必要最小限にとどめ、過剰な機能は避ける
特にpropsの設計は重要です。既に仕様や実装段階で必要な機能が決まっているものは積極的に組み込んでよいですが、必須でない機能はオプショナルにすることで柔軟性が増します。ただし、「将来的にこの機能が必要かもしれない」という予測に基づいてpropsを追加することは避けましょう。
propsを過剰に渡してしまうと、以下のような問題が発生します
1.コンポーネントの複雑性が増す
2.実際にpropsが使用されているかの判別が困難に
3.メンテナンス時に 使用されていないpropsの影響を考慮する必要がある
このように設計されたコンポーネントを組み合わせることで、様々なユースケースに柔軟に対応できる実装が可能になります。
では、実際にこれらの設計原則に従って作られたコンポーネントを使用してみましょう。
以下は、シンプルなユーザーテーブルの実装例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const UserTable = () => ( <TableBase> <HeaderRow> <HeaderCell label="Name" /> <HeaderCell label="Age" /> </HeaderRow> {users.map(user => ( <TableRow key={user.id}> <TableCell value={user.name} /> <TableCell value={user.age} /> </TableRow> ))} </TableBase> ); |
この例では、基本コンポーネントを組み合わせることで、シンプルながら拡張性の高いテーブルを実現しています。ソート機能やフィルター機能が必要な場合も、これらのコンポーネントをベースに実装できます。
おわりに
汎用的なコンポーネント設計は、開発効率と保守性の向上に大きく貢献します。小さなコンポーネントを組み合わせる設計アプローチを採用することで、プロジェクトの成長に合わせて柔軟に対応できる基盤の実装が可能です。
本記事で紹介した設計の考え方を、是非実際のプロジェクトや個人開発の場で活用してみてください!
実装を行っていくうえで適切な粒度のコンポーネント、再利用しやすいコンポーネントの理解というは、まずは手を動かしてみないと分からないものなのです。