この連載記事は、これから React を学びたい JavaScript 開発者のための入門コンテンツです。対象とする React のバージョンは執筆時点で最新の v16.13 です。連載記事は以下の通り。
Reactとは何か
JSX
属性と状態
フォームとイベントハンドリング
ToDoアプリを作ってみよう
副作用
カスタムフック
Reactプロジェクトを始める方法
理解を助けることを意図としているため、網羅的、リファレンス的な解説はしていません。ドキュメントを併読すると、さらに理解が深まると思います。
React の基本となる API はとてもシンプルです。JSX
、props
、useState
、useEffect
の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.createElement
は React 要素を返却します。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>
要素の type
に text/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 として解釈されます。
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
の値はその繰り返し内で一意でなくてはいけません。別のループ箇所で使われている値と重複しても問題はありません。
Fragment
に key
が必要な場合、省略形 <></>
ではなく、明示的に宣言します。
<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 コンポーネントを同列に、同じ場所で操作することができています。インターフェースに統一性があるので、コードが散らかるのを防いでくれます。
次に、宣言的なコードについてです。
以下は、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>
ヒント
Section
,DefinitionList
の2つのコンポーネントを作成してください。Section
は、id
,title
, 子要素の3つの props を取ります。title
で渡された文字列は、<h1>
要素に描画されます。DefinitionList
は、props としてitems
を取ります。items
は以下の形式の配列で、title
が<dt>
要素に、content
が<dd>
要素に描画されます。
[
{ title: '...', content: '...' },
{ title: '...', content: '...' }
]
解答例
こちらの CodePen を参照してください。
連載
-
これはあくまでデモやプロトタイピングのための設定方法です。本格的なセッティングについては後の章で説明します。
↩