Reactでコンポーネントのふるまいだけ使い回したい

はじめに

こんにちは。VIDEO BRAINでフロントエンドを担当している田村です。いつの間にか新卒2年目になっていました。 最近全く外に出られないのでアニメを見始めました。「かぐや様は告らせたい」めっちゃ面白いですね。速攻で漫画全巻買いました。

で、早速仕事の話なのですが、最近PRのレビューしてるとコンポーネントのふるまい再利用できそうだなぁと思うことがちょくちょくありました。 よく使われる手法として、Render PropsとHigh Order Componentという手法があるので紹介しようと思います。

Render Props

例えば、レンダリング時の月を表示するコンポーネントがあるとします。

class Month extends React.Component {
  constructor() {
    super();
    this.state = {
      time: new Date()
    };
  }
  render() {
    return <h1>今は{this.state.time.getMonth() + 1}月です</h1>;
  }
}
// => 今は7月です

しかし、別のページに年/月/日を表示したいという要望が出てきました。 できれば上のコンポーネントのstateをそのまま使いたいので、Monthを拡張してみました。

class Month extends React.Component {
  constructor() {
    super();
    this.state = {
      time: new Date()
    };
  }
  render() {
    const { pageName } = this.props;
    return pageName === 'month' ? (
      <h1>今は{this.state.time.getMonth() + 1}月です</h1>
    ) : (
      <h1>
        今は
        {`${this.state.time.getFullYear()} / ${this.state.time.getMonth() + 1} / ${this.state.time.getDate()}`}
        です
      </h1>
    );
  }
}

// => 今は7月です
// => 今は2020/7/8です

香ばしいコードの完成です。日だけ表示したい。時/分/秒だけ表示したいという要望が増えるたび条件分岐も増えていく未来が見えます。 できればstateの振る舞いは今のままで、表示するコンポーネントだけ変えたいですね。 こういった場合に使えるのがRender Propsです。

Render Propsは、propsにrender関数を渡すことによって、コンポーネント自信が持つrenderを変化させる手法です。 振る舞いをそのままに、表示するコンポーネントだけ変えることができるので、今回の例にはうってつけです。 コードで書くとこんな感じ。

class DateRender extends React.Component {
  constructor() {
    super();
    this.state = {
      time: new Date()
    };
  }
  render() {
    return (
      <h1>
        今は
        {this.props.render(this.state.time)}
        です
      </h1>
    );
  }
}

class MonthRender extends React.Component {
  render() {
    return (
      <DateRender
        render={time => {
          return <h2>{`${time.getMonth() + 1}月`}</h2>;
        }}
      />
    );
  }
}

// => 今は7月です

こうすれば、表示する日時のパターンがどれだけ増えても、振る舞いを再利用してコンポーネントを作ることができます。 また、props.renderとして渡さずに、childrenとして渡すこともできます。 childrenとして渡す場合には

<DateRender children={time => (
  <h2>{`${time.getMonth() + 1}月`}</h2>
)}/>

<DateRender>
 {time => (
   <h2>{`${time.getMonth() + 1}月`}</h2>
 )}
</DateRender>

こんな風に書くこともできます。

また、同じことを実現する手法として、High Order Componentがあります。

High Order Component ( HOC )

所謂高階関数コンポーネントです。 これもRender Propsと同じようにふるまいを再利用するためによく使われる手法です。

高階関数のReact版みたいなものです。

const date = (dateType) => (number) => {
    return `今は${number}${dateType}です`
}
date('月')(2); // => 今は2月です
date('日')(1); // => 今は1日です

↑の引数と返り値がコンポーネントになったのがHOCです。

HOCでふるまいを使い回す場合は、高階関数のとしての役割を持つラッパーを作って実装していきます。

function dateComponent(WrappedComponent, currentDate) {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        date: currentDate(new Date());
      };
    }
    render() {
      return <WrappedComponent date={this.state.date} {...this.props} />;
    }
  };
}

class Month extends React.Component {
  render() {
    const { date } = this.props;
    return (
      <h1>今は{date}月です</h1>
    )
  }
}

class Date extends React.Component {
  render() {
    const { date } = this.props;
    return (
      <h1>今は{date}日です</h1>
    )
  }
}

const MonthComponent = dateComponent(
  Month,
  time => (time.getMonth() + 1)
);

// => 今は7月です

const DateComponent = dateComponent(
  Date,
  time => time.getDate()
);

// => 今は8日です

どっちを使うのか

以上の2つを比べた時、私個人としてはHOCよりもRender Propsのほうが好きです。 Reactにおいてベストなのは、最もシンプルな形であるpropsを使った純関数の形です。あくまでstateは2の次。 それをわかりやすい形で保てるのがRender Propsだと思っています。 それ以外の理由としては

  • render部分をpropsとして渡していることが直感的にわかる
  • 親と子の間に挟む関数がないので、予期しない実装(prototypeで関数上書きするとか)が入る余地がない

が上げられます。

終わりに

Reactクラスは普通のJsClassのように、継承することを良しとしていません。 Reactを触り始めたばかりだと癖があるように感じるかもしれませんが、Render PropsやHigh Order Componentを使えば、むやみに同じロジックを増やさずに済みます。

はじめから未来の仕様変更を予想してコンポーネントを作るのは難しいですが、なるべく小さなコンポーネントで、減らせる分岐は減らしながら書いてくのが大切だなぁと思うこの頃です。 あと、ここ3ヶ月くらいReact書いてないからそろそろ書きたい。