Reactコンポーネントをうまく分割するためのテクニック〜カリー化〜

大きくなってしまった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 が呼ばれます。

f:id:moritamorie:20200928022206p:plain

①分割しない状態

まずは、コンポーネントを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