最終的に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ファイルを、import・export文を通して利用する仕組みです。
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に近く、空要素には必ずスラッシュ/
を入れる必要があります。
ReactDOM
ReactコンポーネントとDOM要素をつなぐ役割を果たします。
上記の例では、静的メソッドrender
を利用して、<div id="root">
配下にAPPで定義したHTMLが展開されるようにしています。
対話による状態変更
ボタンのクリックに応じて、文字が切り替わるようにしてみます。
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ページを表現します。
renderメソッド
サブクラスで必ず定義しなくてはならないのが、render
メソッドです。
render
メソッドでは、コンテンツをどう描画するのかをJSXなどで定義した情報を返す必要があります。
コンテンツに変更があれば、Reactはrender
メソッドを呼び出し、定義に基づいてコンテンツに紐づくHTML要素を更新します。
この際、全体を丸ごと更新するのではなく、差分を確認して必要な部分のみ更新してくれます。
**※**差分の確認およびDOM要素の更新は、ReactDOMが担っています。
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>
);
}
}
無名関数を直接渡す場合は、アロー関数を利用します。
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は、関数コンポーネントに追加できる様々な機能群です。
クラスコンポーネントでしか出来なかったことが、関数コンポーネントでも出来るようになります。
公式によると、クラスコンポーネントはトランスパイル後のソースにて、関数コンポーネントほど最適化されていないようです。
今のところクラスコンポーネントを将来的に廃止するとかそういう話にはなっていませんが、これからはシンプルな関数コンポーネントの組み合わせによる構成が推奨されているようです。
ステートフック
ステートフックは関数コンポーネントにも内部状態を持たせる仕組みです。
これまで、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.state
とthis.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']
});
自己定義関数
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等の値が無い場合、配列のインデックス値を適用することも可能ですが、インデックスは随時振りなおされるものなので、あまりお勧めは出来ません。
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を通して管理するのが望ましいです。
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>
Flux
上記例では、TodoのテキストのリストはTodoList
コンポーネントが、項目ごとの完了未完了の状態はTodo
コンポーネントが保持しています。
大きなアプリケーションでは、このstateが各コンポーネントに散らばっていることで、管理が煩雑になってしまうかもしれません。
Reactを提供するFaceBookでは、アプリケーションの状態を一元的に管理する設計手法として、Fluxという考え方を提示しています。
Fluxに関しては、以前に投稿したこちらの記事がございます。
- FluxによるReactアプリの状態管理 Flux・FluxUtils編
- FluxによるReactアプリの状態管理 Redux・React-Redux編
- FluxによるReactアプリの状態管理 ReactHooks編
デベロッパーツール
公式からデバッグ用ツールが提供されています。