凝集性を高める Render Hooks パターン
Render Hooks パターンとは
Hooks からコンポーネントを返すパターン。
出自はこちら。
(※ Render Hooks パターンはこちらの記事で独自に命名された設計パターンであり、公式名は存在しない)
(※ また、React 公式はこのパターンを提唱していないし言及もしていない)
例をコードにするとこんな感じ。
// useModal.ts export const useModal = () => { const [isOpen, setIsOpen] = useState(false) const onOpen = useCallback(() => { setIsOpen(true) }, [setIsOpen]) const onClose = useCallback(() => { setIsOpen(false) }, [setIsOpen]) const renderModal: (props: { text: string }) => ReactElement = useCallback( ({ text }) => { return ( <Modal isOpen={isOpen}> <h1>モーダル</h1> <span>{text}</span> <button onClick={onClose}>閉じる</button> </Modal> ) }, [isOpen, onClose] ) return { renderModal, onOpen } } // SampleComponent.tsx (Hooks を呼び出す側のコンポーネント) const SampleComponent = () => { const { renderModal, onOpen } = useModal() return ( <div> <button onClick={onOpen}>モーダルを開く</button> {renderModal()} </div> ) }
Render Hooks パターンのメリット
- 独立可能なコンポーネントの凝集性を高められる
- Hooks は jsx 以外のコンポーネントにまつわるステートやロジックをまとめてくれるが、コンポーネントも一緒に外だししたくなる場合がある
- あくまで設計面でのうれしさ
- パフォーマンス的には影響しない(ただし、注意が必要)
Render Hooks パターンが浮かぶまでの思考
もしこの Render Hooks パターンを採用しなかった場合、 モーダルを利用する SampleComponent の実装は以下のようになる。
// SampleComponent.tsx const SampleComponent = () => { const [isOpen, setIsOpen] = useState(false) return ( <div> <Modal isOpen={isOpen} /> </div> ) }
これはシンプルなコンポーネントなので違和感はないが、ここに「SampleComponent で管理すべきステート」が多数並んでいる重厚なコンポーネントを想像してほしい。
// SampleComponent.tsx const SampleComponent = () => { const [user, setUser] = useState<User>() const [showToaster, setShowToaster] = useState(false) const [bookList, setbookList] = useState<Book[]>([]) // ...多数ステートが並ぶ // Modal のステート const [isOpen, setIsOpen] = useState(false) return ( <div> <div> {showToaster && ( <Toaster message="Success!" onClose={() => setShowToaster(false)} /> )} <div> <h3>User Information</h3> {user ? ( <div> <p>Name: {user.name}</p> <p>Email: {user.email}</p> {/* 他のユーザー情報 */} </div> ) : ( <p>No user data available.</p> )} </div> <div> <h3>Book List</h3> <ul> {bookList.length > 0 ? ( bookList.map((book, index) => ( <li key={index}> {book.title} by {book.author} {/* 他の書籍情報 */} </li> )) ) : ( <p>No books available.</p> )} </ul> </div> </div> <button onClick={() => setIsOpen(true)}>Open Modal</button> <Modal isOpen={isOpen} /> </div> ) }
「isOpen」と言う名前のステートが、一体何を指すのか一見してわかりにくくなる。
もちろんステートの命名を見直しても良いが、あくまでモーダル固有のステートであることが明示されていて欲しくなる。
そこで、Hooks が欲しくなる。
ただし、モーダルの開閉フラグ管理のみ Hooks にまとめるだけでは、 useState をそのまま使うこととなんら変わりない。
なんとなく、「SampleComponent がモーダルに対して行う操作に関する API だけ露出された Hooks」が欲しくなる。
そこで Render Hooks パターンが思いつく。ステートに密に関わる JSX の部分も、Hooks の中に閉じ込め、JSX を Hooks から提供する。
そうすれば、SampleComponent 側で触る必要のある API のみを過不足なく露出させることができる。
例えば、以下の記事がわかりやすい。
ファイルアップロードに必要なロジックは JSX に依存するが、Render Hooks パターンを活用し、JSX ごと Hooks に切り出している。
🚨 注意点
Render Hooks パターンでは、 ReactElement を返す関数を Hooks で提供することが推奨される。
なぜならコンポーネントを返すパターンだと、余計なレンダリングコストがかかってしまう。
❌ コンポーネントを Hooks から提供する形。
const SampleComponent = () => { const { Modal, onOpen } = useModal() return ( <div> <button onClick={onOpen}>モーダルを開く</button> <Modal /> </div> ) }
⭕️ 「ReactElement を返す関数」を Hooks から提供する形。
const SampleComponent = () => { const { renderModal, onOpen } = useModal() return ( <div> <button onClick={onOpen}>モーダルを開く</button> {renderModal()} </div> ) }