React.js/redux のコンポーネントの機能拡張、ストアの機能拡張のよくみかけるアプローチ
目次
- 高階関数とは
- 関数合成とは
- HOC(高階コンポーネント) とは
- StoreEnhancer とは
自己紹介
おもに以下の技術を使って仕事をしています。
- React.js/redux
- Rails
- Node
想定スキルレベル
- fulx についてある程度知っている
- React.js のチュートリアルで TODO アプリを作ったことがある
- redux/fluxフレームワークのチュートリアルで TODO アプリを作ったことがある
- redux の公式レポジトリの examples を参考にしている
目的など
- React.js/redux の機能拡張では、高階関数や関数合成というテクニックが使われている。
- 高階関数や関数合成の基本を振り返り、実際に React.js/redux でどう使われているのか眺めてみる。
- Reactアプリを作ったり/ライブラリのコードを読んだりするときの基礎知識とする。
- 意味の話はしない。カタチの話をする。
高階関数とは
- 関数を引数にとる関数
- 関数を返す関数 <- 今回はこちら
関数を返す関数(基本)
例:
function hoge() {
var foo = 'aaa';
return function(arg) {
return foo + arg;
}
}
var fuga = hoge();
fuga('Z');
//=> aaaZ
メリット:
-
返した関数が行う内容を先延ばしにできる(処理の委譲)。
この例では、'aaa' のあとに 何を連結するかについて、hoge() が呼びだされた時点でわからなくてもよい。そういうのが便利なときもある。 -
"クロージャ" でネット検索してみよう
関数を返す関数(基本)の補足
hoge()() とは?
- hoge()() は (hoge())() という構造
- hoge() が関数を返す関数であると想定して、その戻り値として戻ってきた関数を、一番右側の()で呼び出す。
関数を返す関数の例
function hoge() {
var foo = 'aaa';
return function(arg) {
return foo + arg;
}
}
hoge()('Z');
//=> aaaZ
関数を返す関数(redux ミドルウェア)1
アンスコ区切りのAPIレスポンスを lowerCamelCase に変換するミドルウェア
import { camelizeKeys } from 'humps';
import * as ActionTypes from '../actions/action_types';
export default (store) => (next) => (action) => {
for (const key in ActionTypes) {
if (APIコールが成功したと通達するアクションを選別する条件) {
action.payload = camelizeKeys(action.payload);
}
}
next(action);
};
これは『「関数を返す関数」を返す関数』
関数を返す関数(redux ミドルウェア)2
要点を抜き出すと、
(store) => (next) => (action) => {
next(action);
};
関数を返す関数(redux ミドルウェア)3
これは以下と同じ。
(store) => {
return (next) => {
return (action) => {
next(action);
}
}
};
関数を返す関数(redux ミドルウェア)4
これは以下と同じ。
function middleware(store) {
return function (next) {
return function (action) {
next(action);
};
};
};
middleware(store) した時点で、middleware(store) に対する next と action を未確定のままにできる/可変にできるというメリットがあるのではないかと思いました。
関数を返す関数(redux ミドルウェア)5
補足情報: reduxのcomposeとapplyMiddlewareとenhancer の applyMiddleware() の解説
関数合成とは
- 雑にいうと、関数呼び出しの連鎖を読み書きしやすくしたやつ
関数Aの戻り値を関数Bの引数としてわたして関数Bを呼び出して、
その関数Bの呼び出しの戻り値を、関数Cの引数としてわたして関数Cを呼び出して、
その関数Cの呼び出しの戻り値を、関数Dの引数としてわたして関数Dを呼び出す。
関数Aに役割Aがあり、関数Bに役割Bがあり、関数Cに役割Cがあり、関数Dに役割Dがあると、
上のような関数呼び出しの連鎖が行われることで、
関数Aの引数で渡した値に対し、役割A,B,C,Dが遂行された値が、関数Dの戻り値として取得できる。
言い換えると、関数という単位で役割分担したコードがかける。
関数呼び出しを何度も行うことで機能拡張ができる。
関数合成の例1
以下の3つの関数を定義
- 引数の文字列の末尾に A を連結する関数 addA
- 引数の文字列の末尾に B を連結する関数 addB
- 引数の文字列の末尾に C を連結する関数 addC
関数合成の例2
function addA (arg) {
return arg + 'A';
}
function addB (arg) {
return arg + 'B';
}
function addC (arg) {
return arg + 'C';
}
関数合成の例3
'Z' に対し、CBA を連結して、'ZCBA' を作りたい。
addC('Z') した結果に対して、addB を呼び出して、
その結果に対して addA を呼び出す。
関数合成の例4
addA(addB(addC('Z')));
//=> 'ZCBA'
addA(addB('ZC'));
//=> 'ZCBA'
addA('ZCB');
//=> 'ZCBA'
関数合成の例5
addA(addB(addC('Z'))) は読み書きしにくい。
lodash.js の _.compose を使って書くと、次のようになる。
_.compose(addA, addB, addC)('Z'))
//=> 'ZCBA'
この _.compose が、関数合成する関数。
関数合成する関数があるおかげで、
関数呼び出しの連鎖を想定した設計が行いやすくなる。
なぜなら、
addA(addB(addC('Z'))) と書くよりも、
コードの読み書きがしやすいから。
関数合成の例(redux)
redux の compose
compose(...functions)
Composes functions from right to left.
This is a functional programming utility, and is included in Redux as a convenience.
You might want to use it to apply several store enhancers in a row.
関数合成のその他の例
koa の compose
Middleware composition utility
JS での機能拡張の定番アプローチだと思われる。
HOC とは
- Higher Order Components = 高階コンポーネント
- ほかのコンポーネントをラップするコンポーネントのこと。
- ラッパーコンポーネントによって、ラップされる側のコンポーネントをカスタマイズできる
- コンポーネントを返す関数として実装される
HOC の例1 (postd で翻訳されていたブログ記事から)
function PPHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
const props = Object.assign({}, this.props, {
user: {
name: 'Fran',
email: 'franleplant@gmail.com'
}
})
return <WrappedComponent {...props}/>
}
}
}
class Example extends React.Component {
render() {}
}
const EnhancedExample = PPHOC(Example)
EnhancedExample で this.props.user が読み込める。
HOC の例2-1
ログイン画面のコンテナの実装
class LoginContainer extends Component {
props: {};
render() {}
}
LoginContainer = reduxForm({
form: 'LoginContainerForm',
validate,
})(LoginContainer);
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer);
HOC の例2-2
この reduxForm のところ、
LoginContainer = reduxForm({
form: 'LoginContainerForm',
validate,
})(LoginContainer);
HOC の例2-3
reduxForm() は、きっとこんなふうになっているんだろうと予想できる。必要になったらコードを読む。
function reduxForm(WrappedComponent) {
return class すごいWrappedComponent extends React.Component {
// form に関するなんらかの便利カスタマイズ
render() {
//
return <WrappedComponent {...props}/>
}
}
}
HOC の例2-4
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer);
も、connect()() という ()() があるので、connect が高階関数なのだとわかる。
LoginContainer に、mapStateToProps, mapDispatchToProps を使ったなんらかの機能が追加された、すごい LoginContainer が connect()() で戻り値となって取得できるのだろう、ということ。
補足情報
gaearon さんによるconnect のコード解説がされた gist があった。
// connect() is a function that injects Redux-related props into your component.
// You can inject data and callbacks that change that data by dispatching actions.
StoreEnhancer とは1
// https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer
type StoreEnhancer = (next: StoreCreator) => StoreCreator
A store enhancer is a higher-order function that composes a store creator
to return a new, enhanced store creator.
This is similar to middleware in that it allows you to alter the store
interface in a composable way.
Store enhancers are much the same concept as higher-order components
in React, which are also occasionally called “component enhancers”.
StoreEnhancer とは2
StoreCreator とは
type StoreCreator = (reducer: Reducer, preloadedState: ?State) => Store
A store creator is a function that creates a Redux store.
つまり、「ストアを返す関数」を引数にとって、何かして、「ストアを返す関数」を返すのが、StoreEnhancer。
「何かして」というところに、その StoreEnhancer の作者のやりたかったことを書く。
StoreEnhancer とは3
作り方
// https://github.com/reactjs/redux/issues/922#issuecomment-150011657
const storeEnhancer = createStore => (reducer, initialState) => {
const store = createStore(reducer, initialState);
//
// store にこの StoreEnhancer ならではの何かをする
//
return store;
};
StoreEnhancer とは4
登録のしかた
createStore の第三引数で渡す
createStore(reducer, [preloadedState], [enhancer])
Arguments
[enhancer] (Function): The store enhancer. You may optionally specify
it to enhance the store with third-party capabilities such as middleware,
time travel, persistence, etc. The only store enhancer that ships with
Redux is applyMiddleware().
StoreEnhancer とは5
利用例: redux-persist
store/configure_store.js の例
const enhancer = compose(
applyMiddleware(
routing,
sagaMiddleware,
redirector,
notifier,
responseCamelizer,
purgeStrage,
accessTokenSetter,
createLogger()
),
autoRehydrate(); // localStorage -> Reducer
);
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState, enhancer);
// Reducer -> localStorageへ
persistStore(store, { whitelist: ['session'] });
sagaMiddleware.run(rootSaga);
return store;
}
- autoRehydrate の実装の理解は難解
- Reconciliation の理解が必要そう..
まとめなど
- 高階関数とは関数を引数にとったり関数を返したりする関数だ
- 関数合成は関数呼び出しの連鎖を読み書きしやすくする
- HOC では高階関数が使われていた
- StoreEnhancer では高階関数と関数合成の使われ方を眺めた
- 弱い主張: 採用しているフレームワーク/ライブラリの意味のようなもの(例:Reconciliation)を全部理解しようとすることはないのでは?
- 弱い主張: どういう仕組みで動いているのか、というだけでも一つの理解の段階でそこまで把握できるならしておいて、必要に迫られたら詳細の理解をがんばる、という態度でけっこうなんとかなる
参考 URL
- http://redux.js.org/docs/api/compose.html
- https://github.com/koajs/compose
- https://medium.com/@dtipson/creating-an-es6ish-compose-in-javascript-ac580b95104a#.6wvm0c3yw
- http://postd.cc/react-higher-order-components-in-depth/
- https://github.com/franleplant/react-hoc-examples/blob/master/pp_basic.js
- https://github.com/acdlite/recompose
- http://qiita.com/pirosikick/items/d7f9e5e197a2e8aad62f
- https://gist.github.com/gaearon/1d19088790e70ac32ea636c025ba424e#file-connect-js
- https://github.com/franleplant/react-hoc-examples/blob/master/pp_basic.js
- https://github.com/reactjs/redux/blob/master/docs/Glossary.md#store-enhancer
- https://github.com/rt2zz/redux-persist
その他
今回のプレゼンは Qiita のスライドモードを使いました。
- プレゼン資料の内容は変更する可能性があります。変更内容は履歴を参照して下さい。
- 間違いなどありましたら、「編集リクエスト」をいただけるとうれしいです。