🐼

React を深く知るための入り口

2021/03/07に公開

Reactに対する見方をアップデートする

国内外の優れた開発者の方による React の各論の記事は枚挙にいとまがありません。しかし、React の入門を一通り終えた方に向けの浅く広い総論はあまり見かけません。

React の公式ドキュメントのトップページに掲載されている短い3つの文章があります。この React の本質を表現した文章を掘り下げることが、初学者のステップアップにつながるのではないかと考え、各章に対して注釈を加えました。

React について少し深く知ることで、さらに React を好きになったという方を一人でも多く増やしたい。その思いから本記事を執筆しました。

本記事は React の考え方を知ることで、React に対する見方をアップデートすることを目的としています。

Reactとは何か。それはUIを構築するためのJSライブラリである

React公式ドキュメントの一文

React公式ドキュメント

React の公式ドキュメントに以下の一文が掲げられています。

A JavaScript library for building user interfaces

React は「UI を構築するための JS ライブラリ」です。これはよく知られた事実です。しかし、UI について

React の内容に入る前に、一度 UI について少し考えてみましょう。

UIの定義

UI とは何か。wikipedia による定義は以下です。

ユーザインタフェース(英: User Interface、 UI)または使用者インタフェースは、機械、特にコンピュータとその機械の利用者(通常は人間)の間での情報をやりとりするためのインタフェースである
ユーザインタフェース

英語版 wikipedia の定義の方がわかりやすいです。

In the industrial design field of human-computer interaction, a user interface (UI) is the space where interactions between humans and machines occur.
User Interface

つまり、UI とは人間と機械の境界であり、双方向的なやりとりが生まれるところなのです。

「ReactはUIを構築する」

「React が UI を構築する」とは、ユーザーは React が作成した UI を操作することで、外からは中身の見えないアプリケーションを操作可能にするということです。これは人から機械へのアクションです。

人から機械を操作することとは反対に、操作された機械から人に(より正確には機械の設計者から使用者に対して)伝えたいこともあるでしょう。

UI の英語版の定義にある interaction とは、inter と action からなる単語であり、機械と人の双方向的なやりとりだということを暗示しているからです。

human and machine

スマホの指紋認証センサーというUIの例

例えば、スリープ状態のスマートフォンを思い浮かべてみてください。

事前に指紋登録をした指をセンサーに押し当てると、スリープ状態が解除されてディスプレイは明るくなり画面ロックが解除されます。

指をセンサーに押し当てる行為は人からスマホ(機械)へのアクションです。そして、画面ロックが解除されたことは指紋認証に成功したことを知らせるスマホから人に対するアクションです。

一方、指紋登録していない指を押し当てると認証に失敗し、スマホのディスプレイは暗いままバイブレーションが作動してスマホが少し震えます。

指紋認証センサー自体も、認証に成功してディスプレイが明るくなることも、認証に失敗してバイブレーションが作動することも User Interface といえるでしょう。

私たちはスマホの中の仕組みを知らなくても、UI を通じてスマホの機能を利用したりスマホの状態を知ることができます。

UIとオブジェクト指向のカプセル化

public and private

以下は余談なので興味ある方以外は読み飛ばしても構いません。

オブジェクト指向言語におけるカプセル化とは、この「中身を知らなくてもいい」ことを指しています。カプセル化された仕組みは詳しく知らなくてもいいのです。

例えば、あるオブジェクトの private メソッドはそのオブジェクトの利用側から呼び出すことができません。 オブジェクトを利用する側が呼び出せるのは public メソッドのみです。

この public / private はまとめて visibility と呼ばれます。

あるオブジェクトの利用側からは、文字通りそのオブジェクトの public メソッドしか見えないのです。 見えないものは使えませんね。

private という visibility は、メソッドやプロパティがクラスというカプセルに閉じ込められているのでオブジェクトの利用者側からは見えないのだということを表現しています。

実際、TypeScript のインターフェース は public なメソッドとプロパティしか定義できません。オブジェクトの利用者は private なメソッドを知らなくていいからです。

スマホの例に照らし合わせると、指紋認証センサーが外に露出した目で見える public なインターフェース(UI)であり、指紋の認証処理は中の見えない private な仕組みであるといえます。

指紋認証の例を TypeScript のコードで表現してみました。

// インターフェース
interface AuthInterface {
  check(checkable: Checkable): boolean;
}

// 指紋認証
class FingerPrintAuth implements AuthInterface {
  check(checkable: Checkable): boolean {
    return this.doSomethingGreat(checkable)
  }

  private doSomethingGreat(checkable: Checkable): boolean {
    // 何らかのすごい判定処理
    const isSuccess = //...

    return isSuccess
  }
}

class SmartPhone {
  private locked: boolean = true;
  private auth: AuthInterface;
  private vibration: VibrationInterface;

  constructor(auth: AuthInterface, vibration: VibrationInterface) {
    this.auth = auth;
    this.viberation = vibration;
  }

  unlock(fingerPrint: Checkable): void {
    // auth オブジェクトの public メソッド check しか呼び出せない
    // this->auth->doSomethingGreat と記述するとエラーになる
    const passed = this.auth.check(fingerPrint);

    if (!passed) {
      // バイブレーション作動
      this.vibration.vibrate();
      return;
    }

    // 画面ロック解除
    this.locked = false;
  }
}

UIのまとめ

これも余談ですが、フロントエンドエンジニアの方が自身のことを「JSON 色付け係」と自虐的に称するとき、ソフトウェアの状態をユーザーに表示することが念頭にあるのだと思います。

つまり、機械から人に対する UI を提供しているばかりで、人から機械に対する UI を制作する機会が少ない。このため、JavaScript で複雑な制御が可能である現代的なフロントエンドのパワーを十分に発揮する機会があまりないことを嘆くフレーズなのかなと推測しています(違うかもしれませんがw)。

まとめると、React はアプリケーションの状態を利用者に表現し(機械から人へのアクション)、利用者がアプリケーションを操作するための(人から機械へのアクション)インターフェースを提供する JavaScript のライブラリだということです。そして UI とはアプリケーションの public な部分なのです。[1]

宣言的(Declarative)

宣言的

React の公式ドキュメントの記述に戻りましょう。3つの文章のうち、一番最初に登場するのは「Declarative」(宣言的)という単語です。

日本語版の公式ドキュメントの訳と原文を併記します。

宣言的な View
React は、インタラクティブなユーザインターフェイスの作成にともなう苦痛を取り除きます。アプリケーションの各状態に対応するシンプルな View を設計するだけで、React はデータの変更を検知し、関連するコンポーネントだけを効率的に更新、描画します。
宣言的な View を用いてアプリケーションを構築することで、コードはより見通しが立ちやすく、デバッグのしやすいものになります。

Declarative
React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just the right components when your data changes.
Declarative views make your code more predictable and easier to debug.

以下では、英文を元に解説を加えていきます。

ReactはインタラクティブなUIを構築する

React makes it painless to create interactive UIs.

「React は インタラクティブ な UI を作るときに苦痛を感じないようにする」とあります。上記で見たように、UI はそもそも interactive であることは自明ですが、ここではあえてそれを強調しているのでしょう。

私はcreate interactive UIsというフレーズがこの文で重要なところだと考えます。

このため、あえて先にインタラクティブな UI とは何かを記述しました。

この一文は Vanilla JS や jQueryといった既存技術で interactive な UI を作るのは大変だということを暗示しているのでしょう。

その理由は、後述のDeclarative views make your code more predictable and easier to debug.で詳しく解説することにして次に進みます。

stateに応じてviewが変化する

state and view

Design simple views for each state in your application,

「アプリケーションの各状態に対してシンプルな view を設計する」というこの文をよく読むと、アプリケーションの状態と view が基本的に1対1に対応することが読み取れます。

実際のコードで解説します。

Counterというコンポーネントを作成してみました。これは、+ボタンを押すとカウントを1増やすものです。

import React from 'react'
import ReactDOM from 'react-dom'

const Counter = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)

  if (count === 1) {
    return (
      <div>
        <h1>{count}</h1>
        <button onClick={increment}>+</button>
      </div>
    )
  } else if (count === 2) {
    return (
      <div>
        <h2>{count}</h2>
        <button onClick={increment}>+</button>
      </div>
    )
  }

  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

const App = () => {
  return <Counter />
}

ReactDOM.render(<App />, document.getElementById('#root'))

カウンターの数字こそが React の state(状態) です。 以下で見るように state が変化することで view(ここでは DOM) が変化します。

初期状態は以下の通りです。

<div>
  <span>0</span>
  <button>+</button>
</div>

countが1のとき、spanh1タグに変化します。これは state の変化に対応します。

<div>
  <h1>1</h1>
  <button>+</button>
</div>

さらにcountが2のとき、h1h2タグに変化します。

<div>
  <h2>2</h2>
  <button>+</button>
</div>

これが「state に対応するシンプルな view を設計する」ということです。なお、countが3以上のときは、またspanタグが表示されます。

なお、公式ドキュメントの文章内で DOM ではなく views と単語が使われているのには理由があります。その理由は最後のLearn Once, Write Anywhereという文章で解説します。

stateに応じてviewが変化するもう1つの例

上記の例は少し分かりづらいかもしれません。もう1つ、私の好きな SWR というデータフェッチのライブラリに記載されているサンプルを引用します。

import useSWR from 'swr'

const Profile: React.FC = () => {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div> // エラー状態のときの UI
  if (!data) return <div>loading...</div>     // ローディング状態の UI
  
  return <div>hello {data.name}!</div>        // データ取得完了の UI
}

本記事では AJAX を使ったデータ取得までは紹介しないため深入りしませんが、SWR というライブラリを使うと API をコールしたときのデータの取得状況に応じて状態が更新されます。

データ取得中という状態は<div>loading...</div>という view で表現し、データ取得完了はプロフィール名を表示する view で表現します。

通信エラーの状態は<div>failed to load</div>という view で表現しています。

改めて、これが「state に対応するシンプルな view を設計する」ことです。

データの変更をトリガーにしてコンポーネントを更新する

rerender

React will efficiently update and render just the right components when your data changes.

「React はデータが変化したとき、まさにそのデータが変化したコンポーネントを効率的に更新して再描画します」と訳します。

この一文を「update and render」と「when your data changes」に分けて考えます。

update and render

React は Virtual DOM をメモリ上に構築します。これは実際の DOM(Real DOM)とは異なります。

Virtual DOM は React コンポーネント内の DOM (React Element)を、実際に描画されている DOM に一致させる役割を担います。

React コンポーネントに状態の変化があれば、React が構築した Virtual DOM が差分を検知します(reconciliation) 。

つまり、update は React コンポーネントから Virtual DOM に対して変更を通知すること、render(描画) は Real DOM に対する変更の反映を指すと私は考えています。

React のレンダリング用のライブラリであるreact-domを使うとき、このReal DOM は HTML を指します。先程の state を view に対応させるという例では、spanタグをh1h2といったタグに更新、描画していました。

「update and render」とは、React コンポーネントの変更を検知し、その変化を Real DOM に反映することです。

when your data changes

data という言葉は React では特別な意味を持ちます。React 内で変化する data とは、state または props のみだからです。

state は上記で見たようなコンポーネント内の状態です。一方、props(property)は「親コンポーネントから子コンポーネントに渡される、子コンポーネント内で変化しない値」です。

なお、子コンポーネントは親コンポーネントの state を参照できません。親コンポーネントの state は子コンポーネントに props という形で渡されるからです。

この props または state が変化すると、React はコンポーネントを再描画します。

私が React について学び始めた頃、この rerender(再描画)という単語の意味を理解するのに時間がかかりました。

簡単に書くと、render とは「React コンポーネントという JS の関数(またはクラス)の初回実行」であり、rerender は「state もしくは props の変化をトリガーとした、関数の(またはクラスコンポーネントの render メソッドの)再実行」です。

この記事では触れませんが、このことを知っていると useEffect という React Hooks の dependencies array(依存配列)の意義を理解しやすくなるかと思います。

さて、先ほどは state が変化することで再描画される例をコードで解説しました。

次は、props の変化をトリガーとしてコンポーネントが再描画される例を記述します。Counter コンポーネントから変化する箇所を Count コンポーネントとして切り出しましょう。

type CountProps = { count: number }

const Count: React.VFC<CountProps> = (props) => {
  if (props.count === 1) {
    return <h1>1</h1>
  } else if (props.count === 2) {
    return <h2>2</h2>
  }

  return <span>{props.count}</span>
}

const Counter: React.VFC = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)

  return (
    <div>
      <Count count={count} />
      <button onClick={increment}>+</button>
    </div>
  )
}

CounterコンポーネントからCountコンポーネントに渡した値が props です。この props は{ count: 0 }{ count: 1 }といったcountをプロパティに持つオブジェクトです。

Counterコンポーネントの state であるcountの値が変更されると、Countコンポーネントが受け取った props も同時に変化します。

そして、Countコンポーネントが受け取った props が変化すると(update)、Countコンポーネントが再描画されます(render)。その際、Countコンポーネントは受け取った props に応じて描画する内容を決定します。

今回はcountの値が1のときは<h1>1</h1>、2のときは<h1>2</h1>を描画し、それ以外の時は<span>3</span>などのspanタグを描画します。

以上が本文で「データが変更されたとき」という表現が指している内容です。

さらに詳しい説明は「コンポーネントと props」「Thinking in React(React の流儀)」 をお読みください。

Declarative views make your code more predictable and easier to debug.

さて、この文章のまとめです。結局、state に 対応する view を作成したり、data(state と props) の変更に伴ってコンポーネントを更新、再描画する Declarative views は何に役立つのでしょうか。

それは「コードの実行過程と結果をより予測可能にして、デバッグをさらに容易にする」ことです。

predictable(予測可能な)は馴染みのない言葉かもしれません。pre は「前もって、予め」、dict は「言う、指し示す」、able は「〜可能な」という意味なので、「こうすればこうなるよねと前もって言い当てられる」というような単語です。

前もって挙動がわかるため、JS を実行せずともコードを読むだけで React アプリケーションの動作が頭の中で想像できます。

なぜ React 製のアプリケーションは予測可能でデバッグが容易なのでしょうか。

それは、どんなに巨大なアプリケーションであっても React で記述されているのであれば、異常終了しない限り全て起こりうる UI がコードとして記述されているからです。 さらに ErrorBoundary を使うことで異常終了時の view すらも制御できます。

この Declarative View の考え方は、@sonatard さんの宣言的UI というスライドでわかりやすく解説されています。特に Vanilla JS や jQuery という命令的な UI 操作と宣言的な UI の対比は白眉です。

「宣言的」なインフラ

宣言的という考え方はフロントエンド独自のものではありません。Infrastructure as Code の文脈でも宣言的なアプローチが主流になっています。

例えば、「コンテナを4つ起動する」という命令を記述するとこの命令を2度実行すればコンテナが8つ立ち上がります。

しかし、「コンテナは4つである」と望ましい状態(desired)を宣言しておくとコンテナの数が0個のときは4つ立ち上げます。何らかの理由でコンテナの数が3つになったときは、合計4つになるように新しいコンテナを1つ立ち上げます。

React の Declarative View も、もちろん後者と同じことを指します。予測可能であることと宣言的であることは密接に関連しているのです。

デバッグが容易である理由

なおデバッグが容易であるということは、アプリケーションにバグが生じる状況の再現が容易であり、バグ修正が簡単であるということです。

なぜなら、バグが生じたときの state や props を再現するだけで、再度バグを発生させられるからです。

例えば、jQuery でバグを生じさせた原因が「ボタンを11回クリックしたこと」であれば、バグを再現するためにブラウザでボタンを11回クリックする必要があります。

しかし React では state や props に11という値を設定したり、 React Developer Tools で直接 state や props を11に書き換えることでバグを再現できます。

命令的に同じ手順を踏まずとも、同様の状態を設定することで UI のバグを再現できるのです。また、異常終了しない限り、React アプリケーションは宣言した通りにしか動きません。つまり、記述したコード以外の動作はしないのです。

これもデバッグを簡単にする要素です。

Declarative まとめ

この文章のまとめです。

  • UI とは、人と機械の相互的なやりとり(インタラクション)を司る部分である
  • 開発者は state に対応する view を用意する
  • state と props の変化によってコンポーネントを再描画する
  • React は 宣言的な View により、コードの動作を予測しやすい

コンポーネントに基づいている(Component-Based)

コンポーネント指向

次は「Component-Based」です。日本語訳と原文を両方記載します。

コンポーネントベース
自分自身の状態を管理するカプセル化されたコンポーネントをまず作成し、これらを組み合わせることで複雑なユーザインターフェイスを構築します。
コンポーネントのロジックは、Template ではなく JavaScript そのもので書くことができるので、様々なデータをアプリケーション内で簡単に取り回すことができ、かつ DOM に状態を持たせないようにすることができます。

Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.
Since component logic is written in JavaScript instead of templates, you can easily pass rich data through your app and keep state out of the DOM.

英文を元に解説を加えます。

自身の状態を管理するカプセル化されたコンポーネントを作る

Build encapsulated components that manage their own state

先程解説した「stateに応じてviewが変化する」という一文では state に対応した view を用意するということが主眼でした。

しかし、この文はコンポーネントが自身の状態を管理することに焦点が当てられています。

コンポーネントが自身の state を管理することは重要です。状態の影響箇所を限定することで、アプリケーション全体の管理コストが下がるからです。「分割して統治せよ」(divide and conquer)戦略ですね。

コンポーネントが自身の状態(ローカルステート)を管理していれば、特定のコンポーネントの挙動を変更するとき、そのコンポーネントを探し出して書き換えるところから作業をスタートできます。

ローカルな状態管理の恩恵を実感するためには、グローバルな関数を使う場面を想像することが近道です。

例えば10ページあるサイトで全ての JS をmain.js1つに記述し、10の HTML からmain.jsを呼び出しているとします。

この場合、どの関数がどのページに影響を与えているか容易には分かりません。仕様変更に伴って HTML や JS 修正をすると複雑さばかりが増していきます。

呼び出し側はコンポーネントの内部実装を知らなくても良い

また、コンポーネントはそれぞれカプセル化されているため、コンポーネントの内部実装を知らなくても呼び出して利用できます。

コンポーネントを呼び出す側が知っていなければならないのは、コンポーネントの名前、ファイルパス、そして props として渡す値です。

中でも props はオブジェクト指向言語のクラスの constructor で受け取る値と似ています(クラスコンポーネントでは明示的でしたが、今は関数コンポーネント全盛期なのでぼかした書き方をしています)。

Counter クラスを作ってCounterコンポーネントと比べてみましょう。

class Counter {
  private count: number;

  constructor(initialCount: number) {
    this.count = initialCount;
  }

  increment(): void {
    this.count++;
  }

  getCount():number {
    return this.count
  }
}

const counter = new Counter(0);

counter.increment();
counter.increment();
console.log(counter.getCount()) // 2

initialCountとして0を与え、Counter クラスをインスタンス化しています。

Counterオブジェクトが2度 increment メソッドを呼び出すと、プロパティ count は2になります。

次のCounterコンポーネントと比べてみてください。

type Props = { initialCount: number }

const Counter: React.VFC<Props> = (props) => {
  const [count, setCount] = useState(props.initialCount)
  const increment = () => setCount(count + 1)

  return (
    <div>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

// 呼び出し側
// <Counter initialCount={0} />

私には呼び出し方がとても似ているように見えます。しかもどちらのinitialCountも型定義できています。

const counter = new Counter(0)
<Counter initialCount={0} />

React コンポーネントの場合、呼び出し側は props として initialCount を渡すことでコンポーネントを生成します。返り値は DOM です。一方、オブジェクト指向言語の例では Counter オブジェクトが生成されました。

また、button を2度クリックすることは、increment関数を実行することに他なりません。この時、spanタグで囲まれた変数countには2が格納され、描画されます。

カプセル化され自身の状態を管理している React コンポーネントは、自身の state を更新するための UI(DOM)も同時に提供します。これがオブジェクト指向言語と異なる点です。

なお、2019年の Custom Hooks の登場により、状態管理をコンポーネント内でせずに Custom Hooks に任せるという選択肢が取れるようになりました。

コンポーネントを組み合わせて複雑なUIを構築する

design comp

compose them to make complex UIs

「それぞれのコンポーネントを組み合わせて複雑な UI を構築する」と書かれています。これは文字通りの意味で、各コンポーネントを組み合わせてアプリケーション全体を作るという意味です。

これはオブジェクト指向言語でオブジェクト同士を協調させ、1つのアプリケーションを作成するのと同じ意味だと私は捉えています。

コンポーネントの組み合わせ方といえば、Atomic Design という考え方があります。記事は2013年に書かれたものですが、先見の明があったため開発現場でそのエッセンスを取り入れる取り組みがされているのを見聞きします。

Atomic Design はあくまでコンポーネントの組み合わせ方の一手段です。

なお2021年現在、Atomic Design を提唱したブログの著者の関心はデザインシステムを構築するための別のコンポーネント設計手法(system-components / recipes / snowflakes) に移っています。

大きくいってしまえば、Atomic Design は GoF のデザインパターンに似た位置付けかもしれません。それを知っていると数多くの場面に適用・応用できるものの、だからといってシステム設計の全ての要件には対応できないということです。

そして、開発者として成長したいならデザインパターンを知っていた方が良いように、フロントエンドを担当する方は Atomic Design の考え方を頭に入れておいた方が良いのも同様です。

なお、GoF のデザインパターンの勘所は田中ひさてるさんのちょうぜつ Advent Calendar 2019 によくまとまっています。

フロントエンドのコンポーネント設計にも銀の弾丸はなく試行錯誤が続けられています。

コンポーネントのロジックはテンプレートではなくJSで書ける

Since component logic is written in JavaScript instead of templates,

テンプレートとは異なり、コンポーネントのロジックが全て JavaScript で書けるというのは大きな利点です。React は、JSX を使うことによりこれを実現しています。

テンプレートというのは、例えば PHP のテンプレートエンジン Blade や ruby の Slim、Node.js の Pug など簡略化した独自記法で HTML を生成する技術を指します。

そもそもプログラムというものは「順次」(逐次実行)、「選択」(if 文)、「反復」(for 文)のまとまりです(構造化プログラミング)。

テンプレートエンジンを使うと HTML に存在しない選択、反復を簡単に記述できます。

以下は Blade で if 文と for 文を記述した例です。

{{-- 選択 --}}
<p>
    @if (count($records) === 1)
        I have one record!
    @elseif (count($records) > 1)
        I have multiple records!
    @else
        I don't have any records!
    @endif
<p>

{{-- 反復 --}}
<ul>
    @foreach ($users as $user)
        <li>This is user {{ $user->id }}</li>
    @endforeach
</ul>

PHP には@を付与する文法は存在しません。選択に対応するための @if@endif、反復のための@foreach@endforeachはテンプレートエンジン blade の独自記法です。

では JSX で置き換えてみましょう。

{/* 選択 */}
<p>
  {records.length === 1
    ? 'I have one record!'
    : records.length > 1
    ? 'I have multiple records!'
    : "I don't have any records!"}
</p>

{/* 反復 */}
<ul>
  {users.map((user, index) => (
    <li key={index}>{user.id}</li>
  ))}
</ul>

こちらは JavaScript の文法で記述されていますね。三項演算子で選択に対応し、mapメソッドで反復に対応しています。

なお、選択に関して React では以下のようにロジックと JSX を分離すると見通しがいい場合があります。

const SomeComponent = (props) => {
  const paragraph =
    props.records.length === 1
      ? 'I have one record!'
      : props.records.length > 1
      ? 'I have multiple records!'
      : "I don't have any records!"

  return <p>{paragraph}</p>
}

上記の記述を見てもわかるように、JavaScript を知っている人にとって、JSX の記法は書き慣れた JavaScript そのものです。

Blade の解説ページには、上記で紹介した以外にも多くの独自記法が紹介されていますが、JSX を使う場合はテンプレートエンジンのように独自の書き方を新たに覚える必要はありません。

実際、React のコアチームにいた Pete Hunt氏(現在は退職済み)は、 2013年の JS Conf of EU のプレゼンで「テンプレートエンジンは技術を分離するが関心を分離していない」と述べています(スライド Rethinking Best Practices、動画 Pete Hunt: React: Rethinking best practices -- JSConf EU)。

彼はこのプレゼンの中で Handlebars を取り上げ、表示ロジックとマークアップは密結合であるべきではなく、むしろ疎結合な小さい単位のコンポーネントを作成することを推奨しています。そして、それらを組み合わせることでアプリケーションを作るべきと主張しています。

また Angular を取り上げ、「directive など多くの概念を作ってしまっている」と話していますが、これは彼の Angular の捉え方を表しています(私自身は Handlebars や Angular は使ったことがないです)。

ここでは冒頭の一文を、React コンポーネントは全て JS で書かれており、JS を知っている人にとって JSX の学習コストが低いことを指しているのだと理解します。

ReactがJSXを使う意義

JSX で覚えなければならないルールは多くありません。実際、JSX は HTML を知っていれば、JS に慣れていないデザイナーさんも使いやすい技術です。

実際、JSX は JS の関数の糖衣構文(シンタックスシュガー)です。 以下のシンプルな JSX を例に挙げましょう。

<p className="main">I have one record!</p>

これを Babel で JavaScript に変換すると、以下のような関数 createElement とその引数に変貌します。

React.createElement(
  "p",
  {className: "main"},
  "I have one record!"
)

React 開発者は、JSX のおかげで変換後のコードを意識せずにコーディング可能です。

また、コンポーネントを一見すると、ロジックと JSX を書く場所が分離されていることもわかります。

type Props = { initialCount: number }

// コンポーネント
const Counter: React.VFC<Props> = (props) => {
  // ロジック
  const [count, setCount] = useState(props.initialCount)
  const increment = () => setCount(count + 1)

  // JSX
  return (
    <div>
      <Count count={count} />
      <button onClick={increment}>+</button>
    </div>
  )
}

ここからさらに一歩踏み込み、Vue.js の SFC を参考にロジックと JSX をそれぞれコンポーネントとして分離する書き方も提案されています(@takepepe さんの「経年劣化に耐える ReactComponent の書き方」)。

簡単にリッチなデータをアプリ内に渡せて、DOMから状態を分離できる

you can easily pass rich data through your app and keep state out of the DOM

「簡単にリッチなデータをアプリ内に渡せて、DOM から state を分離できる」と書かれています。

リッチなデータという単語の意味は定かではないですが、私は「プロパティが多く、ネストの深い JS オブジェクト」と解釈しています。

例えば、GitHub のfacebook/reactレポジトリのメタデータを取得する API をコールすると以下のような他項目のデータを含む JSON が返却されます(一部省略)。

{
  "id": 10270250,
  "name": "react",
  "full_name": "facebook/react",
  "private": false,
  "owner": {
    "login": "facebook",
    "id": 69631,
    "node_id": "MDEyOk9yZ2FuaXphdGlvbjY5NjMx",
  },
  "html_url": "https://github.com/facebook/react",
  "description": "A declarative, efficient, and flexible JavaScript library for building user interfaces.",
  "created_at": "2013-05-24T16:15:54Z",
  "homepage": "https://reactjs.org",
  "size": 162610,
  "stargazers_count": 164556,
  "license": {
    "key": "mit",
    "name": "MIT License",
    "url": "https://api.github.com/licenses/mit",
    "node_id": "MDc6TGljZW5zZTEz"
  },
  "forks": 33007,
  "open_issues": 723,
  "watchers": 164556,
  "organization": {
    "login": "facebook",
    "id": 69631,
    "node_id": "MDEyOk9yZ2FuaXphdGlvbjY5NjMx",
    "avatar_url": "https://avatars.githubusercontent.com/u/69631?v=4",
  }
}

全ての項目は 実際の JSON をご覧ください。

React では、JSON をパースすると JS のオブジェクトとして扱えます。このとき、JS オブジェクトはプロパティに関数を持たないため、データの集合と呼べます。

今回は API からのデータ取得までは踏み込みません。このため、上記の JSON 相当のオブジェクトを変数dataに格納して解説します。

以下のように App コンポーネントの中で変数dataを得られるならば、あとは React コンポーネント内の子や孫以下のコンポーネントに簡単に渡せます。

type TitleProps = {
  name: string
  fullName: string
}

const Title: React.VFC<TitleProps> = (props) => (
  <h1>
    {props.name}{props.fullName}</h1>
)

type DetailProps = {
  description: string
  stargazersCount: number
  licenseName: string
}

const Detail: React.VFC<DetailProps> = (props) => (
  <>
    <p>{props.description}</p>
    <p>stars: {props.stargazersCount}</p>
    <p>license: {props.licenseName.toUpperCase()}</p>
  </>
)

const App: React.VFC = () => {
  const data = {
    id: 10270250,
    name: "react",
    full_name: "facebook/react",
    // 省略
  }

  return (
    <div>
      <Title name={data.name} fullName={data.full_name}/>
      <Detail
        description={data.description}
        stargazersCount={data.stargazers_count}
        licenseName={data.license.name}
      />
    </div>
  )
}

<Detail>コンポーネントの表示/非表示は、このコンポーネントの state であるshownの値によって決まります。

これが「state から DOM を分離できる」ということです。

useStateは DOM とは関係がありません。useStateの返り値 shown(状態)とtoggle(状態の更新関数)を DOM の中で使っているだけです。つまり、DOM はuseStateに依存していますが、useStateは独立しています。

これにより状態を DOM から切り離せるのです。さらに、Custom Hooks を作って状態管理をコンポーネントから独立させることも可能です。

const App: React.VFC = () => {
  const [shown, toggle] = React.useState(false)

  const data = { ... } // 省略

  return (
    <div>
      <Title name={data.name} fullName={data.full_name} />
      {shown && (
        <Detail
          description={data.description}
          stargazersCount={data.stargazers_count}
          licenseName={data.license.name}
        />
      )}
      <button type="button" onClick={() => toggle(!shown)}>
        {shown ? 'close' : 'open'}
      </button>
    </div>
  )
}

これは余談ですが、状態管理をコンポーネントから分離し、単一の JS オブジェクトで集中的に(Single source of truth)行えるようにしたものが Redux です。

このため、Redux もまた DOM とは無関係です。Redux は React でも Vue.js でも使えますし、DOM すらなくても利用可能です(実用的ではありませんが)。

Redux はkeep state out of the DOMを突き詰めたライブラリといえるでしょう。

Component-Based まとめ

この文章のまとめです。

  • React コンポーネントはカプセル化されている
  • コンポーネント同士を組み合わせてアプリケーションを構築する
  • JSX を使うことで、DOM 構築に新しいことを覚えなくて済む
  • React は全て JS で記述されている
  • DOM と state は分離されている

JSX の挙動についてさらに詳しく知りたい方は、@sadnessOjisan 氏の「どうしてJSXを使ってもエラーにならないのか?」がおすすめです。

一度学べばどこでも書ける(Learn Once, Write Anywhere)

一度学べばどこでも書ける

最後は「Learn Once, Write Anywhere」です。

React と組み合わせて使用する技術に制限はありません。React を使って新しい機能を追加する際に、既存のソースコードを書き換える必要はありません。
React は Node を使ったサーバ上でもレンダーできますし、React Native を使うことでモバイルアプリケーションの中でも動きます。

We don’t make assumptions about the rest of your technology stack, so you can develop new features in React without rewriting existing code.
React can also render on the server using Node and power mobile apps using React Native.

React 以外の技術スタックは仮定していないので、既存コードを書き変えずに新機能を開発できる

We don’t make assumptions about the rest of your technology stack,

「React 以外の技術スタックは仮定していないので、既存コードを書き変えずに新機能を開発できる」と書かれています。

React は JavaScript で記述されているため、npm で管理されているあらゆる JS パッケージを利用できます。これはつまり豊かな JS エコシステムの資産をフル活用できることに直結します。

React の公式ドキュメントでは remarkable を使って、マークダウンを HTML に変換するサンプルが提示されています。

これ以外にも、TypeScript や flow を使った静的型付けが簡単に始められるのも全て React が JS で記述されていることに由来すると考えています。

また、「新機能の開発で既存コードを書き換えずに済む」というのは Progressive(漸進的)に React を導入できるということだと考えています。

SPA ではなくても、HTML の中に新しくdivタグを用意して React で記述したコンポーネントをマウントすれば、ある画面の一部分だけ React で実装したことになります。

React はサーバーでもモバイルアプリでも描画できる

virtual dom

さて、最後のセンテンスは React を深く知るために最も重要な文章です。

あえて深入りしませんでしたが、実は React はツリー構造の Virtual DOM(仮想 DOM) を構築して差分検知をするライブラリです。

Virtual DOM については公式ドキュメントに解説があります が、@mizchi 氏の WEB+DB PRESS Vol.106 (Amazon) の「仮想DOM革命 ReactでGUI設計が変わる!」という章を読む方が理解は深まります。

簡単にまとめると Virtual DOM とは「実際のツリー構造に対応するメモリ内に構築した仮想のツリー構造そのもの」と「実際のツリー構造と仮想のツリー構造との差分検知アルゴリズム」です。差分検知・更新の概要については React’s diff algorithm という記事で簡潔に解説されています。

つまり、React は Virtual DOM を構築するライブラリであり、Virtual DOM は実際のツリー構造との差分検知に使われるものです。

ではなぜ Virtual DOM を構築する React を使えば Web アプリケーションを作れたり、React Native を使えばモバイルアプリが制作できるのでしょうか。

それは、React が検知した差分を通知する相手が異なるからです。

Web アプリケーションではreact-domという Renderer を使います。React は Virtual DOM の差分を検知するとreact-domに通知します。react-domの内部には JS の DOM 操作 API が実装されているので、通知に基づいて実際の DOM を書き換えます。

モバイルアプリであれば、React はreact-nativeという Renderer に検知した差分を通知します。

さらに、独自の Renderer を作れば、view は DOM や モバイルアプリに限定されません。

実際、ink というライブラリを使うと Terminal の画面を React で構築できます。また、react-360 を使えば、VR の画面を構築できました(現在 react-360 はアーカイブ済み)。

なお、Node.js でも描画できるというのは SSR(サーバーサイドレンダリング)のことです。 ReactDOMServer というオブジェクトを使えば可能とのことですが、私はあまり詳しくありませんのでここまでにしておきます。

react 360

(アーカイブされた React 360 の開発画面)

Learn Once, Write Anywhere まとめ

この章のまとめです。

  • React は Virtual DOM を構築し、差分検知をするライブラリ
  • Renderer に応じて UI を表示する対象を自由に変えられる

なお、Virtual DOM に関しては実際に作ってみるのが一番です。 Rodrigo Pombo氏 による building your own react という記事を読んで私も作ってみました

詳細は忘れてしまいましたが、泥臭い実装なんだなと思った記憶があります。日本語では 「仮想DOMは本当に“速い”のか? DOM操作の新しい考え方を、フレームワークを実装して理解しよう」 という記事があります。

また、この章に関しては 「ReactはウェブやHTMLとは特に関係のないライブラリです」 という記事もおすすめです。

この記事で語っていないこと

この記事で紹介した内容は React に関する一部分だけです。以下のことは本記事では触れていません。

React に関する議論としてとよく取り上げられるのは、Redux での状態管理や Rest vs GraphQL といったデータ取得方法、また CSS の話やTypeScript での型パズル辛いといったものでしょうか。

この他にも React に関して様々なテーマがありますが、よくよく話を聞いてみると React 開発に付随する個別テーマだったりします。

React とは何かを知っていると問題の切り分けが容易になり、検索ワードに見当をつけられたり、テーマごとに頭を切り替えられます。

この記事で記述していないテーマに関しても、ぜひ深掘りしていただければと思います。

まとめ

最後までお読みいただきありがとうございました。

あるライブラリやフレームワークに対する理解を深める方法は、そのツールが解決したい課題の考察や解決アプローチに関する公式ドキュメントの記載を読み、そこで導入された概念を知り、コードを手元で試し、また公式ドキュメントを読み直すことです。

公式ドキュメントで紹介されている抽象的な概念と具体的な実装を行き来することで、自分の認識を何度も改めることが上達につながるのだと考えています。

そして、学習の面白いポイントは認識が変わって「世界の見え方が変わる」ところだと思います。この世界の見え方が初学者と熟練者では異なるという好例を 「問題解決大全」(Amazon) という名著から引用します。

物理学の初心者と熟達者の知識の構造を比較した研究では、初心者の場合、「斜面」の概念に「傾き」や「角度」などの知覚的な特徴が結びつけられ知識は構造化されていたが、熟達者の場合は「エネルギーの保存則」や「ニュートンの力の法則」といった物理学の原理が結びつけられていた。

つまり、知識が深まるに従って、知識の量が増えるだけでなく、知識を構成するネットワークの再編成が起こると考えられる。何かを知るとは、自分の知識のネットワークを拡張するだけでなく、組み替えることでもある。

(出典「問題解決大全」読書猿、2017年)

この記事を読んでくださった方の React や React のコードに対する見え方をアップデートできたなら、私にとって望外の喜びです。

脚注
  1. UI の説明はあまりうまくないと思います。UI についてより深く探究したい方は、Apple の Human Interface Guidelines が参考になるかもしれません。 ↩︎

Discussion