2020.05.04

React入門チュートリアル (2) JSX


この連載記事は、これから React を学びたい JavaScript 開発者のための入門コンテンツです。対象とする React のバージョンは執筆時点で最新の v16.13 です。連載記事は以下の通り。

1️⃣ Reactとは何か
2️⃣ JSX
3️⃣ 属性と状態
4️⃣ フォームとイベントハンドリング
5️⃣ ToDoアプリを作ってみよう
6️⃣ 副作用
7️⃣ カスタムフック
8️⃣ Reactプロジェクトを始める方法

⚠ 理解を助けることを意図としているため、網羅的、リファレンス的な解説はしていません。ドキュメントを併読すると、さらに理解が深まると思います。

React の基本となる API はとてもシンプルです。JSXpropsuseStateuseEffect の4つだけ覚えれば、とりあえず React アプリケーションの開発を始めることができるでしょう。それ以外の機能については、ある程度慣れてから覚えていけば OK です。

本章では、React の基本中の基本、DOM を生成するメソッドである createElement と、その構文糖衣(シンタックスシュガー)である JSX について学びましょう。

createElement

以下は、React を使わずに JavaScript だけで DOM を生成して描画するコードです。

<div id="root"></div>

<script>
  const element = document.createElement('p');
  element.id = 'the-text';
  element.className = 'text';
  element.innerText = 'Hello world';
  const root = document.getElementById('root');
  root.insertAdjacentElement('beforeend', element);
</script>

結果として出来上がる DOM は以下の HTML で表されます。

<div id="root">
  <p id="the-text" class="text">Hello world</p>
</div>

React を使って同じ DOM を描画するコードは以下の通りです。普通の HTML ファイルで簡単に試せるように、<script> 要素で CDN から React を読み込んでいます。

<div id="root"></div>

<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<script>
  // React 要素を生成する
  const element = React.createElement('p', { id: 'the-text', className: 'text' }, ['Hello world']);

  // 描画元の親要素を取得する
  const root = document.getElementById('root');

  // React 要素を描画する
  ReactDOM.render(element, root);
</script>

ここでは二つのライブラリを読み込んでいます。React と ReactDOM です。React は Web 以外の場面での利用も想定されています。React で作成した UI を、Web 向けに、つまり DOM にレンダリングするためのライブラリが、ReactDOM です。

さて、要素を作成するメソッドが、以下のシグネチャを持つ React.createElement です。

説明
第一引数 文字列 描画したい要素名
第二引数 オブジェクト 描画したい要素に渡す属性
第三引数 文字列 or 配列 描画したい要素の子要素

第三引数に渡す子要素は、以下のように第二引数の children として渡すこともできます。

const element = React.createElement('p', {
  id: 'the-text',
  className: 'text',
  children: ['Hello world']
});

children(または第三引数)は、配列で複数の子要素を指定できます。たとえば以下の HTML で表される DOM を出力したい場合は…

<ul class="list">
  <li class="list-item">one</li>
  <li class="list-item">two</li>
  <li class="list-item">three</li>
</ul>

このようなコードになります。

const element = React.createElement('ul', { className: 'list' }, [
  React.createElement('li', { className: 'list-item' }, ['one']),
  React.createElement('li', { className: 'list-item' }, ['two']),
  React.createElement('li', { className: 'list-item' }, ['three'])
]);

const root = document.getElementById('root');

ReactDOM.render(element, root);

React.createElementReact 要素を返却します。React 要素は、仮想 DOM を構築するための、JavaScript の HTMLElement とは異なる、React 独自のオブジェクトです。

そして、以下のシグネチャを持つ ReactDOM.render により、特定の HTML の中に、作成した React 要素を描画します。

説明
第一引数 React 要素 描画したい要素
第二引数 HTML 要素 React 要素を内部に描画する親要素

JSX

JSX は createElement である

React.createElement はシンプルな関数ですが、複雑な入れ子になった DOM 構造を表現しようとすると、可読性に難がありそうです。そこで、JSX という特殊な構文を利用します。

JSX は JavaScript の拡張構文であって、標準機能ではありません。そのため、Babel を利用して、ブラウザが読み取れる JavaScript に変換する必要があります。

簡易的に JSX を試す(*2)ため、CDN から Babel を読み込んだ状態で、<script> 要素の typetext/babel を指定します。すると、実行時に変換が実行されます。

<p> 要素を描画する例は、JSX では以下のように記述できます。

<div id="root"></div>

<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<script type="text/babel">
  const element = <p id="the-text" className="text">Hello world</p>;
  const root = document.getElementById('root');
  ReactDOM.render(element, root);
</script>

HTML に似せた JSX の構文は、機能的には createElement と同一です。以下の2つのコードは全く同じ意味を持ちます。というのも、上の JSX コードは、Babel によって下の createElement を使ったコードに変換されるのです。

// JSX
const element = <p id="the-text" className="text">Hello world</p>;

// 👇 上の JSX は Babel によって下のコードに変換される

// createElement
const element = React.createElement('p', { id: 'the-text', className: 'text' }, ['Hello world']);

ブラウザの開発者ツールで変換後のコードを覗いてみると、実際に変換されていることが確認できるでしょう。

さて、改めて JSX コードを見てみましょう。JSX の構文は HTML に似せて作られてはいますが、当然 HTML そのものではありません。似ているのに少し違うので最初は戸惑うかもしれませんが、React.createElement のコードとの対比で考えると、理解しやすいでしょう。

<p id="the-text" className="text">Hello world</p>

createElement の第一引数が JSX のタグになり、第二引数が HTML でいうところの属性(attribute)の形で渡され、第三引数が入れ子の子要素として記述されます。

この属性値を props と呼びます。後の説明にも出てくるので覚えておいてください。

第三引数は第二引数の children プロパティとして渡すこともできるので、以下のように書いても同じ意味です。上のタグに挟む書き方のほうが見やすいので、あえてこちらの記述を選択することはないでしょうが。

<p id="the-text" className="text" children="Hello world" />

子要素のない要素は、/> で閉じてやる必要があります。HTML はこの辺のルールは緩いですが、JSX の場合はエラーになるので注意しましょう。

また、リストの例を JSX で書くと以下のようになります。

const element = (
  <ul className="list">
    <li className="list-item">one</li>
    <li className="list-item">two</li>
    <li className="list-item">three</li>
  </ul>
);

const root = document.getElementById('root');
ReactDOM.render(element, root);

JSX が複数行になる場合は、括弧 () で括ります。

Fragment

JSX には、単一のルート要素を持つべき、というルールがあります。つまり、下のコードはエラーで実行できません。

// ❌ Error
const element = (
  <article>...</article>
  <aside>...</aside>
);

このように、他の要素でまとめてやると、単一のルート要素(この例では <div>)になるので、実行可能になります。

// ✅ Not bad
const element = (
  <div>
    <article>...</article>
    <aside>...</aside>
  </div>
);

ただ、JSX の制約のために本来不要な要素を追加するのは不本意ですよね。React から提供される Fragment 要素を利用すれば、この問題を解決できます。

// ✅ Better!
const element = (
  <React.Fragment>
    <article>...</article>
    <aside>...</aside>
  </React.Fragment>
);

この JSX は、以下の HTML で表される DOM を生成します。

<article>...</article>
<aside>...</aside>

つまり、Fragment は JSX 的には要素とみなされますが、DOM としては何も出力されません。さらに、Fragment には以下のシンタックスシュガーも用意されています。

// ✅ More better!
const element = (
  <>
    <article>...</article>
    <aside>...</aside>
  </>
);

実際はこちらの記述を使うことがほとんどです。

値を埋め込む

JSX にはテンプレートエンジンのように、変数を埋め込むことができます。JSX 内では、{} で括られた箇所は JavaScript として解釈されます。

JSX
const name = 'John';

const element = <p>Hello, {message}</p>;
// 👉 <p>Hello, John</p>

{} の中に記述できるのは、のみです。文を記述することはできません。JSX 内で条件分岐を行う場合は、演算子を用いて式として記述します。

// ❌ Error
<p>{if (hello) return 'Hello'}, world</p>

// ✅ Good
<p>{hello && 'Hello'}, world</p>
<p>{hello ? 'Hello' : 'Goodbye'}, world</p>

リスト描画と key

配列を元にループ処理を行いたい場合は、map で JSX の配列を返します。

const members = [
  { name: 'John', instrument: 'guitar' },
  { name: 'Paul', instrument: 'bass' },
  { name: 'George', instrument: 'guitar' },
  { name: 'Ringo', instrument: 'drums' }
];

const element = (
  <ul>
    {members.map(member => (
      <li key={member.name}>
        {member.name} plays {member.instrument}
      </li>
    ))}
  </ul>
);

// 👇
// <ul>
//   <li>John plays guitar</li>
//   <li>Paul plays bass</li>
//   <li>George plays guitar</li>
//   <li>Ringo plays drums</li>
// </ul>

少しややこしいですが、<ul> の次の { から JavaScript の世界で、map の引数の戻り値である ( からまた JSX の世界になります。その中の {} は JavaScript として評価されます。

ループ処理で JSX を記述する場合、繰り返し生成される要素には key プロパティが必要です。key は、React が DOM 生成を最適化するのに使用します。出力はされません。

key の値はその繰り返し内で一意でなくてはいけません。別のループ箇所で使われている値と重複しても問題はありません。

Fragmentkey が必要な場合、省略形 <></> ではなく、明示的に宣言します。

<One>
  {items.map(item => (
    <React.Fragment key={item.id}>
      <Two>{ item.two }</Two>
      <Three>{ item.three }</Three>
    </React.Fragment>
  ))}
</One>

ドキュメントも併せて確認しましょう。

コンポーネントを定義する

React では、<div><p> のような HTML ネイティブな要素に留まらず、独自要素を作ることもできます。独自要素は、コンポーネントとも呼ばれ、関数として定義します。

<div id="root"></div>

<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<script type="text/babel">
  // React コンポーネント
  function Text(props) {
    return <p id={props.id} className="text">{props.children}</p>;
  }

  const root = document.getElementById('root');
  ReactDOM.render(<Text id="the-text">Hello world</Text>, root);
</script>

React コンポーネントは、以下の条件を満たした関数です。

  • ネイティブの要素と区別するために、必ず関数名は大文字で始める。
  • React 要素(または null)を返却する。

null を返すのは、何らかの処理のみを行い、DOM を出力しない独自要素を作成する場合です。今のところは、そういうこともある、くらいに思っていてください。

独自要素を表す関数は、第一引数に、属性値をまとめたオブジェクトを取ります。これは React.createElement でいうと第二引数にあたります。

仮引数部分にスプレッド構文を用いて、以下のように記述することも多いです。

function Text({ id, children }) {
  return <p id={id} className="text">{children}</p>;
}

以下の例のように、同名の HTML 要素があったとしても、大文字から始まっていれば独自要素と解釈されます。

function Button() {
  // ...
}

const elem = <Button>click me</Button>;

JSX の意義

JSX を初めて目にして、若干の気持ち悪さを感じた方がいるかもしれません。私もそうでした。JavaScript の中に HTML が書かれているなんて。なぜ、こんな変テコな構文をわざわざ発明したのでしょう?

その意味は、以下の2つあると思います。

  • 既存の HTML 要素と React コンポーネントを透過的に扱える。
  • 宣言的に UI を記述できる。

まず一つ目についてですが、構文を HTML に似せたことで、既存の HTML 要素とオリジナルの React コンポーネントを同列に、同じ場所で操作することができています。インターフェースに統一性があるので、コードが散らかるのを防いでくれます。

Components and HTML elements live together

次に、宣言的なコードについてです。

以下は、React を使わないコードの例です。このようなコードは、「手続き的」と表現されます。上から順番に、実行したいことを手続きとして並べて書いていくスタイルです。

function text({ id, children }) {
  const elem = document.createElement('p');
  elem.className = id;
  elem.className = 'text';
  elem.innerHTML = children;
  return elem;
}

この書き方の欠点は、「結局どうなるの?」という、最終形が判別しにくいことです。上の例はとてもシンプルで DOM 操作という一種類のことしかしていませんが、現実のコードはもっと複雑で、間にデータの取得や操作のようなビジネスロジックが混ざります。かなり気を遣わないと、どこからどこまでがなにをやっているのかも、判別しにくくなります。

逆に、以下は React を使ったコード例ですが、こちらはより「宣言的」と言えます。DOM を操作する手続きは書かれていません(その部分は React が面倒を見るので書く必要がない)。その代わり、「あるデータによって、どのような DOM が生成されるか」が書かれています。

function Text({ id, children }) {
  return <p id={id} className="text">{children}</p>;
}

「こういう風にする」という手続きに対し、「最終的にこうなる」という宣言が書かれるのが、「宣言的」という言葉の意味です。私はこちらの方が圧倒的に見やすいと思います。

もちろん React を使っていても、気を抜くと手続き的な、読み取りづらいコードになってしまいますが、指針がなにもないよりはマシですよね。

おまけ:CodePen の使い方

最後に、CodePen の使用方法について触れておきます。

CodePen は、プレビュー付きの HTML + CSS + JavaScript のコードスニペットを作成、共有できる Web サービスです。本文中のサンプルコードや練習問題の解答で使っていきます。

ちょっとした学習や検証、プロトタイピングに使えるので、便利ですよ 😇

"Pen" と呼ばれるスニペットを作成すると、以下の編集画面が得られます。

  • HTML ブロックには body 要素の内容
  • CSS ブロックには読み込むスタイルシートの内容
  • JavaScript ブロックには読み込む JS コードの内容

を記述すれば、プレビューブロックにレンダリング結果が出力されます。

JavaScript ブロックに React コードを書くには、少し設定が必要です。"JS" の左の歯車マークをクリックすると、以下の設定モーダルウィンドウが開きます。

  • "JavaScript Preprocessor" に "Babel" を選択します。
  • "Add External Scripts/Pens" に React と ReactDOM を指定します。検索欄から「react」などと検索すれば候補が出てくるのでそれを選べば OK です。

練習問題

問題 1

以下の HTML に表される DOM を出力する React コードを書いてください。
(JSX を使用します。)

<img src="https://media.giphy.com/media/33OrjzUFwkwEg/giphy.gif" alt="" />

🤫 ヒント

以下の Gif コンポーネントの中身を実装しましょう。

function Gif({ id }) {
  /* Gif コンポーネントを実装する */
}

const app = <Gif id="33OrjzUFwkwEg" />;

const root = document.getElementById('root');
ReactDOM.render(app, root);

🙌 解答例

こちらの CodePen を参照してください。

問題 2

以下の HTML に表される DOM を出力する React コードを書いてください。
(JSX を使用します。)

<section id="react" class="box">
  <h1 class="title">React</h1>
  <dl class="definition">
    <dt class="definition-title">Initial release</dt>
    <dd class="definition-content">2013/5</dd>
    <dt class="definition-title">Github stars</dt>
    <dd class="definition-content">147,940</dd>
  </dl>
</section>
<section id="vue" class="box">
  <h1 class="title">Vue.js</h1>
  <dl class="definition">
    <dt class="definition-title">Initial release</dt>
    <dd class="definition-content">2014/2</dd>
    <dt class="definition-title">Github stars</dt>
    <dd class="definition-content">163,165</dd>
  </dl>
</section>
<section id="angular" class="box">
  <h1 class="title">Angular</h1>
  <dl class="definition">
    <dt class="definition-title">Initial release</dt>
    <dd class="definition-content">2016/9</dd>
    <dt class="definition-title">Github stars</dt>
    <dd class="definition-content">60,571</dd>
  </dl>
</section>

🤫 ヒント

  1. Section, DefinitionList の2つのコンポーネントを作成してください。
  2. Section は、id, title, 子要素の3つの props を取ります。title で渡された文字列は、<h1> 要素に描画されます。
  3. DefinitionList は、props として items を取ります。items は以下の形式の配列で、title<dt> 要素に、content<dd> 要素に描画されます。
[
  { title: '...', content: '...' },
  { title: '...', content: '...' }
]

🙌 解答例

こちらの CodePen を参照してください。

連載

  1. Reactとは何か
  2. JSX
  3. 属性と状態
  4. フォームとイベントハンドリング
  5. ToDoアプリを作ってみよう
  6. 副作用
  7. カスタムフック
  8. Reactプロジェクトを始める方法

  1. これはあくまでデモやプロトタイピングのための設定方法です。本格的なセッティングについては後の章で説明します。