今回は React Hooks が普及する前の話。
主に関数コンポーネントに機能を付与することに使われる HOC を取り扱う Recompose についてです。
※2020/10/03追記
この記事は Zenn に転載しました。
以降の更新は Zenn の方で行っていきますので、最新状態はそちらでご確認ください。
※2022/01/24追記 あくまでライブラリの記事であるとわかりやすくするために、記事タイトルを変更しました(旧:React入門 ~Recompose編~)
Recomposeとは?
前提知識として、Reactには高階コンポーネント (Higher-Order Component)という概念があります(通称HOC)
具体的には、あるコンポーネントを受け取って、それに機能を付与した新規のコンポーネントを返すような関数のことを指します。
これまでの記事でもHOCと書いていたものは、これのことでした。
HOCを利用することで、HOC側にロジック、コンポーネント側はビューといったように責務を分離させたり、ロジック部分を複数のコンポーネントで再利用したりといったことができます。
また、stateやライフサイクルを持てない関数コンポーネントに、これらの機能を付与することもできます。
RecomposeはこのHOCを扱うユーティリティ的なライブラリです。
以前は多くの方々に利用されていましたが、React 16.8で追加されたReact Hooksにより、React本体だけでも同様のことができるようになりました。
そのためライブラリの更新はすでに止まっており、今後は使われなくなっていくのではないかと思いますが、業務で使用する機会があったので今回記事に書くことにしました。
インストール
$ yarn add recompose
今回使用するバージョンは0.30.0
です。
使い方
以下、記載しているコードは公式サンプルのコードを元にしています。
基本的な使い方
HOCを使う場合、主に以下のような書き方をします。
const enhance = HOC(Component);
ここでの定数名、HOC名、コンポーネントはあくまで仮のものですが、その中身としては、以下のようなものになります。
- enhance:機能が追加され、新たに作成されたコンポーネント
- HOC:引数のコンポーネントに何らかの処置を施す関数
- Componet:元となるコンポーネント
元となるコンポーネントをHOCでラップするイメージですね。
これを踏まえたうえで、Recomposeが提供するHOCを使用した例がこちら。
import React from 'react';
import { withState } from 'recompose';
const enhance = withState('counter', 'setCounter', 0);
const Component = enhance(({ counter, setCounter }) => {
return (
<div>
<p>カウンター: {counter}</p>
<button onClick={() => setCounter(n => n + 1)}>Increment</button>
<button onClick={() => setCounter(n => n - 1)}>Decrement</button>
</div>
);
});
export default Component;
上記ではstateを扱えるようにするwithState
で、ビュー側であるコンポーネントをラップするようになっています。withState
で定義したstateとstateを更新する関数はpropsに渡されるので、そこから使用できます。
複数のHOCを併用するやり方
Recomposeではいろんな種類のHOCが提供されているので、それらを併用して使用したい場合もあると思います。
その場合、普通にやろうとすると以下のようになります。
const enhance = HOC1(HOC2(Component));
実際のコードにするとこんな感じです。
withState
とwithHandlers
の組み合わせ。
import React from 'react';
import { withState, withHandlers } from 'recompose';
const stateEnhance = withState('counter', 'setCounter', 0);
const handleEnhance = withHandlers({
incrementCounter: props => () => {
props.setCounter(v => v + 1)
},
decrementCounter: props => () => {
props.setCounter(v => v - 1)
},
});
const Component = stateEnhance(handleEnhance((
{ counter, incrementCounter, decrementCounter }) => {
return (
<div>
<p>カウンター: {counter}</p>
<button onClick={incrementCounter}>Increment</button>
<button onClick={decrementCounter}>Decrement</button>
</div>
);
}));
export default Component;
この例では2つのHOCなのでまだいい方ですが、これがさらに数が増えるとラップする数が増えて可読性が落ちてえらいことに...。
また、先に書いたHOCから実行されるので、上記のようにwithHandlers
のなかでwithState
で定義したものを使用している場合は、withState
の方を先に書く必要があります。
この可読性の問題を解消するためにはcompose
という関数を使うとよいです。
compose
を使うと、以下のように書くことができます。
const enhance = compose(HOC1, HOC2)(Component);
実際のコードだとこんな感じです。
複数のHOCをまとめて書けるので、すっきりしますね。
import React from 'react';
import { compose, withState, withHandlers } from 'recompose';
const enhance = compose(
withState('counter', 'setCounter', 0),
withHandlers({
incrementCounter: props => () => {
props.setCounter(v => v + 1)
},
decrementCounter: props => () => {
props.setCounter(v => v - 1)
}
})
)
const ComposeComponent = enhance(
({
counter,
incrementCounter,
decrementCounter
}) => {
return (
<div>
<p>カウンター:{counter}</p>
<button onClick={incrementCounter}>Increment</button>
<button onClick={decrementCounter}>Decrement</button>
</div>
)
})
export default ComposeComponent;
Reduxとの併用(※2020/05/06追記)
Reduxと併用したい場合は、react-reduxのconnect
を使うとよいです。
このconnect
もHOCが使われているそうで、同様にcompose
で他のHOCとまとめて書くことができます。
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { bindActionCreators } from 'redux';
import { incrementOn, decrementOn } from '../../Actions/Counter';
const enhance = compose(
connect(
state => ({
counter: state.counter
}),
dispatch => ({
actions: bindActionCreators({ incrementOn, decrementOn }, dispatch)
})
)
)
const ComposeComponent = enhance(
({ counter, actions }) => {
return (
<div>
<p>カウンター:{counter}</p>
<button onClick={actions.incrementOn}>Increment</button>
<button onClick={actions.decrementOn}>Decrement</button>
</div>
)
})
export default ComposeComponent;
HOCの種類
数が多いので一部のみ紹介。
無駄な再レンダリングを抑制する:pure(※2020/05/06追記)
propsが変更されない限り、コンポーネントが更新されないようにします。
変更されたことを検知するロジックとしてはshallowEqual
が使われているようです。
import React from 'react';
import { pure } from 'recompose';
const enhance = pure;
const Component = enhance(
() => {
return (
<div>
<p>pure test</p>
</div>
);
});
export default Component;
propsを置き換える:mapProps
現在のpropsを関数が返すものに置き換えます。
mapProps
実行後は、num1
とnum2
はなくなり、sum
というpropsのみに置き換えられます。
使用例(propsに num1={10}、num={20} 指定)
import React from "react";
import { mapProps } from 'recompose';
const enhance = mapProps(props => {
return {
sum: props.num1 + props.num2
}
})
const Component = enhance(({ num1, num2, sum }) => {
return (
<div>
<p>{num1 ? num1 : 'propsなし'}</p>
<p>{num2 ? num2 : 'propsなし'}</p>
<p>{sum}</p>
</div>
);
});
export default Component;
propsを追加する:withProps
現在のpropsに関数が返すものを追加します。
使用例(propsに num1={10}、num2={20} 指定)
import React from "react";
import { withProps } from 'recompose';
const enhance = withProps(props => {
return {
sum: props.num1 + props.num2
}
})
const Component = enhance(({ num1, num2, sum }) => {
return (
<div>
<p>{num1 ? num1 : 'propsなし'}</p>
<p>{num2 ? num2 : 'propsなし'}</p>
<p>{sum}</p>
</div>
);
});
export default Component;
指定したpropsが変更された時のみ、propsを追加する:withPropsOnChange
基本的にはwithProps
と同じであるものの、こちらは指定したpropsが変更された場合のみpropsの追加が行われます。
使用例
import React from "react";
import { withPropsOnChange } from 'recompose';
const enhance = withPropsOnChange(['num'], props => {
return {
sum: props.num * 2
}
})
const Component = enhance(({ num, sum }) => {
return (
<div>
<p>{num ? num : 'propsなし'}</p>
<p>{sum ? sum : 'propsなし'}</p>
</div>
);
});
export default Component;
propsのデフォルト値を指定する:defaultProps
React本体で使用できるdefaultProps
プロパティとほぼ同じことができるものの、厳密には違う模様。
なお、コンポーネント呼び出し時に対象のpropsが指定されていた時は、そちらが優先して使われます。
使用例(propsに指定なし)
import React from "react";
import { defaultProps } from 'recompose';
const enhance = defaultProps({
text: 'default'
})
const Component = enhance(({ text }) => {
return (
<div>
<p>{text}</p>
</div>
);
});
export default Component;
propsの名前を変更する:renameProp
第1引数の名称のpropsを第2引数の名称にリネーム。
このHOC1つにつき、1つしか書けません。
使用例(propsに text="テスト" を指定)
import React from "react";
import { renameProp } from 'recompose';
const enhance = renameProp('text', 'renameText');
const Component = enhance(({ text, renameText }) => {
return (
<div>
<p>{text ? text : 'propsなし'}</p>
<p>{renameText ? renameText : 'propsなし'}</p>
</div>
);
});
export default Component;
一度に複数のpropsの名前を変更する:renameProps
renamePropsの複数版。
使用例(propsに text="テスト" num={10} を指定)
import React from "react";
import { renameProps } from 'recompose';
const enhance = renameProps({
'text': 'renameText',
'num': 'renameNum'
});
const Component = enhance(({ text, renameText, num, renameNum }) => {
return (
<div>
<p>{text ? text : 'propsなし'}</p>
<p>{renameText ? renameText : 'propsなし'}</p>
<p>{num ? num : 'propsなし'}</p>
<p>{renameNum ? renameNum : 'propsなし'}</p>
</div>
);
});
export default Component;
平坦化したpropsを追加する:flattenProp
あくまで平坦化したpropsを追加なので、平坦化の元になったpropsもそのまま残ります。
使用例(propsに obj={{'a': 'A', 'b': 'B', 'c': 'C'}} を指定)
import React from "react";
import { flattenProp } from 'recompose';
const enhance = flattenProp('obj');
const Component = enhance(({ obj, a, b, c }) => {
return (
<div>
<p>{obj.a}・{obj.b}・{obj.c}</p>
<p>{a}</p>
<p>{b}</p>
<p>{c}</p>
</div>
);
});
export default Component;
stateを追加する:withState
第1引数にstate名、第2引数にstateを更新する関数、第3引数にデフォルト値を指定します。
stateを更新する関数を使用する際の引数は、ただ設定値だけを渡すほかに、現在の値を引数とした処理を記述することも可能です。
デフォルト値の指定に関しても、単純な値のほかにコールバック関数も指定できます。
使用例(基本的な使い方の例と同じです)
import React from 'react';
import { withState } from 'recompose';
const enhance = withState('counter', 'setCounter', 0);
const Component = enhance(({ counter, setCounter }) => {
return (
<div>
<p>カウンター: {counter}</p>
<button onClick={() => setCounter(n => n + 1)}>Increment</button>
<button onClick={() => setCounter(n => n - 1)}>Decrement</button>
</div>
);
});
export default Component;
関数ハンドラーを追加する:withHandlers
定義した関数ハンドラーにはpropsが渡されるので、その値を使った処理を記述することができます。
使用例(複数のHOCを併用するやり方の例と同じです)
import React from 'react';
import { compose, withState, withHandlers } from 'recompose';
const enhance = compose(
withState('counter', 'setCounter', 0),
withHandlers({
incrementCounter: props => () => {
props.setCounter(v => v + 1)
},
decrementCounter: props => () => {
props.setCounter(v => v - 1)
}
})
)
const ComposeComponent = enhance(
({
counter,
incrementCounter,
decrementCounter
}) => {
return (
<div>
<p>カウンター:{counter}</p>
<button onClick={incrementCounter}>Increment</button>
<button onClick={decrementCounter}>Decrement</button>
</div>
)
})
export default ComposeComponent;
※プレビューはwithStateの例と同じなので省略
stateと関数ハンドラーを追加する:withStateHandlers
stateと、そのstateに関する関数ハンドラーをまとめて定義したい時は、こちらを使用。
使用例(propsに指定なし)
import React from 'react';
import { withStateHandlers } from 'recompose';
const enhance = withStateHandlers(
({ initialCounter = 0 }) => ({
counter: initialCounter,
}),
{
incrementOn: props => () => ({
counter: props.counter + 1,
}),
decrementOn: props => () => ({
counter: props.counter - 1,
}),
resetCounter: (_, { initialCounter = 0 }) => () => ({
counter: initialCounter,
}),
}
)
const ComposeComponent = enhance(
({ counter, incrementOn, decrementOn, resetCounter }) => {
return (
<div>
<p>カウンター:{counter}</p>
<button onClick={incrementOn}>Increment</button>
<button onClick={decrementOn}>Decrement</button>
<button onClick={resetCounter}>Reset</button>
</div>
)
})
export default ComposeComponent;
ローカルReducerを追加する:withReducer(※2020/05/06追記)
Actionを発行して、そのタイプに応じた状態の更新を行うReduxライクな処理を書くことができます。
Reduxを使うまでではないが、より複雑な状態管理を行いたいという時に向いています。
import React from 'react';
import { compose, withReducer, withHandlers } from 'recompose';
const counterReducer = (count, action) => {
switch (action.type) {
case 'INCREMENT':
return count + 1
case 'DECREMENT':
return count - 1
default:
return count
}
}
const enhance = compose(
withReducer('counter', 'dispatch', counterReducer, 0),
withHandlers({
incrementOn: props => () => props.dispatch({type: 'INCREMENT'}),
decrementOn: props => () => props.dispatch({type: 'DECREMENT'}),
})
);
const ComposeComponent = enhance(
({ counter, incrementOn, decrementOn }) => {
return (
<div>
<p>カウンター:{counter}</p>
<button onClick={incrementOn}>Increment</button>
<button onClick={decrementOn}>Decrement</button>
</div>
);
});
export default ComposeComponent;
※プレビューはwithStateの例と同じなので省略
ライフサイクルを追加する:lifecycle
componentDidMount
をはじめとした、ライフサイクルを追加できます。
使用例
import React from 'react';
import { compose, withState, lifecycle } from 'recompose';
const enhance = compose(
withState('text', 'setText', ''),
lifecycle({
componentDidMount() {
this.props.setText('initial');
}
})
)
const ComposeComponent = enhance(({ text }) => {
return (
<div>
<p>{text}</p>
</div>
)
})
export default ComposeComponent;
Recomposeは業務で使用した経験があったので、記事に起こすのそんなに難しくないと思いきや、思いのほか機能が多かったです(苦笑)
自分が使った機能はほんの一部にすぎなかったようです。
またもや長くなって力尽きたので、とりあえず一部機能のみ紹介にしました。
気が向いたら他の機能も書くかも?
↓
2020/05/06 一部追記しました。
今後使われなくなっていくと思われるライブラリではありますが、保守案件とかで触れる機会があるかもしれないので、さらっとした知識は一応持っておきたいですね。