大きくなってしまったReactコンポーネントを小さく分割するにはカリー化が効果的だと感じています。
- 少しずつ機能を足していったら大きくなってしまって、次第に分割するのが怖くなってしまった
- そもそも、どう分割したら良いのか分からない
というのが、コンポーネントが大きくなってしまう理由としてあるかと思います。
今回はカリー化とはなにか、そしてどうしてカリー化がReactコンポーネントの分割に有効なのかを共有してみます。保守しやすいReactコンポーネントの実装にお役に立てればと思います。
カリー化とは
まず、Javascriptでシンプルな掛け算をする関数を作ってみたいと思います。
const multiply = (x, y) => { return x * y; } multiply(4, 5); // => 20
この関数をカリー化するとこんな書き方になります。
const multiply = x => y => { return x * y; } multiply(4)(5); // => 20
最終的な結果は同じ 20
になりますが、カリー化の方は
multiply(4)
を呼び出した時点で、return 4 * y
という関数が返りmultiply(4)(5)
を呼び出した時点で、20
が返ります。
つまり、カリー化すると、関数の引数を部分的に固定化できます。
以下のようにすると、1つ目の引数を 5
に固定した関数を作ることができます。
const multiply = x => y => { return x * y; } const multiply2 = multiply(5); console.log(multiply2(6)); // => 30 ( = 5 * 6 ) console.log(multiply2(7)); // => 35 ( = 5 * 7)
Reactコンポーネントでカリー化を使う
以下のような画面をReactで作ってみます。アイテムをクリックすると、名前を更新する関数 updateItem
が呼ばれます。
①分割しない状態
まずは、コンポーネントを1つで分割せず、関数のカリー化もしないで、Javascript / Typescriptコードを書いてみます。
import React, { useState } from 'react'; import axios from 'axios'; const initialCategories = [ { id: 1, name: '学園モノ', items: [ { id: 11, name: '彼女、お借りします' }, { id: 12, name: '魔王学院の不適合者' } ] }, { id: 2, name: '異世界転生', items: [ { id: 21, name: 'Re:ゼロから始める異世界生活' } ] } ] function App() { const [categories] = useState(initialCategories); const updateItem = async (categoryId: number, itemId: number, name: string) => { await axios.put( `http://sample_domain.com/categories/${categoryId}/items/${itemId}`, { name: name } ) } return ( <div className="App"> {categories.map(category => ( <div key={category.id}> <span>{category.name}</span> <ul> {category.items.map(item => ( <li key={item.id} onClick={() => updateItem(category.id, item.id, '更新後の名前')}> {item.name} </li> ))} </ul> </div> ))} </div> ); } export default App;
②カリー化を使わないでコンポーネントを分割
①のコンポーネントの Item
の箇所を別コンポーネントに分割してみたいと思います。
注目すべきは、親コンポーネントからID(categoryId)を子コンポーネントに渡している点です。子コンポーネントであるItemの更新APIの構造的に親コンポーネントのidが必要で、子コンポーネントから関数を呼び出す際、親のIDが必要になっています。
type ItemtProps = { categoryId: number; id: number; name: string; updateFunc: Function; } const Item: React.FC<ItemtProps> = props => { return ( <li onClick={() => props.updateFunc(props.categoryId, props.id, props.name)}> {props.name} </li> ) }; function App() { const [categories] = useState(initialCategories); const updateItem = async (categoryId: number, itemId: number, name: string) => { await axios.put( `http://sample_domain.com/categories/${categoryId}/items/${itemId}`, { name: name } ) } return ( <div className="App"> {categories.map(category => ( <div key={category.id}> <span>{category.name}</span> <ul> {category.items.map(item => ( <Item key={item.id} categoryId={category.id} id={item.id} name={item.name} updateFunc={updateItem} /> )) /* ↑カリー化せずに関数をそのまま渡している */ } } </ul> </div> ))} </div> ); }
③カリー化を使ってコンポーネントを分割
カリー化を使うと、親コンポーネントからID(categoryId)を子コンポーネントに渡さなくて済むようになります。
type ItemtProps = { id: number; name: string; updateFunc: Function; } const Item: React.FC<ItemtProps> = props => { return ( <li onClick={() => props.updateFunc(props.id, props.name)}> {props.name} </li> ) }; function App() { const [categories] = useState(initialCategories); const updateItem = (categoryId: number) => async(itemId: number, name: string) => { await axios.put( `http://sample_domain.com/categories/${categoryId}/items/${itemId}`, { name: name } ) } return ( <div className="App"> {categories.map(category => ( <div key={category.id}> <span>{category.name}</span> <ul> {category.items.map(item => ( <Item key={item.id} id={item.id} name={item.name} updateFunc={updateItem(category.id)} /> )) /* ↑カリー化して、カテゴリーIDを固定にした関数を渡している */ } </ul> </div> ))} </div> ); }
なぜカリー化が効果的なのか
一般的に、コンポートを分割する際には、結合度という観点に着目すると保守性があがります。
コンポーネントの分割方は2通りあり
- 親子(parent/child)関係
<div className="App"> <parent> <child /> </parent> </div>
- 兄弟(sibling)関係
<div className="App"> <sibling /> <sibling /> </div>
のどちらかになります。(HoCのような共通で部品化する場合も親子関係とします。)
兄弟(sibling)関係の場合、情報として各々依存関係がないため、分割を考えた時に結合度という観点では考慮する必要がないです。なので、何の工夫もなくスッキリコンポーネントを分割できることが多いです。
一方、親子(parent/child)関係の場合、親に依存する情報は、親だけにもたせたほうが保守性が高くなります。カリー化の特性を利用し、関数の引数の親だけにある情報を固定化して、子コンポーネントに関数を渡して上げると依存度が低くなり、結合度が低く保つことができます。
参考
Split fat component into smaller flexible components in React - DEV Community 👩💻👨💻