25
22

More than 5 years have passed since last update.

TypeScriptでrecomposeを使う

Last updated at Posted at 2018-06-12

はじめに

普段は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);

まとめ

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

25
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
22