Posted at

Reactの基本まとめ(Hooks含む)

最終的にReactでTodoリストを構築します。

Reduxなどの状態管理ライブラリは含みません。


Reactとは

Reactは、クライアントサイドでWebページのレンダリングを行うサイトSPA(Single Page Application)を構築する際に便利なライブラリーです。

SPAでは、Webページ表示後も引き続きクライアント側でコンテンツの表示非表示等の状態管理を、継続的に行う必要があります。

Reactでは、予めJSによるロジックとHTMLがセットになった部品(コンポーネント)を用意しておき、それらを組み合わせることでWebページを構成する、コンポーネントベースと呼ばれる考え方を採用しています。

Reactは内部でJSのロジックと対応するDOM要素をマッピングしており、JS側の処理に応じて自動でDOM要素を更新してくれます。

このあたりの仕組みは仮想DOMと呼ばれています。

静的ページやサーバーサイドレンダリングのページでは、引き続きjQueryも便利かなと思っています。

Reactの本家サイトは、最近日本語にも対応しておりとても充実しています。

ここのチュートリアルを一通り実践することもお勧めいたします。

Node.jsとnpmの利用環境が既に整っていることを前提としています。

Node.js / npmをインストールする(for Windows)

過去に投稿したこちらの記事でも触れています。

Node.jsとExpressでローカルサーバーを構築する(1) ―Node.jsとnpmの導入―


環境構築

React公式では、Create React Appというスターターキットを用意しています。

今回は、簡単にはじめられるParcelを利用して環境を構築したいと思います。


インストール

React本体と、ReactとDOM要素をつなげるためのReactDOMをインストールします。

$ npm install --save react react-dom

引き続き、ParcelとBabelのReact用プリセットをインストールします。

$ npm install --save-dev parcel-bundler @babel/preset-react


トランスパイラ

Reactは、通常のJavaScript構文だけで構築することも可能ですが、基本的にJSXと呼ばれる構文を利用するのが一般的です。

ブラウザに導入されているわけではないので、トランスパイラと呼ばれるツールを用いてJavaScripの構文に変換(トランスパイル)する必要があります。

このトランスパイルには一般的にBabelというツールが用いられます。

公式提供スターターキットのCreateReactAppでも内部でBabelが利用されているようです。

Babel自体は、自分で色々設定が出来る自由度の高いツールであると同時に、設定が面倒なツールでもあります。

ParcelはこのBabelを内包した上で、細かな設定をせずともすぐに使えるように作られています。

プリセットは、Babelのオプション用パッケージのことを指します。

ReactのJSXのトランスパイルはオプション機能として提供されている為、別途インストールが必要です。

Parcelの場合、インストールするだけで設定は特に不要です。


バンドラ

es2015にて、JavaScripにモジュール機能(ESModules)が導入されました。

用途ごとに分割された外部JSファイルを、importexport文を通して利用する仕組みです。

HTMLファイル上での読み込み順序を気にすることなく、JS側でファイルの依存関係の管理が完結します。

また、各モジュール毎にスコープが閉じているので、ファイル間での名前の衝突の心配がなくなります。

既にブラウザに機能導入が進んでいるようです。

対象のJSファイルを読み込む際に<script type="module">とすると、ESModulesの対象となります。

ただ、読み込むファイル数が増えればリクエストの回数も増えてしまうので、今のところモジュールバンドラーというツールを用いて、ある程度まとめてしまう(バンドルする)のが一般的です。

モジュールバンドラーは色々ありますが、特に人気なのはwebpackでしょうか。

webpackもBabelと同様、自由度が高い分、設定の手間が多いツールです。

実は、Parcelはバンドルもしてくれます。

こちらも細かな設定は不要なので、インストールした時点でほぼほぼ環境構築が完了しています。

JavaScript modules | MDN

webpackとBabelについては、過去に投稿した記事もございます。

webpackとBabelの基本を理解する(1) ―webpack編―


Reactコンポーネントの基本


JSXをHTMLで出力する

ひとまず、Reactを使って何かしらのHTMLを出力してみましょう。

下記のフォルダ構成でファイルを用意します。

ファイルの拡張子はjs以外にjsxも使えます。

root

└ src
├ index.html
└ index.jsx

<!DOCTYPE html>

<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>React Todo</title>
</head>
<body>
<div id="root"></div>
<script src="index.jsx"></script>
</body>
</html>

// Reactパッケージの読み込み

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

// Reactコンポーネント
class App extends React.Component {
render() {
return (
<h1>Hello React!</h1>
);
}
}

// HTMLタグにReactコンポーネントを紐付ける
ReactDOM.render(
<App />,
document.getElementById('root')
);

rootフォルダで下記コマンドを実行してみます。

npx parcel src/index.html


Parcelによるバンドル

初回は./distフォルダが作成され、html,js,mapファイルが出力されています。

コマンドで指定したhtmlをスタート地点として、<script>で読み込んでいるJSファイルからimport文を頼りに芋づる式に辿って、一つのファイルにまとめていきます。

出力されたJSファイルには暫定的な名前がつけられ、HTMLファイルの方もこれを参照するように書き換わっています。

mapファイルは、元ファイルと出力後ファイルのコードの対応を示す情報です。

ブラウザのDevToolなどでデバッグする際に、元ファイルの方を参照することが出来ます。

実は、Parcelはテストサーバも立ち上げてくれます。

コンソールに出力されたローカルサーバのURLにアクセスすると、出力を確認できます。

また、特に指定をしなければwatchモードで起動する為、関連ファイルを編集すると勝手にコンパイル(トランスパイルとバンドル)を実行してくれます。


カスタムコンポーネント

React.Componentクラスを継承したサブクラスAppが、ユーザ定義のReactコンポーネント(カスタムコンポーネント)です。

名前は大文字で始める必要があります。

Appクラスのrenderというメソッドにて、戻り値を定義している部分に書かれているHTMLのようなものがJSXです。

正確にはXMLに近く、空要素には必ずスラッシュ/を入れる必要があります。

React.Component | React


ReactDOM

ReactコンポーネントとDOM要素をつなぐ役割を果たします。

上記の例では、静的メソッドrenderを利用して、<div id="root">配下にAPPで定義したHTMLが展開されるようにしています。

ReactDOM | React


対話による状態変更

ボタンのクリックに応じて、文字が切り替わるようにしてみます。

class App extends React.Component {

constructor(props) {
super(props);
this.state = {
isMorning: true
};
}
render() {
return (
<div>
<button
onClick={e => {
this.setState(
{ isMorning: !this.state.isMorning }
);
}}
>
Click
</button>
<h1>
{this.state.isMorning ? 'Good Morning' : 'Hello'} React!
</h1>
</div>
);
}
}

buttonタグをクリックする度に文字が切り替わるようになりました。

このコンポーネントは、内部で状態isMorningを保持・管理しています。

クリックするとisMorningの状態が切り替わり、それに応じてHTMLも再レンダリングされます。

renderメソッド内のJSXの部分が、通常のHTMLとは様子が異なってきました。

onClickはHTMLタグのonclick属性とは異なる、JSX側の構文です。

用途はonclick属性と同様、HTML要素にイベントハンドラを設置する為のものです。

h1のテキストコンテンツ部分には、JSの三項演算子が追加されました。

isMorningの値に応じて異なる文字列を返すようにしています。

つまりHTMLにJSのコードを埋め込んだものがJSXです。

埋め込み部分は波括弧{ }で囲みます。

PHPなどでテンプレートページを作る時と要領は似ており、JSの実行結果がHTML上に描画されます。

コンテンツのデータ、そのデータの処理ロジック、描画が一通りセットになったオブジェクトがReactコンポーネントです。

このコンポーネントを適宜組み合わせることで、状況に応じたWebページを表現します。

JSXの導入 | React


renderメソッド

サブクラスで必ず定義しなくてはならないのが、renderメソッドです。

renderメソッドでは、コンテンツをどう描画するのかをJSXなどで定義した情報を返す必要があります。

コンテンツに変更があれば、Reactはrenderメソッドを呼び出し、定義に基づいてコンテンツに紐づくHTML要素を更新します。

この際、全体を丸ごと更新するのではなく、差分を確認して必要な部分のみ更新してくれます。

差分の確認およびDOM要素の更新は、ReactDOMが担っています。

render() | React


stateとsetState

前項で「コンテンツに変更があれば」と表しましたが、renderメソッドにの実行に関わる情報は、厳密にはコンポーネントの「状態」を表す情報です。

このコンポーネントの「状態」を表す情報は、クラスのstateプロパティとして、コンストラクタ内で定義します。

そして、このstateプロパティを更新する専用メソッドsetStateを利用して、コンポーネントの状態を更新します。

するとrenderメソッドが呼び出され、stateに紐づくHTML要素が再レンダリングされます。

プロパティをstate以外の名前で定義したり、直接更新するなどのsetStateメソッド以外の手段でstateを更新した場合、renderは呼ばれません。

上記サンプルコードにてボタンがクリックされた時に行っていることは、状態の変更だけです。

this.state.isMorningの値を変更しているだけです。

isMorningの値に紐づいているh1タグは、Reactが勝手に更新してくれます。

HTMLのDOM要素を直接管理する手間から開放され、JavaScript上でのデータ管理を気にかけるだけで済みます。


setStateを利用したstateの更新例

setStateで渡されたstateの断片は、最終的にthis.stateにマージされます。

配列を利用する場合は一旦複製する必要があります。

// stateを複数定義する

this.state = {
stateA: 'hoge',
stateB: [ 'cat', 'dog' ]
};

// 特定のstateを更新する
this.setState({
stateA: 'moimoi'
});

// 配列の場合は、一旦複製
const stateB = this.state.stateB.slice();
stateB.push('rabbit');
this.setState({ stateB });

// 下記では再描画が発生しない
this.state.stateB.push('rabbit');
this.setState({
stateB: this.state.stateB
});

Reactは、更新内容を最小限にとどめる為に差分チェックをします。

setStateを実行しても、前回と値に変わりが無ければ再レンダリングされません。

その際はObject.isのアルゴリズムに基づいて比較を行います。

配列は、保存されたメモリの位置でざっくりと比較されるので、新規配列を渡す必要があります。

const stateA = ['cat', 'dog'];

const stateB = stateA; //stateAもstateBも同じ配列を参照している
const stateC = stateA.slice();

stateB.push('rabbit');
stateC.push('rabbit');

console.log(Object.is(stateA, stateB)); // true
console.log(Object.is(stateA, stateC)); // false

setState() | React

state とライフサイクル | React


イベント処理

イベントハンドラに無名関数を渡すのではなく、クラスのメソッドを設定する場合は以下のようになります。

Reactに限った話ではありませんが、イベントリスナーを登録する際にelement.addEventListener('click', this.someMethod)の要領でメソッドをそのまま渡しても、コールバックで実行された関数内でのthisの参照はそのメソッドが属するオブジェクトにはなりません。

素のJSでは、イベントを登録したDOM要素がthisになります。Reactの場合はundefinedでした。

その為、コンストラクタ内にてbindメソッドで所定のオブジェクトをthisとして参照する関数を生成して、上書きしています。

class App extends React.Component {

constructor(props) {
super(props);
this.state = {
isMorning: true
};
// コールバック関数内で、thisの参照がこのクラスを指すための設定
this.handleClick = this.handleClick.bind(this);
}
// クリック時のハンドラ
handleClick(e) {
this.setState(
{ isMorning: !this.state.isMorning }
);
}
render() {
return (
<div>
<button onClick={this.handleClick} >
Click
</button>
<h1>
{this.state.isMorning ? 'Good Morning' : 'Hello'} React!
</h1>
</div>
);
}
}

無名関数を直接渡す場合は、アロー関数を利用します。

イベント処理 | React


propsによるデータの受け渡し

複数のコンポーネントを組み合わせてこそのReactなので、ボタンを別のコンポーネントとして切り出してみます。

// 子コンポーネント

// ボタンコンポーネント
class MyButton extends React.Component {
// state等の宣言をしない場合は省略可
constructor(props) {
super(props);
}
render() {
return (
<button onClick={this.props.onClick} >
{this.props.text}
</button>
);
}
}

// 親コンポーネント
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isMorning: true
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
this.setState(
{ isMorning: !this.state.isMorning }
);
}
// <button>の代わりに<MyButton />
render() {
return (
<div>
{/* 文字列とイベントハンドラを渡す */}
<MyButton text="Click" onClick={this.handleClick} />
<h1>
{this.state.isMorning ? 'Good Morning' : 'Hello'} React!
</h1>
</div>
);
}
}

JSX上で、一つのタグとしてコンポーネント名を記述します。

この際、子コンポーネントとなるコンポーネントに値を渡すことが出来ます。

受け取った側のコンポーネントは、this.propsからこの値にアクセスできます。

コンストラクタ内で受け取っているpropsがそうです。

this.propsは読み取り専用です。this.stateの様に更新することは出来ません。

this.propsもまた、更新されるとrenderメソッドが呼び出され、紐づくDOM要素が再レンダリングされます。

外部から渡される読み取り専用データpropsと、内部で管理する制御データstate、この二つがコンポーネントのレンダリング実行に関係します。

尚、Reactコンポーネントが大文字で始める必要があるのは、通常のHTMLタグと区別する為です。

<div>はhtmlのdivタグとして認識されますが、<Div>はReactコンポーネントと見なされます。

スコープ内に該当コンポーネントが無い場合はエラーになります。


関数コンポーネント

上記ボタンコンポーネントは、内部で状態stateを持ちません。

受け取ったpropsをJSXで展開するのみです。

この場合、クラス構文ではなく関数の構文で定義する方が好ましいです。

// 一つの連想配列でpropsを受け取る

function MyButton(props) {
return (
<button onClick={props.onClick} >
{props.text}
</button>
);
}

// 分割代入が便利
function MyButton({ onClick, text }) {
return (
<button onClick={onClick} >
{text}
</button>
);
}

renderメソッドと同じく、JSXを返します。

Reactがインポートされているモジュール内であれば、そのままReactコンポーネントとして認識されます。


Hooks

ReactのVer.16.8から、新機能Hooksが導入されました。

Hooksは、関数コンポーネントに追加できる様々な機能群です。

クラスコンポーネントでしか出来なかったことが、関数コンポーネントでも出来るようになります。

公式によると、クラスコンポーネントはトランスパイル後のソースにて、関数コンポーネントほど最適化されていないようです。

今のところクラスコンポーネントを将来的に廃止するとかそういう話にはなっていませんが、これからはシンプルな関数コンポーネントの組み合わせによる構成が推奨されているようです。

フックの導入 | React


ステートフック

ステートフックは関数コンポーネントにも内部状態を持たせる仕組みです。

これまで、stateを保持する場合はクラスコンポーネント、保持しない場合は関数コンポーネントと使い分けていたのが、Hooksを利用すると関数コンポーネントでもstateを持つことが出来ます。

上記のクラスコンポーネントAppをHookを利用した関数コンポーネントに書き換えてみます。

import React, { useState } from 'react';

function App() {
// useStateの引数はstateの初期値
const [isMorning, toggleFlag] = useState(false);

return (
<div>
<MyButton
onClick={e => toggleFlag(!isMorning)}
text="Click"
/>
<h1>
{isMorning ? 'Good Morning' : 'Hello'} React!
</h1>
</div>

);
}

useState関数をインポートして利用します。

必ず、関数コンポーネント直下のスコープで実行する必要があります。

引数はstateの初期値です。

戻り値は配列です。0番目が現在のstate、1番目がstateを更新する為の関数です。分割代入で受け取っています。

それぞれ、クラスコンポーネントのthis.statethis.setStateと同じ役割を果たします。

初回実行時はuseStateの引数で渡した初期値をstateとして受け取ります。

2回目以降は現在のstateの値(更新用関数で更新した場合は更新後の値)を受け取ります。

必要に応じて複数のstateを用意することも出来ますし、クラスコンポーネントの様に一つのオブジェクトで管理する事もできます。

function Example() {

// 別々にstateを用意する
const [count, setCount] = useState(0);
const [animal, setAnimal] = useState(['cat']);

// もしくは一つのオブジェクトで定義
// 但し、this.setStateとは異なり、古いものにマージされるのではなく置換されるので注意
const [state, setState] = useState({
count: 0,
animal: ['cat']
});

ステートフックの利用法 | React

自己定義関数

useState関数はレンダリングの度に実行されますが、引数が有効なのは初回レンダリング時のみです。

実際に中のソースがどうなっているのかは分かりませんが、挙動としては自己定義関数が近いでしょうか。

let animal = (first) => {

// 初回しか実行されない処理
console.log(first);
// 自らを上書き
animal = () => {
// 2回目以降に実行される処理
console.log('cat!');
};
};

animal('dog!'); // dog!
animal('dog!'); // cat!
animal('dog!'); // cat!


Todoアプリ

シンプルなTodoアプリを作成します。


Todoリストを表示する

ひとまず、予め用意したTodoリストを表示するコンポーネントを作成します。

リストは配列で管理します。

import React, { useState } from 'react';

import ReactDOM from 'react-dom';

// Todo項目
function Todo({ text }) {
const [completed, setState] = useState(false);

return (
<li
onClick={e => setState(!completed)}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
);
}

// Todoリスト
function TodoList() {
const [todos, setTodo] = useState(
[
'牛乳 2本',
'卵 10ヶ入 1パック',
'食パン 5枚切り 1袋'
]
);

// todosを基に<Todo />の配列を作成
return (
<ul>
{todos.map((todo, index) => (
<Todo key={index} text={todo} />
))}
</ul>
);
}

ReactDOM.render(
<TodoList />,
document.getElementById('root')
);

コンポーネントTodoは、一つのtodo項目を表します。

完了の状態を保持し、クリックすると完了を示す打ち消し線が表示され、再度クリックすると未完了の状態になります。

JSXにてstyle属性を適用したい場合は、オブジェクト{style名:値}を渡します。

text-decorationの様なハイフン-を含む名前は、ローワーキャメルケースで指定します。

コンポーネントTodoListは、todoリストを保持します。

配列をJSX内で展開する場合はmapメソッドを利用します。

その際、key値も指定する必要があります。

keyはReactが内部でリストの各要素を識別する為に用いる為、リスト内で一意の値でなくてはなりません。

固有のID等の値が無い場合、配列のインデックス値を適用することも可能ですが、インデックスは随時振りなおされるものなので、あまりお勧めは出来ません。

リストと key | React


Todoを追加する

Todoを自分で入力するためのコンポーネントを追加します。

function Todo({ text }) {

/* 変更なし */
}

// Todoを追加する
function AddTodo({ addTodo }) {
const [inputText, setInputText] = useState('');

return (
<form
onSubmit={e => {
e.preventDefault(); // 素のJSのEvent.preventDefault()と同じ
if (!inputText.trim()) {
return;
}
addTodo(inputText);
setInputText('');
}}
>
<input
type="text"
value={inputText}
onChange={e => setInputText(e.target.value)}
/>
<button type="submit">Add Todo</button>
</form>
);
}

// Todoリスト
function TodoList() {
const [todos, setTodo] = useState([]);

return (
<div>
<AddTodo addTodo={newTodo => setTodo(todos.concat(newTodo))} />
<ul>
{todos.map((todo, index) => (
<Todo key={index} text={todo} />
))}
</ul>
</div>
);
}

ReactにてInputフォームを利用するにあたり、Input要素の状態(value属性の値)を管理する為のstateを用意しています。

入力値を受け取りtodosを更新する関数を、propsを通してTodoListからAddTodoに渡しています。


Ref

DOM要素に直接アクセスする方法として、Refがあります。

React.createRef()メソッドを利用してRefを作成します。

そのRefを対象のReact要素のref属性に設定すると、currentプロパティを通してDOM要素に直接アクセスすることが可能になります。

function AddTodo({ addTodo }) {

let textInput = React.createRef();

return (
<form
onSubmit={e => {
e.preventDefault();
const elInput = textInput.current;
if (!elInput.value.trim()) {
return;
}
addTodo(elInput.value);
elInput.value = '';
}}
>
<input
type="text"
ref={textInput}
/>
<button type="submit">Add Todo</button>
</form>
);
}

HTML要素ではなく、カスタムクラスコンポーネントのref属性にRefを適用した場合、curretプロパティでアクセスできるのは、そのクラスのインスタンスです。

関数コンポーネントはインスタンスが存在しない為、ref属性は利用できません。

あまり多用するとReactを利用するメリットが薄くなるので、基本的にはstateとpropsを通して管理するのが望ましいです。

RefとDOM


Todoに表示フィルタを追加する

Todoリストを「全て・完了・未完了」で切り替えられるよう、フィルター機能を追加します。

// 表示切替用フィルタ

const VisibilityFilters = {
SHOW_ALL: 'All',
SHOW_COMPLETED: 'Completed',
SHOW_ACTIVE: 'Active'
};

function Todo({ text, filter }) {
const [completed, setState] = useState(false);

if (
(filter === VisibilityFilters.SHOW_ACTIVE && completed)
|| (filter === VisibilityFilters.SHOW_COMPLETED && !completed)
) {
// 表示対象外の場合はnullを返す(非表示)
return null;
}

return (
<li
onClick={e => setState(!completed)}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
);
}

function AddTodo({ addTodo }) {
/* 変更なし */
}

// フィルターボタン
function Link({ active, onClick, children }) {
// 予めオブジェクトで定義して展開するのもあり
const params = {
onClick: onClick,
disabled: active,
style: { marginLeft: '4px' }
};

return (
<button {...params}>
{children}
</button>
);
}

// 表示の切替
function Footer({ filter, setFilter }) {
const linkList = Object.values(VisibilityFilters);

return (
<div>
{linkList.map(myFilter => (
<Link
key={myFilter}
active={myFilter === filter}
onClick={e => setFilter(myFilter)}
>
{myFilter}
</Link>
))}
</div>
);
}

// Todoリスト
function TodoList() {
const [todos, setTodo] = useState([]);
const [filter, setFilter] = useState(VisibilityFilters.SHOW_ALL);

return (
<div>
<AddTodo addTodo={newTodo => setTodo(todos.concat(newTodo))} />
<ul>
{todos.map((todo, index) => (
<Todo
key={index}
text={todo}
filter={filter}
/>
))}
</ul>
<Footer filter={filter} setFilter={setFilter} />
</div>
);
}

TodoListに、現在のフィルターを表す新たなstatefilterを追加しました。

Todoコンポーネント内では、フィルターと項目の完了状態が一致しない場合にnullを返すようにしています。

nullを返すと、そのコンポーネントはレンダリングの対象から外れます。


props.children

Reactコンポーネントは、空要素以外にコンテンツを閉じタグで囲って記述することも可能です。

この場合、子要素はprops.childrenで取得できます。

他のReactコンポーネントを渡すことも可能で、コンポーネントのネストが深くなることを防ぐことが出来ます。

function Child(props) {

return <p>{props.children}</p>
}

function OtherChild(props) {
return <span>{props.text}</span>
}

function Parent() {
return (
<Child>
<OtherChild text="HOGE" />
</Child>
);
}

//<p>
// <span>HOGE</span>
//</p>

props.children | React


Flux

上記例では、TodoのテキストのリストはTodoListコンポーネントが、項目ごとの完了未完了の状態はTodoコンポーネントが保持しています。

大きなアプリケーションでは、このstateが各コンポーネントに散らばっていることで、管理が煩雑になってしまうかもしれません。

Reactを提供するFaceBookでは、アプリケーションの状態を一元的に管理する設計手法として、Fluxという考え方を提示しています。

Fluxに関しては、以前に投稿したこちらの記事がございます。


デベロッパーツール

公式からデバッグ用ツールが提供されています。