もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

Recomposeを使ってReact/ReduxのComponentを整理する

Recompose

元のComponent

  • 対象は何かAPIを呼んで受け取った結果を表示するComponentで、呼び出し中はProgressを表示して完了したら結果を表示するという仕様です。
  • Sampleのコードは色々省略してるので雰囲気です。
  • 言語はTypeScriptです。型はいいものです。

containers/SampleHoc.tsx

const mapStateToProps = ({ isFetched, data }: GlobalState) => {
  // APIが呼び出し終わっていたらisFetchedはtrueになってdataが取得できている。
  return {
    isFetched,
    data
  };
};

const mapDispatchToProps = (dispatch: Dispatch<GlobalState>) => {
  return {
    onStart: () => {
      dispatch(fetchData());
    }
  }
};

export default connect(mapStateToProps, mapDispatchToProps)(Sample);

components/Sample.tsx

interface SampleProps {
  isFetched: boolean;
  data: Data;
  onStart: () => void;
}

export default class Sample extends React.Component<SampleProps> {

  public componentDidMount() {
      // 初回描画時のイベントを発火してContainerでAPI呼ぶ
      this.props.onStart();
  }

  public render() {
    if (!this.props.isFetched) {
      // 読み込み中表示
      return (
        <Progress />
      );
    }

    // Data表示
    return ();
  }
}

SampleをStatelessComponentにする

  • 上ではReact.Componentクラスを継承してライフサイクルのイベントを実装していますが、StatelessComponentはpropsを受け取ってcomponentを返す形式の関数を書きます。

components/Sample.tsx

const Sample = (props: SampleProps) => {
  if (!props.isFetched) {
    // 読み込み中表示
    return (
      <Progress />
    );
  }
    
  // Data表示
  return ();
}

export default Sample;

で、これだけだとライフサイクルのハンドリングができないのでRecomposeのlifecycleを使ってHOCで実装します。

components/Sample.tsx

// 上の続き

export default lifecycle<SampleProps, {}>({
  componentDidMount() {
    this.props.onStart();
  }
})(Sample);

これで同じ挙動になりました。こちらのほうがPropsをViewに変換するというイメージでComponentを書けます。

SampleComponentの責務を分割する

  • Sampleをrenderするときに読み込み中の判定をしているところを改善してSampleは受け取ったdataを表示するだけという形にします。Recomposeのbranchを使って実装してみます。

containers/SampleHoc.tsx

// SamplePropsからisFetchedを分離してこちらに定義する
export interface SampleHocProps {
  isFetched: boolean;
}

// Propsからデータ取得済みか判定する関数を作成
const checkFetched = ({ isFetched }: SampleHocProps) => isFetched;

// checkFetchedがtrueならwithFetchedCheckに渡したcomponentが返る
// checkFetchedがfalseならInitializerComponentが返るという設定。Initializerは次で説明
const withFetchedCheck = branch<SampleHocProps>(
  checkFetched,
  component => component,
  _ => Initializer
);

export default connect(mapStateToProps)(withFetchedCheck(Sample));
  • Progressを表示する際にAPIをコールするようにReduxのHOCを実装します。

containers/Initializer.tsx

const mapDispatchToProps = (
  dispatch: Dispatch<GlobalState>
): ProgressDispatchProps => {
  return {
    onStart: () => {
      dispatch(fetchData());
    }
  };
};

const Initializer = lifecycle<ProgressDispatchProps, {}>({
  componentDidMount() {
    this.props.onStart();
  }
})(Progress);

export default connect(undefined, mapDispatchToProps)(Initializer);

これでAPIの呼び出しとProgressの表示は分離できました。Sample.tsxは以下のように書き直せて、純粋に受け取ったPropsをViewに変換するという形になります。

interface SampleProps {
  data: Data;
}

const Sample = (props: SampleProps) => {
  // Data表示
  return ();
}

export default Sample;

おわり

  • Recomposeを使ってHOCとComponentを整理してみました。他にも便利な関数群が用意されているみたいなので少しずつ試そうかなと思います。
  • 実際にこの形式で実装しているアプリケーションはWIPなので完成したらまた公開します。