Functional ComponentsとrecomposeでらくらくReactプログラミング

こんにちは。Nulab Appsチームの_fpです。Nulabではそれぞれのプロジェクトに合ったWeb Frameworkを使った開発を行なっており、Nulab AppsではReactを使用しています。

さて、Reactで苦労するポイントといえばいろいろありますが、中でもやはり状態管理とライフサイクルイベントハンドラRefとDOMのあたりではないでしょうか。

実装方法はいくつかありますが、ー般的なのは公式ドキュメントにあるようにReact.Componentを継承したクラスに機能を実装することかと思います。

ただ、いくつかクラスを作ると、だんだん似たような状態やライフサイクルを持つものばかりになることも少なくありません。

そこで、今回はFunctional ComponentsとHOCの組み合わせについて紹介してみたいと思います。サンプルコードは末尾のCodePenで試すことができます。

Functional ComponentsとHOC

まず、Functional Componentsとは、stateもlifecycleも持たず、propsを受け取りElementを返すだけのコンポーネントです。
Functionalの意味は、文字通りただのJavaScript関数だからだそうです。
ES6/JSXではこのように記述します。

// メッセージを表示するだけのFunctional Component
const Message = ({ message }) => (
  <div>
    {message}
  </div>
);

次に、Higher-Order Component(HOC)とはコンポーネントを引数に取りコンポーネントを戻り値とするような関数です。
日本語に訳すとすれば高階コンポーネントになるでしょうか。関数を引数にしたり、関数を戻り値とするような関数を高階関数と呼ぶのと似ています。
有名なものでは、Reduxのconnectや、recomposeがあります。

今回は、Functional Componentsとrecomposeの組み合わせを紹介したいと思います。

recomposeに含まれるHOCの紹介

withState()

ステートの値とそのステートを更新するアップデータをpropsとして渡します。

// 引数は'ステート名', 'アップデータ名', 初期値
const enhanceWithState =
  withState('message', 'setMessage', 'hello');

これと、stateとsetStateをpropsに含むFunctional Componentを組み合わせて、ステートフルなコンポーネントを作ることができます。

// まずFunctional Componentを定義する
const HelloButtonBase = ({ message, setMessage }) =>
  <button onClick={() => setMessage(x => ${x} world)}>
    {message}
  </button>

// HOCを適用する
const HelloButton = enhanceWithState(HelloButtonBase);

withStateHandlers()

ステートの値と複数のハンドラをpropsとして渡します。複数のハンドラを持つことができるので、状態遷移をカプセル化したい場合に適しています。

// 引数は、初期値, ハンドラのオブジェクト
const enhanceWithCounterState = withStateHandlers(
  {
    counter: 0,
  },
  {
    increment: ({ counter }) => (value) => ({
      counter: counter + value,
    }),
    decrement: ({ counter }) => (value) => ({
      counter: counter - value,
    }),
    reset: () => () => ({
      counter: 0,
    }),
  }
);

withRenderProps()

recompose 0.27.1で追加された、HOCを適用したコンポーネントに渡されるpropsをRender Propsの引数にするというものです。Render PropsとはElementを返す関数をpropsに渡す手法のことで、特にprops.childrenに関数を渡すことをFunction as Child Componentsと呼んだりします。
先ほどのwithStateと組み合わせると、

// HOCを引数として渡す
const MessageState = withRenderProps(
  withState('message', 'setMessage', 'hello'),
);

// 子の関数の引数として、withStateのpropsが渡される
const App = () => (
  <MessageState>
    {({ message, setMessage }) =>
      <button onClick={() => setMessage(x => ${x} world)}>
        {message}
      </button>
    }
  </MessageState>
);

のように、コンポーネントとしてJSXの中に記述すると、その子要素へのパラメータとしてwithStateのステートとアップデータが渡されていることがわかります。

lifecycle()

componentDidMountやcomponentWillUnmountといったlifecycleイベントを扱います。
初回表示時にデータをロードしたい場合や、addEventListenerを使いたい場合にも利用できます。

// 引数はライフサイクルのメソッドを持つオブジェクト
lifecycle({
  componentDidMount() {
    window.addEventListener('click', this.props.close);
  },
  componentWillUnmount() {
    window.removeEventListener('click', this.props.close);
  },
}),

lifecycle自体は子に何も渡さないので、次のcomposeで他のHOCと組み合わせて使われることが多いと思います。

compose()

複数のHOCを組み合わせたHOCを返します。よくある使い方として、

// withStateとlifecycleを組み合わせて、初回表示時にfocus()を呼ぶHOC
const enhanceWithAutofocus = compose(
  withState('inputRef', 'setInputRef', React.createRef()),
  lifecycle({
    componentDidMount() {
      const ref = this.props.inputRef;
      if (ref && ref.current) {
        ref.current.focus();
      }
    },
  }),
);

のように、異なる機能を持つ複数のHOCを組み合わせるというものがあります。
最初のwithStateによってpropsにinputRefが追加されているので、lifecycleの中ではthis.propsを通じて参照できます。

使用例

カウンタ

withStateHandlerの例として紹介したenhanceWithCounterStateを使うと、次のようにカウンタを作ることができます。
ハンドラはbindする必要はなく、直接指定することもArrow Functionでパラメータを渡すこともできます。

// HOCをRender Propsにする
const CounterStateHandler = withRenderProps(
  enhanceWithCounterState
);

// Render Propsを使ってFunctional Componentとして実装する
const Counter = () => (
  <CounterStateHandler>
    {({ counter, increment, decrement, reset }) => (
      <div>
        <span>{counter}</span>
        <button onClick={() => increment(1)}>Increment</button>
        <button onClick={() => decrement(1)}>Decrement</button>
        <button onClick={reset}>Reset</button>
      </div>
    )}
  </CounterStateHandler>
);

inputの初回表示時にフォーカスを当てる

focus()を呼ぶにはRefを取得する必要がありますが、composeの例にあるenhanceWithAutofocusとwithRenderPropsを使うと、Functional Componentで実現できます。

// HOCをRender Propsにする
const AutofocusRef = withRenderProps(
  enhanceWithAutofocus
);

// Render Propsを使ってinputのrefを渡す
const AutoFocusInput = () => (
  <AutofocusRef>
    {p => <input ref={p.inputRef} />}
  </AutofocusRef>
);

CodePen

以上の例は、こちらのCodePenで試すことができます。

是非Forkしていろいろ試してみてください!

まとめ

以上、よく使うものを中心に解説してみましたが、Functional ComponentsとHOCを組み合わせることで必要な機能だけをピンポイントに実現できることが分かります。
アプリケーションが複雑化するにつれて状態管理が大変になったりコードが冗長になったりしがちで、それはReactにおいても例外ではありませんが、再利用性の高いコンポーネントでシンプルに記述することで可読性の高いコードが実現できます。

それでは、楽しいReactライフを!

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる