👨‍👩‍👧‍👦

ReactのProp CollectionsとProp Gettersについて

2024/06/12に公開

今回はReactのデザインパターンであるProp CollectionsとProp Gettersについて整理します。書いたり見たことあるようなパターンでしたが、それらに名前があることを最近知りました。パターンの名前を知ることで、自分の中で定着しやすくなりますし、チームメンバーと共通認識としてコミニケーションが取りやすくなりますね。

Prop Collections

Prop Collectionsは、複数のpropsをひとつのオブジェクトにまとめてコンポーネントに渡すデザインパターンです。一つのオブジェクトにまとめることで、コードの重複をなくし再利用しやすくなります。

使用例

例として、Modalコンポーネントに対してProp Collectionsを適用してみます。Modalコンポーネントには複数のpropsを渡す必要があります。Modalコンポーネントの呼び出し元で全てのpropsを書くのは面倒ですね。

<Modal
  isOpen={true}
  disabled={false}
  title="新規登録"
  content="登録しますか?"
  className="modal"
  nextButtonText="登録"
  cancelButtonText="閉じる"
  onNext={()=>{}}
  onCancel={()=>{}}
  onClose={()=>{}}
  size="large"
  hasFooter={true}
  hasHeader={true}
/>

ここでProp Collectionsを利用して、propsをbasePropsとしてまとめます。

const baseProps = {
  isOpen: true,
  disabled: false,
  title: '新規登録',
  content: '登録しますか?',
  className: 'modal',
  nextButtonText: '登録',
  cancelButtonText: '閉じる',
  onNext: ()=>{},
  onCancel: ()=>{},
  onClose: ()=>{},
  size: 'large',
  hasFooter: true,
  hasHeader: true
}

Modalコンポーネントには、basePropsをスプレッド構文で渡すだけで良くなります。見通しが良くなりますね。

<Modal {...baseProps} />

特定のpropsを変更したければ、basePropsをスプレッド構文で展開した後に、特定のpropsだけ上書きすることができます。

<Modal {...baseProps} size="small" />

また、basePropsからsmallModalPropsを定義して、それをModalコンポーネントに渡すこともできます。こうするとsmallModalPropsを別の場所で再利用できます。

const smallModalProps = {
  ...baseProps,
  size: 'small'
}
<Modal {...smallModalProps} />

Prop Getters

Prop Gettersはpropsを返す関数を定義し、それ利用してコンポーネントにpropsを渡すデザインパターンです。Prop Collectionsでプロパティをカスタマイズする際、{...baseProps,size: 'small'}のようにスプレッド構文を利用するとプロパティが上書きされてしまいます。そのため、Prop Collectionsだけでは特定のプロパティのデフォルト処理を維持しつつカスタマイズ処理を追加したい等のケースに対応できませんでした。Prop Gettersを利用すると柔軟にpropsをカスタマイズできます。

使用例

Prop GettersとしてgetModalProps関数を定義し、先ほどのModalコンポーネントに渡すpropsを返します。このままだとpropsを返しているだけで処理をカスタマイズできません。

const getModalProps = (props) => {
  return {
    ...props,
  }
}
<Modal {...getModalProps(baseProps)} />

まず処理を合成するためのcompose関数を定義します。compose関数は、複数の関数を引数として受け取り、それらを配列として順次実行する関数を返します。

const compose =
  (...funcs) =>
  (...args) =>
    funcs.forEach(fn => fn?.(...args))

例えば、以下のようにadd関数とmultiply関数を合成したmathOperations関数を作成します。

const add = (x, y) => console.log(x + y);
const multiply = (x, y) => console.log(x * y);

const mathOperations = compose(add, multiply);

この時、mathOperations関数は以下のようになり、mathOperations関数に引数を与えて実行すると、その引数でadd関数とmultiply関数が順次実行されます。

const mathOperations = (...args) =>[add,multiply].forEach(fn => fn?.(...args));

mathOperations(2,5)
//Output:
// 7
// 10

getModalProps関数に戻ってonNextのデフォルト処理として()=>alert("ご登録ありがとうございます")を定義します。次にreplacementOnNextとして受け取ったonNextの追加処理をcompose関数を利用して合成し、それを返します。

const getModalProps = ({ onNext: replacementOnNext, ...props }) => {
  const defaultOnNext = () => {
    alert('ご登録ありがとうございます')
  }
  return {
    onNext: compose(replacementOnNext, defaultOnNext),
    ...props
  }
}

この状態でonNextに追加したい処理(() => alert('追加処理!'))を渡してあげると、onNext実行時に() => alert('追加処理!')()=>alert("ご登録ありがとうございます")が順番に実行されます。

<Modal {...getModalProps({ ...baseProps, onNext: () => alert('追加処理!') })} />

このようにProp Gettersを使うと、コンポーネントに渡すpropsを纏めつつ特定のプロパティの処理を柔軟に変更することできます。propsとして渡すイベントハンドラにデフォルト処理としてevente.preventDefault()やログ送信処理を仕込みたい時などに使えそうですね。

参考

https://www.oreilly.com/library/view/fluent-react/9781098138707/

Linc'well, inc.

Discussion