Edited at

TypeScriptでrecomposeを使う


はじめに

普段はReact.SFCで書いてるけど、ステート追加したり、ライフサイクル追加したりしたいことがあります。

recomposeつかえばいいのですが、型定義が結構深くて調べるのが大変でした。調べる度に追記していきます。


Samples

実際に使ったやつとか組み合わせとかをのせます。


lifecycle

一番簡単なやつです。componentDidMountで統計情報送ったりしたいとき便利です。


LifeCycleComponent.tsx

import * as React from 'react';

import {
lifecycle,
ReactLifeCycleFunctions,
} from 'recompose';

type Props = {
text: string;
};

const component: React.SFC<Props> = (props: Props) => {
return <div>{props.text}</div>;
};

const lifeCycleFunctions: ReactLifeCycleFunctions<Props, {}> = {
componentWillMount() { console.log('component will mount'); }, // thisにもアクセス可
componentDidMount() { console.log('component did mount'); },
};

export default lifecycle<Props, {}>(
lifeCycleFunctions
)(component);



withStateHandlers

これを使うとStateを追加できます。


WithStateHandler.tsx

import * as React from 'react';

import {
mapper,
StateHandler,
StateHandlerMap,
StateUpdaters,
withStateHandlers,
} from 'recompose';

type Outter = { // こいつがモジュールの外側から注入するProps
initialCount: number;
};

type State = { // こいつが添加するState
counter: number;
};

interface Updaters extends StateHandlerMap<State> {
increment: StateHandler<State>; // Stateを受け取って、'Stateの部分集合を返す関数'を返す。
decrement: StateHandler<State>;
}

type Props // こいつには
= Outter // 一番外側から渡されるプロパティと
& State // Stateの中身と
& Updaters; // Stateの部分集合を返す関数たちが入ってる

const component: React.SFC<Props> = (props: Props) => {
return (
<div>
<span>{props.counter}</span>
<button onClick={(e) => {
e.preventDefault();
props.increment(2);
}}>
increment
</button>
<button onClick={(e) => {
e.preventDefault();
props.decrement(1);
}}>
decrement
</button>
</div>
);
};

const createProps: mapper<Outter, State> // 外側からのプロパティを受け取って、初期Stateを返す
= (props: Outter): State => ({ counter: props.initialCount });

const stateUpdaters: StateUpdaters<Outter, State, Updaters> = {
increment: (prev: State, props: Outter): StateHandler<State> => ( // propsは省略可
(value: number): Partial<State> => ({ // 引数受け取ってもOK
counter: prev.counter + value,
})
),
decrement: (prev: State, props: Outter): StateHandler<State> => (
(value: number): Partial<State> => ({
counter: prev.counter - value,
})
),
};

export default withStateHandlers<State, Updaters, Outter>( // initialCountをPropertyとするStatefulなコンポーネント
createProps,
stateUpdaters,
)(component);



withStateHandlers + lifecycle

componentWillMountとかでsetStateをしたいケースがある。(例ではしてないけど。)


WithStateHandlerWithLifecycle.tsx

import * as React from 'react';

import {
compose,
lifecycle,
mapper,
ReactLifeCycleFunctions,
StateHandler,
StateHandlerMap,
StateUpdaters,
withStateHandlers,
} from 'recompose';

type Outter = {
initialCount: number;
};

type State = {
counter: number;
};

interface Updaters extends StateHandlerMap<State> {
increment: StateHandler<State>;
decrement: StateHandler<State>;
}

type Props
= Outter
& State
& Updaters;

const component: React.SFC<Props> = (props: Props) => {
return (
<div>
<span>{props.counter}</span>
<button onClick={(e) => {
e.preventDefault();
props.increment(2);
}}>
increment
</button>
<button onClick={(e) => {
e.preventDefault();
props.decrement(1);
}}>
decrement
</button>
</div>
);
};

const createProps: mapper<Outter, State>
= (props: Outter): State => ({ counter: props.initialCount });

const stateUpdaters: StateUpdaters<Outter, State, Updaters> = {
increment: (prev: State): StateHandler<State> => (
(value: number): Partial<State> => ({
counter: prev.counter + value,
})
),
decrement: (prev: State): StateHandler<State> => (
(value: number): Partial<State> => ({
counter: prev.counter - value,
})
),
};

const lifeCycleFunctions: ReactLifeCycleFunctions<Props, {}> = {
componentWillMount() { console.log('component will mount'); },
componentDidMount() { console.log('component did mount'); },
};

export default compose<Props, Outter>( // composeを使ってまとめる
withStateHandlers<State, Updaters, Outter>(createProps, stateUpdaters),
lifecycle<Props, {}>(lifeCycleFunctions), // lifecycleまできたときにはStateは隠れてるので第二ジェネリック引数は{}でOK
)(component);



withStateHandlers + withHandlers + lifecycle

withStateHandlersだとハンドラに非同期処理をもたせられない…という時用の組み合わせ。別途非同期処理ハンドラをもたせて、stateHandlerを呼び出すような形。

そして、コンポーネントのマウント時に状態をセットする。


WithStateHandlerWithAsyncHandlersAndLifecycle.tsx

import * as React from 'react';

import {
compose,
withStateHandlers,
withHandlers,
StateHandler,
StateHandlerMap,
lifecycle,
} from 'recompose';

type Outter = {}; // ここから外側のプロパティを注入できる。

type Body = { // 非同期処理の結果の肩
[key: string]: string;
};

type State = {
data: Body;
};

interface StateUpdaters extends StateHandlerMap<State> {
recieveData: StateHandler<State>; // ここでは普通に状態の更新のインターフェースを用意する
}

type Handlers = {
fetchData: () => Promise<void>; // StateHandlerとは別に非同期関数を用意する
};

type Props = Outter & State & StateUpdaters & Handlers;

const component: React.SFC<Props> = (props: Props) => {
const entries = Object.entries(props.data);
return (
<dl>
{
entries.map((e, i) => ([
<dt key={i}>{e[0]}</dt>,
<dd key={i + entries.length}>{e[1]}</dd>,
]))
}
</dl>
);
};

export default compose<Props, Outter>(
withStateHandlers<State, StateUpdaters, Outter>(
{ data: {} },
{
recieveData: (_: State) => (body: Body) => ({ data: body }),
},
),
withHandlers<Props, Handlers>({
fetchData: props => async () => {
// ここの実装がミソ。
try {
const resp = await fetch('https://api.github.com');
const body = await resp.json();
props.recieveData(body); // StateHandlerのインターフェースを呼び出しちゃう。
} catch (e) {
console.log(e.message);
}
},
}),
lifecycle<Props, {}>({
componentDidMount() {
this.props.fetchData();
},
}),
)(component);



まとめ

型ありがたい。型定義調べると使い方がわかる。