Reactのコンポーネントをステートレスに!!!recomposeとは? ~0からreact習得記 day 09~

  • 14
    いいね
  • 0
    コメント

これまでのreact習得記は、こちらから閲覧できます。

はじめに

株式会社VRizeでインターンとして勤務しているryo_tです。
短期目標に「1ヶ月でReactのUIを開発できるように」と伝えられたので、Reactはほとんど未経験ですが一歩一歩頑張って行きます。

目的

Higher-order Componentsとrecomposeを理解してコンポーネントに状態を持たせない設計を
行える様にする。

Qiitaで検索してもrecomposeの記事は13件しか(2017年10月12日現在)なかったので、使い方を簡単にまとめてみようと思います。

Screen Shot 2017-10-12 at 17.27.04.png (704.2 kB)

用語整理

recompose

  • recomposeはHigher-order Componentsを作成・提供するための便利関数ライブラリです。
  • React用のlodash.jsのようなものです。

Higher-order Components

  • Higher-order ComponentsはComponentを引数にとり、Componentを戻り値とする関数や実装パターンの事です。高階関数の関数がComponentになったと考えると分かりやすいかもしれません。

高階関数

  • 高階関数とは関数を引数や戻り値として扱う関数です。高階関数に関しては前回学んだので分からない方はこちらをご覧ください。

手順

開発環境のセットアップ

前回と同じように、今回もReact Boilerplateを用いて開発環境のセットアップを行います。更にrecomposeのインストールも行います。
https://github.com/nolotz/react-simple-boilerplate


#terminalで 
git clone https://github.com/nolotz/react-simple-boilerplate.git
#を入力してクローンします

# react-simple-boilerplate`内に移動する
cd react-simple-boilerplate

# パッケージをinstallする
npm install

# npmからrecomposeをインストールする。
npm install recompose --save

# webpackの開発サーバーを起動する
npm start

ブラウザのアドレスバーにlocalhost:3000と入力すると
Hello React :) と画面左端にこのように表示されるはずです。

Screen Shot 2017-09-25 at 10.33.04.png (13.1 kB)

recomposeを使ってみる前に、src/App.jsx内の<h1>Hello React :)</h1>を消します。同様に、styles/home.scss内の記述も全て消します。これらの記述はHello React :)と表示する為の記述で、今回は必要ありません。

解説

実際に関数を触ってみる前に解説を交えたいと思います。というのも自分がrecomposeを使い始めた時、仕組みを理解せずに学び始めたので大事な部分を飛ばして何が起きてるのか分からない事がよくありました。なので今回は、コードを交えながら簡単に解説したいと思います。

自分はEnhancerとMyComponentを理解していなかったので大分ハマってしまいました。
なので、今回は自分のよく分からなかった部分を交えてざっくりと解説していきたいと思います。詳しく知りたい方はドキュメントをお読みください。

メリット

recomposeは先程も述べた様に便利関数ライブラリです。
recomposeを使用せずにHigher-order Componentsを実装すると、

App.jsx
const composedHoc = BaseComponent => hoc1(hoc2(hoc3(BaseComponent)))

一見何が起こっているのかよく分からない関数ですが、recomposeを使用すると
下記の様なすっきりとしたコードになります。

App.jsx
const composedHoc = compose(hoc1, hoc2, hoc3)

基本

鍵となる2つは
* Enhancerはコンポーネントを受け取ってコンポーネントを返す関数です。Higher-order Componentsです。
* MyComponentは適応される側のコードです。

Enhance.jsx
 import { Component } from "React";

 export const Enhance = ComposedComponent => class extends Component {
   constructor() {
     this.state = { data: null };
   }
   componentDidMount() {
     this.setState({ data: 'Hello' });
   }
   render() {
     return <ComposedComponent {...this.props} data={this.state.data} />;
   }
 };
  • Enhance.jsxのEnhancer = (ComposedComponent) => class extends Component...は、ComposedComponentを引数に受けて、無名のクラス(class extends Component)をReturnしています。
HoCs.jsx
 import { Enhance } from "./Enhance";

 class MyComponent {
   render() {
     if (!this.data) return <div>Waiting...</div>;
     return <div>{this.data}</div>;
   }
 }

 export default Enhance(MyComponent); // Enhanced component
  • export default Enhance(MyComponent)でHigher-order Components適用済みのComponentをexportしています。

中がどうなっているのか一つずつ紐解いていくと、そんなに難しいことをやっている訳ではないことに気付く事が出来ました。次はいくつかの関数に触れながらrecomposeを覚えていきたいと思います。

関数

defaultProps()

  • defaultPropsはベースのコンポーネントにpropsを渡す関数です。withProps()と似ています。
App.jsx

import React from 'react';
import { defaultProps } from 'recompose';

const enhance = defaultProps({
  hello: 'hello, world'
});

const App = ({ hello }) => {
  return (
    <div>
      <h1>defaultProps: { hello } </h1>
    </div>
  );
}

export default enhance(App);

renameProps()

  • renamePropsは受け取ったpropを違う名前に変更できます。
  • 例では、コンポーネントの中で呼んでいたhelloがhogehogeで呼び出せる様になります。
  • export default enhance(enhance2(App));の様に関数ごとにEnhancerを定義していくのでEnhancerが増えるに連れて徐々に分かりづらいコードになっていきます。その問題を解決する関数がcomposeです
App.jsx

import React from 'react';
import { defaultProps, renameProps } from 'recompose';

// default propsのenhancer
const enhance = defaultProps({
  hello: 'hello, world'
});

const enhance2 = renameProps ({
  hello: 'hogehoge'
});

const App = ({ hogehoge }) => {
  return (
    <div>
      <h1>defaultProps: { hogehoge } </h1>
    </div>
  );
}

export default enhance(enhance2(App));

compose()

  • composeは複数のrecompose関数を一つに構成する事が出来ます。これにより複数のEnhancerが出力時に分かりづらくなるのを防ぎます。
App.jsx

import React from 'react';
import { defaultProps, renameProps, compose } from 'recompose';

const Enhance = compose(
  defaultProps({
    hello: 'hello, world'
  }),
  renameProps ({
    hello: 'hogehoge'
  })
);

const App = ({ hogehoge }) => {
  return (
    <div>
      <h1>defaultProps: { hogehoge } </h1>
    </div>
  );
}

export default Enhance(App);

withProps()

  • withProps()はdefaultProps()と似ていてコンポーネントにpropsを渡す関数です。今回はオブジェクトhumanを作って中の値をAppに与えて出力しています。defaultPropsなどと競合した場合、defaultPropsよりも優先されます。flattenPropがオブジェクトhumanを平らにして、Propsにアクセスできる様にしています。
App.jsx
import React from 'react';
import {compose, withProps, flattenProp } from 'recompose';

const Enhance = compose(
  withProps({
    human: {
      name: 'Ryo',
      age: 22,
      org: 'VRize'
    }
  }),
  flattenProp('human')
);

const App = ({ name, age, org }) => {
  return (
    <div>
      <h3>name: {name}/ age: {age}/ org: {org}/</h3>
    </div>
  );
}

export default Enhance(App);


withState()

  • withState()は、クラスを作らずにpropsの値の受け渡しをステートレスで行う事が出来ます。
    1. withState()はStateName、StateUpdateName、InitialStateと3つの引数を取ります。
    2. updaterNameにupdateCounterという関数を指定します。
    3. initialStateで定義した初期値を0とした値はcounterに格納されています。
    4. button内のonClickイベント内でupdateCounter関数を定義して呼び出しています。
App.jsx

import React from 'react';
import { compose, withState } from 'recompose';

const Enhance = compose(
  withState('counter', 'updateCounter', 0)
);

const App = ({counter, updateCounter}) => {
  return (
    <div>
      <h1>Count: {counter}</h1>
      <button onClick={() => updateCounter((counter) => counter+1 )}>Increment</button>
    </div>
  );
}

export default Enhance(App);

withHandlers()

  • withHandlersはwithState()の値を使用して関数を作ります。
App.jsx

import React from 'react';
import { compose, withState, withHandlers } from 'recompose';

const Enhance = compose(

  withState('counter', 'updateCounter', 0),
  withHandlers({
    increment: ({ updateCounter }) => () => {updateCounter(counter => counter + 1);},
    decrement: ({ updateCounter }) => () => {updateCounter(counter => counter - 1);},
    reset: ({ updateCounter }) => () => {updateCounter(0);}
  })
);

const App = ({counter, increment, decrement, reset}) => {
  return (
    <div>
      <h1>Count: {counter}</h1>
        <button onClick={increment}>Increment</button>
        <button onClick={decrement}>Decrement</button>
        <button onClick={reset}>Reset</button>

    </div>
  );
}

export default Enhance(App);

withState()とwithHandlers()を使用する事で、ステートレスなカウンターアプリを作る事が出来ました

mapProps()

  • mapProps()は、与えられた配列のPropsを書き換えたり並び替えたりします。
  • flattenPropの後ろに置くとflattenPropが働いて、配列ではなくなってしまうので上に再配置しています。
App.jsx

import React from 'react';
import { compose, withProps, flattenProp, mapProps } from 'recompose';

const Enhance = compose(
  withProps({
    human: {
      name: 'Ryo',
      age: 22,
      org: 'VRize'
    }
  }),
  mapProps(({ human }) => {
    return {
      human: {
        name: human.name.toUpperCase(),
        age: human.age
      }
    };
  }),
  flattenProp('human'),
);

const App = ({name, age, org}) => {
  return (
    <div>
      <h3>name: {name}/ age: {age}/ org: {org}/</h3>
    </div>
  );
}

export default Enhance(App);

withStateHandlers()

  • withStateHandlers()はwithState()とwithHandlers()が合わさった様な関数です。
App.jsx

  const Counter = withStateHandlers(
    ({ initialCounter = 0 }) => ({
      counter: initialCounter,
    }),
    {
      increment: ({ counter }) => (value) => ({
        counter: counter + value,
      }),
      decrement: ({ counter }) => (value) => ({
        counter: counter - value,
      }),
      reset: (_, { initialCounter = 0 }) => () => ({
        counter: initialCounter,
      }),
    }
  )(
    ({ counter, incrementOn, decrementOn, resetCounter }) =>
      <div>
        <Button onClick={() => increment(2)}>Inc</Button>
        <Button onClick={() => decrement(3)}>Dec</Button>
        <Button onClick={reset}>Res</Button>
      </div>
  )

おわりに

中々思うように進まなかったのですが、抜けていた部分を埋め、なんとかrecomposeが使える様になってきました。recomposeを使用してステートレスなアプリを何個か作ってみたいと思います。

補足、間違っている箇所がございましたらご指摘よろしくお願いします。

株式会社VRizeは、VR広告ネットワークやVR動画アプリの制作等、VRに関わる様々なサービスを製作しています。VRとReactに興味があるフロントエンジニアさん、絶賛募集中です!
https://vrize.io/
https://www.wantedly.com/projects/79383