フロントエンドエンジニアじゃないけれど React を書きたい人のための実践入門(前編) #Zaim
こんにちは、Zaim でフロントエンドやサーバーサイドの開発を担当している takeshy です。
Zaim では Web 版を大幅に作り直す計画があり、そのための技術として Go, React, TypeScript などを採用することが決まっています。
Zaim は、iOS や Android といったアプリ開発者が比較的多く在籍している会社です。そうした「フロントエンド以外の開発担当者」や「これからフロントエンドを学びたいデザイナー」向けに、React と Redux に関する社内勉強会を開きました。
せっかくなので、その時に作成したスライドの資料を元に、note 記事として公開することにしました。
改めて React おさらい
React は「コンポーネント指向の UI ライブラリ」です。コンポーネントとは、他から呼び出されることを前提にインターフェースが定められた、パーツとして独立したソフトウェア部品のことを指します。
React の場合は、コンポーネント内に
表示内容(HTML)
スタイル(CSS)
処理(Javascript)
をパッケージングしています。これにより、React コンポーネントを引数付で生成するだけで、専用の UI・UX が実現できます。
React とそれ以外との比較
コンポーネント化の有無による違いを、具体的なサンプルコードで示してみます。
コンポーネント化されていない Web の場合
index.html
<html>
<head>
<script src="./index.js"></script>
<link rel="stylesheet" href="./style.css"/>
</head>
<body>
<button id="sample_btn">Click Me!</button>
</body>
</html>
style.css
#sample_btn{
color: red;
}
index.js
document.addEventListener("DOMContentLoaded",
function(){
document.getElementById("sample_btn").addEventListener("click",
function(){ alert("Hello") }
);
}
);
部品(HTML)と処理(Javascript)やスタイル(CSS)はバラバラで、クラス名や ID 名などの文字列で連携しています。
Javascript の場合はクラス名や ID 名を動的に生成したり、関係がないファイルでも同じクラス名を使っているだけだったりする場合があり、単純な grep では影響範囲が読み切れません。
React コンポーネントの場合
index.html
<html>
<head>
<script src="./index.js"></script>
</head>
<body>
<div id="root"/>
</body>
</html>
SampleButton.jsx
import React from "react";
const styles = {
btn: {
color: "red"
}
}
export default function(){
return (
<button style={styles.btn} onClick={()=> alert("hello")}>
Clicke Me!
</button>
);
}
index.js
import React from "react";
import ReactDOM from "react-dom";
import SampleButton from "./SampleButton";
document.addEventListener("DOMContentLoaded", function(){
ReactDOM.render(<SampleButton />, document.getElementById("root"));
});
React コンポーネントを使う側は、SampleButton.jsx を import して、<SampleButton> タグを呼び出すだけで、 パッケージングされた部品(HTML)と処理 (Javascript)やスタイル(CSS)を取得できます。
ES6 modules の機能である import により、どこでどのように定義しているのかの依存関係も、明示的です。
補足
React を使う場合は、コンパイルして Javascript に変換する必要があります。前述のサンプルを実行したい場合は、以下を参考にしてください。
# node v8.10 以上
npx create-react-app sample1
cd sample1
src/index.js の内容を上記の index.js に置き換え、src/SampleButton.jsx を作成。上記の SampleButton.jsx を書き込んだ上で、以下を実行してください。
npm start
React コンポーネント
React コンポーネントの作成
React のコンポーネントを作成する方法は「Class Component」と「Functional Component」の 2 パターンがあります。
Class Component
React.Component を継承し、React コンポーネントを返す render メソッドを実装したクラスです。
コンポーネント内に state という内部変数を保持でき、state が変更されると React は再度 render メソッドを呼び出します。これにより、リアクティブに表示を更新することが可能です。
また、描画前や廃棄前といった lifecycle のイベントに対してハンドラを設定することで、描画前に API を呼び出したり、廃棄前にタイマーを解除したりを実現しています。
Functional Component
props を引数に取り、React コンポーネントを返す出力関数のみで構成されています。状態を持たずに props によって出力が一意に決まるため、出力される仮想 DOM の予測がしやすいのが特徴です。
補足
React 16.8 で React Hooks API が追加されています。Class Component でしかできなかった機能が、Functional Component でも使えるようになりました。
ただし、Class Component に対して、今後もサポートを続けていくと公式で表明されています。
状態を持たない Functional Component によりReact Componentを作成するのは、出力される仮想 DOM を予測しやすい、Component を再利用しやすいといった観点から、今後も重要です。
今まで使っていた Class Component が React Hooks API を使った Functional Component になるという捉え方で、以降の説明を見てください。
React コンポーネントの種別
React は UI ライブラリであるものの、あらかじめ与えられたデータを使って表示するだけでは静的ページと変わりがありません。実務では、Ajax でデータを取得したり、クリックなどのイベントによって状態を変えたりする処理が必要になるでしょう。
ただ、こうした処理が UI の部分に入ってしまうと見通しが悪くなり、再利用しづらいものとなってしまいます。
そこで React の開発では、Container Componentと Presentational Component に分けて開発することが推奨されています。
Container Component
API 呼び出しや state を使った状態管理、イベントハンドラの実装など、ロジックの実装部分を担うコンポーネントのことです。API の結果や state の内容、イベントハンドラの実装を props として、Presentational Component を呼び出します。
React 16.8 以前は Class Component によって作られていましたが、最近は Hooks API を使って Functional Component で生成されるようになってきています。
Presentational Component
props を使って、見せ方のみに集中したコンポーネントです。Functional Component で作ります。
props から取得したデータやイベントハンドラを使うため、例えばデータが localStorage からではなく API 経由になったとしても、Presentational Component は影響を受けません。
コードはほぼ JSX で見た目は HTML のため、Javascript に詳しくないデザイナーの方でも修正・反映することも容易です。
日英・単語変換のサンプルアプリ
サンプルの概要
それでは Container Component と Presentational Component の違いを、サンプルアプリを使って見てみます。具体的には「日本語の単語に対して英語の単語を登録するアプリ」になります。
localStorage にデータを保存することで、再読み込みしてもデータが引き継がれるようにしています。
ClassComponent を使った Container Component 例
app.js
import React from "react";
import WordList from "./WordList";
class App extends React.Component {
constructor(props) {
// constructorを定義する時は必ずsuper(props)を呼ぶ
super(props);
// 初期化時のみstateに直接代入できる
this.state = { japanese: "", english: "", words: [] };
// bindでthisをひもづけることで、イベントハンドラ内のthisが
// このインスタンスのthisになります
this.update = this.update.bind(this);
this.register = this.register.bind(this);
this.save = this.save.bind(this);
}
// componentDidMountというメソッドを定義するとコンポーネントが読み込まれた後に実行される
componentDidMount() {
const item = localStorage.getItem("words");
if (item) {
this.setState(JSON.parse(item));
}
}
//input更新用のイベントハンドラ
update(e) {
// stateを変更する場合はsetStateに新しいstateを渡すことで、Reactが
// 変更を検知できるようにする
// ...はスプレッド演算子と呼ばれ、マージする際に使われるReactでは必須のイディオム
// フィールドごとに更新ハンドラを容易するのは大変なので、name属性にstateのキー名が
// セットされている前提とすることで同じハンドラを使いまわせるようにします
this.setState({
...this.state,
[e.currentTarget.attributes.name.value]: e.currentTarget.value
});
}
//登録用のイベントハンドラ
register(e) {
//SPAの場合は実際にsubmitすることはないため、常にFormのsubmit処理をキャンセルする
e.preventDefault();
// setStateの第2引数にsetState実行後のcallbackを渡すことが可能。
// この例ではstateをlocalStorageに保存するために使用
this.setState(
{
japanese: "",
english: "",
words: [
...this.state.words,
{ japanese: this.state.japanese, english: this.state.english }
]
},
this.save
);
}
//永続化処理
save(e) {
localStorage.setItem("words", JSON.stringify(this.state));
}
render() {
return (
<WordList
japanese={this.state.japanese}
english={this.state.english}
words={this.state.words}
update={this.update}
register={this.register}
/>
);
}
}
export default App;
単語アプリの機能の部分を実装している Container Component です。コンポーネントが生成される際に constructor が呼ばれるので、そこで state の初期化や各メソッドの bind を実行しています。
よく使う lifecycle 用のメソッドには、コンポーネント読み込み後の componentDidMount とコンポーネント破棄前の componentWillUnmount があります。componentDidMount は API 呼び出しなど、一度だけ実行したい処理に使い、componentWillUnmount はタイマーの解除等のリソースの後始末に利用することが多いです。
イベントハンドラの登録は Presentational Component で実行しますが、そのイベントハンドラ自体は Container Component で実装することで、Presentational Component が読みやすく、再利用性が高まります。今回は LocalStorage を使いましたが、Web API を使う場合も Presentational Component を変更することなく実装できます。
Presentational Component へ渡す props の数がそこそこ多くなるものの、昨今 React は TypeScript で使うことがほとんどのため、IDE によるサジェストやチェック機能が充実していて、それほど困りません。
Functional Component を使った Presentational Component 例
WordList.jsx
import React from "react";
export default props => {
return (
<div>
<form onSubmit={props.register}>
日本語{" "}
{
// Reactで管理するためにinputタグに対してonChangeのイベントハンドラと
// valueの値の指定は必須。
}
<input
type="text"
name="japanese"
onChange={props.update}
value={props.japanese}
/>
英語{" "}
<input
type="text"
name="english"
onChange={props.update}
value={props.english}
/>
<input type="submit" value="登録" />
</form>
<table>
<thead>
<tr>
<th>日本語</th>
<th>英語</th>
</tr>
</thead>
<tbody>
{
// 配列の場合はそれぞれにkey属性にその配列でユニークな値をセットする必要
// この例では同じ単語は登録されないという規約を前提に単語名をkeyにしている
}
{props.words.map(word => {
return (
<tr key={word.japanese}>
<td>{word.japanese}</td>
<td>{word.english}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
単語アプリの表示の部分を実装している Presentational Component です。
与えられた props を元に React Component を返すだけの関数です。
props の値が同じであれば、同じ内容の React Component が返ることになり、予期しやすい安定したパーツになります。JSX を使うことで、サーバーサイドの View 部分とほぼ同じ感覚で書くことが可能です。
サンプルアプリの実行
実際に動かすには、下記の手順を参考にしてください。
# node v8.10以上
npx create-react-app sample2
cd sample2
src/index.js の内容を下記に置き換えます。
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
src/App.js の内容を上記の App.js に書き換えます。src/WordList.jsx を作成し、上記の WordList.js に更新してください。
npm start
ターミナルから上記コマンドを実行すれば、完成です。
終わりに
思いの外、長くなってしまったので前後編に分けることにしました。後半では、この続きとして Storybook や JSX の実践的な使い方を解説したいと思います。
Zaim ではアプリだけではなく、Web も大幅に改善して使いやすくしていく方針のため、Web に興味のあるエンジニアの方、お気軽に「話を聞いてみたい」をお待ちしています!