⚠️ 記事の内容が古くなっているのでご注意ください。
本記事で公開されているユーティリティのメンテナンスを終了したため、リポジトリは非公開としました。
はじめに
こちらは バイセルテクノロジーズAdvent Calendar 2022の25日目の記事です。
昨日は小林さんによる「実務で使った React-Hook-Form × Zod の事例紹介」という記事でした。
こんにちは! テクノロジー戦略本部 開発二部の金子です。
自分が担当するプロジェクトでは、横断的関心事を扱うためにRender Propsを頻繁に利用しています。
本記事では、Render Propsのコールバック地獄を解消するユーティリティーを公開したので、紹介します。
https://github.com/buysell-technologies/react-composegithub.com
対象読者
以下のような課題を抱えている開発者の参考になれば幸いです。
- Render Propsのコールバック地獄で可読性が下がっている
- react-adopt を検討したがTypeScriptが不完全で断念した
Render Props とは?
Reactの公式ドキュメントより引用すると、以下のように書かれています。
“Render Props (render prop)”という用語は、値が関数である props を使って、コンポーネント間でコードを共有するためのテクニックを指します。
Render Propsは、横断的関心事に利用されます。 Render Propsを用いた実装例は以下の通りです。
// こちらからコードを引用: https://dmitripavlutin.com/solve-react-render-props-callback-hell/ import Mouse from "Mouse"; function ShowMousePosition() { return ( <Mouse render={({ x, y }) => ( <div> Position: {x}px, {y}px </div> )} /> ); }
上記の例では、マウスの位置を追跡した結果を子コンポーネントに渡しています。 Render Propsを利用することで、位置情報取得のコンポーネントと、位置情報を受け取って表示するコンポーネントを切り分けることができます。
Render Props の課題
Render Propsは横断的関心事を扱うための優れた手段です。 しかし、複数のRender Propsを利用すると問題が発生します。 Render Propsはコールバック関数を利用するため、複数扱うとネストが深くなっていきます。
複数のRender Propsを用いた例は以下の通りです。
// こちらからコードを引用: https://dmitripavlutin.com/solve-react-render-props-callback-hell/ function DetectCity() { return ( <AsyncCoords render={({ lat, long }) => { return ( <AsyncCity lat={lat} long={long} render={({ city }) => { if (city === null) { return <div>Unable to detect city.</div>; } return <div>You might be in {city}.</div>; }} /> ); }} /> ); } // Somewhere <DetectCity />;
上記のように、AsyncCoords
のコールバック関数が AsyncCity
に渡り、AsyncCity
のコールバック関数が続きます。
このような問題に対して、以下の記事で react-adopt を用いた解決策が述べられています。
従来手法 (react-adopt)
react-adopt は、Render Propsのコールバック関数を解消するユーティリティーを提供します。
先ほどのコードを react-adopt を用いて書き換えると、以下のようになります。
// こちらからコードを引用: https://dmitripavlutin.com/solve-react-render-props-callback-hell/ import { adopt } from "react-adopt"; const Composed = adopt({ coords: ({ render }) => <AsyncCoords render={render} />, city: ({ coords: { lat, long }, render }) => ( <AsyncCity lat={lat} long={long} render={render} /> ), }); function DetectCity() { return ( <Composed> {({ city }) => { if (city === null) { return <div>Unable to detect city.</div>; } return <div>You might be in {city}.</div>; }} </Composed> ); } // Somewhere <DetectCity />;
react-adopt を用いると、Render Propsのコールバック地獄を解消できます。 しかし、react-adopt の利用を検討したところ、様々な課題がありました。
従来手法の課題
react-adopt の課題として以下が挙げられます。
- TypeScriptサポートが不完全
- メンテナンスが止まっている (最終リリースは2018年5月)
TypeScriptサポートの問題点について、以下の2つを例として挙げます。
#24 については、tsconfigの allowSyntheticDefaultImports
を true
にしないと動作しないことが挙げられています。
#21 については、adopt
の引数に渡すコンポーネントの children
がオプショナル型でなければ動作しないことが挙げられています。
上記より、react-adopt を使うためにユーザ側での設定や実装を捻じ曲げなければならないという問題があります。
提案手法
従来手法の問題点を解消するため、弊社では独自のユーティリティーを自作しました。
https://github.com/buysell-technologies/react-composegithub.com
コードを抜粋すると、以下のようなインタフェースになっています。 「Render Propsを持つコンポーネント」の数に応じたシグネチャを定義しています。 これにより、複雑な型の定義を可能にしています。
/** * Composes multiple render prop components */ export function compose< P extends Record<string, unknown>, A extends Record<string, unknown> >( CompA: React.FC<P & { render: React.FC<A> }> ): React.FC<Omit<P, "render"> & { render: React.FC<A> }>; export function compose< P extends Record<string, unknown>, A extends Record<string, unknown>, B extends Record<string, unknown> >( CompA: React.FC<P & { render: React.FC<A> }>, CompB: React.FC<A & { render: React.FC<B> }> ): React.FC<Omit<P, "render"> & { render: React.FC<A & B> }>;
実際の使用例は以下の通りです。
import { compose } from '@buysell-technologies/react-compose'; const Composed = compose( AsyncCoords AsyncCity ); function DetectCity() { return ( <Composed render={({ city }) => { if (city === null) { return <div>Unable to detect city.</div>; } return <div>You might be in {city}.</div>; }} /> ); } // Somewhere <DetectCity />
compose
に AsyncCoords
と AsyncCity
を渡すことで、合成されたコンポーネント (Composed
) が作成されます。合成されたコンポーネント (Composed
) は、渡されたコンポーネントより型推論された型が付与されるようになります。
また、react-adoptと比較して、引数のマッピングが必要ないためより簡単に利用できます。
比較
比較結果は以下の通りです。
従来手法 | 提案手法 | |
---|---|---|
TypeScript サポート | ❌ | ✅ |
使いやすさ | ❌ | ✅ |
多機能 | ✅ | ❌ |
提案手法では、型の問題が解消され、プロパティのマッピングを行わずに使うことができるようになりました。これにより、Render Propsのコールバック地獄を型安全かつ簡単に解決できるようになりました。
しかし、従来手法に対して機能の豊富さという観点では劣っており、今後の機能追加で対応していこうと考えています。
具体的には、従来手法では render
以外の props
を利用できるのに対し、提案手法では render
という名前を使わなければならないというルールがあります。
まとめ
Render Propsのコールバック地獄を解消するユーティリティーを作りました。 react-adopt と比較して、Render Propsのコールバック地獄を型安全かつ簡単に解決できるようになりました。
このパッケージが同様の悩みを抱えている方の助けになればと思っております。
BuySell Technologiesではエンジニアを募集しています。こういった取組に興味がある方はぜひご応募をお願いします!