はじめに
この記事は「ウィルゲート Advent Calendar 2024」の 18 日目の記事です! adventar.org
こんにちは!ウィルゲート開発グループの中尾(のまど )です!
弊社のオンライン記事編集サービスEditorUの改修を中心に、フロントエンドからバックエンドまで広くWeb開発を担当しています。
今回EditorUで新規画面の設計・実装を担当し、その過程でCompound Components Patternを導入しました。
本記事では、導入に至った背景や具体的な実装内容、導入後に得られた成果についてご紹介します。
Compound Components Patternとは?
Compound Components PatternはReactのデザインパターンの一つで、親コンポーネントが子コンポーネントを構成要素として提供し、子コンポーネントが連携して一つの機能を実現する設計手法です。
特にタブやモーダルのようなUIを構築する際に、子コンポーネントを親コンポーネント内で柔軟に組み合わせられるため、機能をシンプルかつカスタマイズしやすくなります。
具体例
モーダルの例
function Modal({ children }) { return <div className="modal">{children}</div>; } Modal.Header = function Header({ title }) { return <div className="modal-header">{title}</div>; }; Modal.Body = function Body({ children }) { return <div className="modal-body">{children}</div>; }; Modal.Footer = function Footer({ actions }) { return <div className="modal-footer">{actions}</div>; }; // 使用例 <Modal> <Modal.Header title="サンプルモーダル" /> <Modal.Body> <p>モーダルの内容です。</p> </Modal.Body> <Modal.Footer actions={<button>閉じる</button>} /> </Modal>;
このように、親コンポーネント(Modal)が子コンポーネント(Header、Body、Footer)を柔軟に組み合わせることで、一つの機能を効率的に実現します。
コンポーネントの依存関係
メリット
- 柔軟性の向上: 子コンポーネントの配置や構成を自由に変更できる。
- 再利用性の向上: 子コンポーネントが独立しており、異なるコンポーネント間でも再利用しやすい。
- シンプルな設計: 親コンポーネントが状態や振る舞いを強制せず、子コンポーネントは役割に集中できる。
導入の背景と課題
EditorUでは立ち上げ期から6年が経ち、その間に幾度もリアーキテクトを経て以下の設計方針で進んでいました。
- コンポーネント分割: Atomic DesignとfeatureDirectoryの混在
- Atomic Design: コンポーネントを「Atoms(最小単位)」「Molecules(組み合わせ)」「Organisms(画面部品)」などに分割して設計する手法。
- featureDirectory(package by feature): 特定の機能単位でディレクトリを構成する設計手法。
- 状態管理: Reduxを使用したre-ducks Pattern。ただし、主にAPI通信とそのレスポンス管理が中心
- Redux: 状態管理ライブラリで、複数のコンポーネント間で状態を一元管理する仕組み。
当初はこれで問題ありませんでしたが、開発が進むにつれ次の課題が顕在化しました。
課題 | 内容 |
---|---|
Propsのバケツリレー | 中間コンポーネントを経由するデータ伝達が増え、コードの可読性が低下。 |
Reduxのオーバーヘッド | API通信中心に運用していたが、あらゆるコンポーネントから呼ばれることで結果神クラスのようになり、依存関係が追いづらくなっていた。 またUIコンポーネントの責務が不明確になっていた。 |
Formikの制約 | 当時Formik をAPI通信に使用していたが、バージョンが古くuseFormikContext() が利用できなかった。そのため状態共有の工夫が必要だった。 |
Propsのバケツリレーとは: 親コンポーネントから子孫コンポーネントへデータを渡す際に中間コンポーネントを経由し、コードが複雑化する状態です。
ParentComponent └── ChildComponentA └── ChildComponentB └── ChildComponentC └── Props
これらの課題を解決するため、Compound Components Patternを導入しました。
ディレクトリ構造の比較
Before: Compound Components導入前
src/ └── コンポーネントA/ └── Form/ ├── component.tsx ├── container.ts ├── index.ts ├── types.ts ├── Modal/ │ ├── index.tsx │ └── types.ts ├── InputField/ │ ├── index.tsx │ └── types.ts └── Table/ ├── index.tsx ├── types.ts └── Row/ ├── index.tsx └── types.ts
依存関係
After: Compound Components導入後
src/ └── コンポーネントA/ ├── Modal/ │ ├── component.tsx │ ├── container.ts │ ├── index.ts │ └── types.ts ├── Form/ │ ├── component.tsx │ ├── container.ts │ ├── index.ts │ ├── provider.tsx │ └── types.ts ├── InputField/ │ └── index.tsx └── Table/ ├── Row/ │ ├── index.tsx │ └── types.ts ├── component.tsx ├── container.ts ├── index.ts └── types.ts
依存関係
Before | After |
---|---|
状態管理・通信の責務が分散 | 通信ロジックをFormに集約し、子コンポーネントは表示専用化 |
Propsのバケツリレーで管理が煩雑化 | ContextAPIを導入し、直接的な状態管理が可能に |
Formikの工夫とContextAPI
Compound Components Patternの導入後、Formikの状態管理を改善するためContextAPIを活用しました。
実装例
import React, { createContext, useContext } from 'react'; // Contextの作成 const FieldValueContext = createContext(null); export const useFieldValue = () => { const context = useContext(FieldValueContext); if (!context) { throw new Error('useFieldValueはFieldValueProvider内で使用してください'); } return context; }; export const FieldValueProvider = ({ setFieldValue, children }) => { return ( <FieldValueContext.Provider value={setFieldValue}> {children} </FieldValueContext.Provider> ); };
使用例
FormikのsetFieldValueをContext経由で共有することで、子コンポーネント間の状態更新が容易になりました。
export const コンポーネントA() => { return ( <Formik initialValues={initialValues} onSubmit={handleSubmit}> {({ setFieldValue }) => ( <FieldValueProvider setFieldValue={setFieldValue}> <CustomFormComponent /> </FieldValueProvider> )} </Formik> ) }
この工夫により、Compound Components Patternと組み合わせてフォーム状態管理がシンプルかつ柔軟になり、複雑なデータ伝達の課題を解消しました。
導入後に感じたメリット
コードの改善点
責務の明確化: 通信ロジックをFormコンポーネントに集約し、子コンポーネントは表示専用に設計。
Propsの最適化: ContextAPIの活用により、Propsのバケツリレーを解消。
状態管理の改善: FormikとCompound Componentsの組み合わせでシンプルな状態管理を実現。
おわりに
本記事では、EditorUにおけるCompound Components Patternの導入事例を紹介しました。コードの保守性や開発効率が大幅に向上し、UI設計が柔軟でシンプルになったと感じています。
複雑なUI設計やPropsのバケツリレーに悩んでいる方は、ぜひCompound Components Patternの導入を検討してみてください。
最後までお読みいただき、ありがとうございました!
「ウィルゲート Advent Calendar 2024(https://adventar.org/calendars/10272)」、翌日ははるみんさんによる「デザインとコーディングの連携をスムーズに!エンジニアが開発で使えるFigma無料版おすすめ機能 」です。乞うご期待!