KitchHike Tech Blog

KitchHike Product, Design and Engineering Teams

Reactで開発するチームが共通認識しておきたい重要な概念

SFC, Redux, HOCなどコンポーネント指向とReact開発のキーワード

CTOの Shoken です。キッチハイクでは2年前にRailsへのReact導入、1年半前に0ベースからReact Nativeでアプリ開発を始めました。この記事では、React, React Nativeで開発しているチームが共通認識したいReactの重要な概念について紹介します。

2018/11/07 追記(はてブコメントより)

Reactリポジトリで名称の変更が行われ、変数名やクラス名が変更されました。いままでの Functional ComponentFunction Component となり、 Stateless は使わなくなって Function に統一されるようです。 Terminology: Functional -> Function Component #13775

Before After
functional component function component
StatelessComponent FunctionComponent

目次

  • SFC ( Stateless Functional Component )
  • Flux
  • Redux
  • Official Context API
  • Render Props と HOC ( Higher Order Components )
  • Suspense と Hooks

SFC ( Stateless Functional Component )

KHでの方針: 可能な限りSFCにする

SFC ( Stateless Functional Component ) は状態を持たないコンポーネントです。Reactのコンポーネントを構成する要素にstateとpropsの2つがありますが、そのうちのstateを持たないものです。コンポーネント設計の基本となるので、状態を持たないSFCという考え方はキッチハイクの開発チームでも特に重要視しています。

React の公式ドキュメントには Components and Props で書かれています。

Conceptually, components are like JavaScript functions. They accept arbitrary inputs (called “props”) and return React elements describing what should appear on the screen

訳: 概念的には、コンポーネントはJavaScriptの関数のようなものです。任意の入力(props)を受け入れて、何が画面に表示されるべきかが記述された React elements を返します。

ドキュメントの最初に書かれている例が Stateless Functional Component と呼ばれているものです。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

コンポーネント指向で設計する際にSFCを使うと、振る舞いと表示の責務をコンポーネントレベルで明示的に分けることができます。キッチハイクの開発ではSFCで書けるコンポーネントは可能な限りSFCにする方針で開発しています。可能な限り、stateを使わずにpropsのみを使ったFunctional Componentとして切り出して、コンポーネントを副作用の無い、値をDOMに変換する装置に徹することを目指しています。

また、後述する React HooksSuspense はSFC + α で状態管理や非同期通信を可能にするような使い方ができるので、今後のReactに追加されるAPIを理解する上でも重要な概念です。

キッチハイクでは以下の書き方をベースにしています。

const KHButton = props => {
  return (
    <View>
      <KHText style={props.textStyle}>{props.title}</KHText>
    </View>
  );
};

KHButton.propTypes = {
  title: PropTypes.string.isRequired,
  textStyle: PropTypes.object,
};

KHButton.defaultProps = {
  title: '',
  textStyle: {},
};

export default KHButton;

メリット

  • テスタブルになる
  • このコンポーネントでは状態を持たないということが明示的になる
  • パフォーマンスが高く、ライフサイクルについて心配する必要がない

Flux

FluxはFacebookが提唱しているデータフローのデザインパターンです。Reactでコンポーネント間のpropsの受け渡しと状態管理を設計する上で、重要な考え方になります。公式ドキュメント にはコンセプトとして以下のように説明されています。

Flux is a pattern for managing data flow in your application. The most important concept is that data flows in one direction.

訳: Fluxは、アプリケーションのデータフローを管理するためのパターンです。 最も重要な概念は、データが一方向に流れることです。

データフロー図

f:id:sfujisak:20181101043211p:plain

Redux

KHでの方針: 使うべき時期が来るまで使わない

Fluxの実装の一つで、コンポーネントの状態管理を行うライブラリです。3つの設計指針があります。

  1. Single source of truth
  2. State in read-only
  3. Changes are made with pure functions

f:id:sfujisak:20181101043226p:plain

1. Single source of truth

Storeと呼ばれるものでアプリケーション全体の状態を集中管理します。一つのオブジェクトツリーなので、JSONを考えるとイメージしやすい。

2. State in read-only

State変更のI/Fを限定その1。Stateは常に読み取り専用にして、変更するにはActionを通じてのみ可能です。 Viewからの直接的な変更がないことがライブラリとして保証されていることがメンテナンス性を向上させます。

3. Changes are made with pure functions

State変更のI/Fを限定その2。Reducerと呼ばれるものしかStateを変更できない設計です。ReducerはActionとStateを受け取り、新しいStateを返す関数です。

データフロー

What's Redux and how to use it? Redux Series I にあるGIFが今まで見た中で一番わかりやすいです。

メリット

f:id:sfujisak:20181101043216p:plain

Reduxの一貫した原則が設計の根幹にあるので、設計にしたがってレールに乗ればReduxの考えるベストプラクティスを自然に実装することになります。ただし、何かを簡単にするというよりは、状態管理に伴う重要な責務が一箇所に集まるようなイメージです。解決したいものはSFCと共通点があり、描画部分と状態管理部分を分けたい、という課題です。Reduxのレールに乗っている限りでは、大規模になっても一貫した設計を提供することが可能になります。

ただし、一方でReactの新しいAPIを見ると、Reduxがカバーしていた範囲の一部を代替するようなものが出てきているような印象を受けます。

Official Context API

KHでの方針: 使える箇所で実験的に使っていく

今まで実験的なAPIとして提供されていたものが React v16.3 で公式リリースされ、ReactのみでReduxが担っていた状態管理機能の部分、いわゆるpropsのバケツリレーを代替できるようになりました。Reduxの全てが代替できるということではなく、あくまでも一部機能の代わりに使えるという位置付けです。

React.createContext() して Consumer とProviderを使うことで、親-子関係以外のコンポーネントに値を渡すことが可能になります。

const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

Render Props と HOC ( Higher Order Components )

KHでの方針: Render Props を採用

コンポーネントに共通の振る舞いを持たせる仕組みとして Render Props と HOC があります。どちらも React の公式ドキュメントで紹介されてますが、キッチハイクでは Render Props に寄せていく方針です。

Render Props

A component with a render prop takes a function that returns a React element and calls it instead of implementing its own render logic.

Render Props – React

訳: render という prop を持つコンポーネントは、React element を返す関数を受け取り、独自のレンダリングロジックを実装する代わりにpropsとして受け取ったrender関数を実行します。

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

値が関数となる prop を使って、React のコンポーネント間でコードをシェアするためのシンプルなテクニックです。 HOCと同じく、カプセル化されたコンポーネント間でライフサイクルメソッド・関数・状態の共有をいかに行うかを解決する一つの方法です。

HOC ( Higher Order Components )

HOC はコンポーネントを引数にとり、付加価値のついたコンポーネントを返す関数です。カプセル化されたコンポーネント間で、ライフサイクルメソッドや状態を変更する関数を共通化し、ロジックを一元管理したコードを再利用することが可能になります。高階関数 ( higher-order function ) をReactのコンポーネント指向の世界で実現したものが、 Higher Order Components。複数のコンポーネントに共通した振る舞いを共通化できるのがメリットで、RubyだとModuleのイメージです。

HOC の構造

以下のかけ算を行うコードを例に考えるとわかりやすいです。関数を返す関数に、引数として何かを渡す、をシンプルに実装するとこのようになります。

const multiply = (x) => (y) => x * y
multiply(5)(20)

上記とHOCの違いはこの2つ。

  • 戻り値がコンポーネント
  • 引数がコンポーネント

HOCコンポーネントのサンプルコードです。

import React, { Component } from "react";

const isEmpty = prop => // do something

const Loading = loadingProp => WrappedComponent => {
  return class LoadingHOC extends Component {
    componentDidMount() {
      // do something
    }

    render() {
      const myProps = {
        loadingTime: ((this.endTimer - this.startTimer) / 1000).toFixed(2)
      };

      return isEmpty(this.props[loadingProp]) ? (
        <div className="loader" />
      ) : (
        <WrappedComponent {...this.props} {...myProps} />
      );
    }
  };
};

export default Loading;

次に使い方のサンプルコードです。 export default Loading("names")(List); という風に Loading 関数が返す LoadingHOC コンポーネントに List コンポーネントを引数で渡す。

import React, { Component } from "react";
import Item from "./Item";
import Loading from "./HOC/Loading";

class List extends Component {
  render() {
    return (
      <Item name={this.props.names} />
    );
  }
}

export default Loading("names")(List);

Suspense と Hooks

最後に、React v17 に向けて開発されている Suspense と Hooks を紹介します。Reduxの一部代替機能を SFC + α で実現できるようになっていくような印象を受けます。

以下は執筆時の情報ですのでリリース時には変更される可能性があります。

Suspense ( React.lazy, React.Suspense )

React v16.6.0 で実装されている、非同期処理を扱う概念です。React.lazy で非同期コンポーネント作成、 React.Suspense で囲むことで非同期処理を行います。 外部APIからデータを取得・表示し、読み込みを開始したらLoadingを出して、APIが戻ってきたらデータを表示する、ローディングを消す、というこれまでRedux(redux-saga)が担当していた責務を代替できる可能性があります。

Hooks

React v16.7.0-alpha で実装されている、HOCの代替として使える機能です。SFC でも状態操作を行う関数( useState )や componentDidMount などのライフサイクル操作が可能になり、SFC + Hooks で状態操作を実現できるようになります。この Pull Request RFC: React Hooks #68 が仕様提案になっているようです。

公式ドキュメント Hooks at a Glance

まとめ

  • SFC ( Stateless Functional Component ) を使うことで状態と描画の責務分離を行うことができる
  • SFC + αで実現できることが増えてきてSFCは今後もコンポーネントの基本となる(例: Hooks)
  • React本家でReduxの一部を代替するようなAPIの開発が進んでいる(例: Context API, Suspense)

あわせてこちらも読んでもらえると嬉しいです。

tech.kitchhike.com

We’re Hiring!

キッチハイクでは、React Nativeエンジニア・Reactエンジニア・Railsエンジニア・インターンエンジニアを募集中です!

www.wantedly.com

www.wantedly.com

www.wantedly.com

www.wantedly.com