はじめに
本投稿の背景と目的
Reactの公式チュートリアルは大変よくできているので、初学者がReactの基本を学ぶためには、まずは公式チュートリアルに沿って手を動かしていくのが一番だと思います。
私自身、公式チュートリアルをやってみて基本的な部分の理解はできたものの、以下の点が自分にとっては課題に感じました。
- TypeScriptで書きたい場合にどうすればよいか分からず、いろいろ調べる必要があった
- 三目並べという題材が若干トリッキーに感じた
というわけで、TypeScriptを使ってReactに入門するための、よりシンプルな題材のチュートリアルを作成することが本投稿の目的です。
私自身が初学者なので、初学者目線での説明を心がけていますが、わかりにくい箇所や間違っている箇所などありましたら忌憚のないコメントを頂けると有り難いです。
(2020-09-22追記)
TypeScriptでReact Hooksに入門するチュートリアルも投稿しましたので、本チュートリアルを終えた後はそちらもご参照頂けると幸いです。
(2020-10-11追記)
「次に学ぶこと」の章を追加しました。
前提とする知識
- HTMLの基本知識
- TypeScriptの基本知識
開発環境
チュートリアル本編は、CodePenを使ってブラウザ上でステップ・バイ・ステップでアプリケーションを開発していく流れとなっています。
ローカル開発環境で動かしたい場合は、(Appendix) ローカル開発環境のセットアップを参考にしてください。
動作確認済みの環境
Mac OS X 10.15.2node v11.10.1npm 6.12.0react 16.12.0typescript 3.7.4
これから作るもの
このチュートリアルでは、とあるテーマパークの入場料計算を行うWebアプリケーションを作成します。
以下のような、カテゴリごとの人数を入力すると合計の人数と金額を計算するシンプルな画面です。
最終的な結果をここで確認することができます:最終結果
チュートリアルを進める前に実際に画面を操作して動作を把握しておくことをお勧めします。
スターターコードの中身を確認する
ブラウザから、このスターターコードを新しいタブで開いてください。
スターターコードではコンポーネントの雛形が用意してあり、ウィンドウ下部のペインには以下にような単純な(動作しない)画面が表示されているはずです。
コードを見てみると、以下の4つのReactコンポーネントがあることがわかります。コンポーネントとは、UIを組み立てるための、小さく独立した部品を指します。
-
Detail(入力用の明細) -
Summary(合計を表示) -
AdmissionFeeCalculator(明細、合計をまとめたもの) -
App(最上位のコンポーネント)
Summaryのコードを見てみましょう。
コンポーネントはReact.Componentを継承したクラスとして作成し、renderメソッドを実装します。renderメソッドでは、JSXと呼ばれる構文で記述したDOMの表現を返却します。
JSXは基本的にHTMLの構文で記述することができますが、以下の点は注意してください。
- 最上位の要素は一つでなければならない(以下のコードサンプルでは
div要素) -
class属性はTypeScriptの予約語と重複するため、classNameと書く必要がある(他にもlabel要素のfor属性はhtmlForと書く、等)
class Summary extends React.Component {
render() {
return (
<div>
<div className="party">
<input type="text" className="party" value="0" />
<span>名様</span>
</div>
<div className="total-amount">
<span>合計</span>
<input type="text" className="total-amount" value="0" />
<span>円</span>
</div>
</div>
);
}
}
コンポーネントは入れ子の構造にすることが可能です。以下のコードサンプルのように、コンポーネントのクラス名がそのままJSXにおける要素名となります。
class AdmissionFeeCalculator extends React.Component {
render() {
return (
<>
<Detail />
<Summary />
</>
);
}
}
最上位の要素は一つでなければならないという制約がありました。もし複数の要素を返したい場合は、それらを束ねるdivタグを上位に配置するという方法も可能ですが、React.Fragment要素を用いるとDOMに余計な要素を加えることなく複数の要素を束ねることができます。上記のコードサンプルでは、React.Fragmentのエイリアスにあたる<></>を使用しており、その配下に複数の要素を並べています。
コンポーネントが必要とするデータをprops経由で渡す
では手始めに、AdmissionFeeCalculatorコンポーネントからDetailコンポーネントにデータを渡してみましょう。
TypeScriptで書くからには、コンポーネントのプロパティもきちんと型付けを行います。
スターターコードをブラウザの新しいタブで開いて、以下のコードを追加しましょう(コードをコピー・ペーストしないで、手でタイプすることをお勧めします)。
type FeeClassification = {
name: string;
description: string;
unitPrice: number;
numOfPeople: number;
totalPrice: number;
}
type DetailProps = {
classification: FeeClassification;
}
FeeClassificationはアプリケーション内で共通的に使用する型の定義です。その下のDetailPropsが実際にDetailコンポーネントが利用するpropsの型定義となります。
次に、Detailコンポーネントのコードを修正します。
React.Componentはpropsの型とstateの型を型パラメータとして受け取るので、先ほど作成したDetailPropsをpropsの型に指定します。stateに関しては後で説明しますので今は気にしないでください。
そして、div要素のコンテンツやselect要素の値にプロパティをバインドします。親のコンポーネントから渡されたプロパティセットはthis.propsで参照することができます。JSXの構文中において{ }で囲まれた範囲はTypeScriptのコードをして扱われます。
class Detail extends React.Component<DetailProps, {}> {
render() {
return (
<div >
<div className="classification-name">{this.props.classification.name}</div>
<div className="description">{this.props.classification.description}</div>
<div className="unit-price">{this.props.classification.unitPrice}円</div>
<div className="num-people">
<select value={this.props.classification.numOfPeople}>
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<span>名</span>
</div>
</div>
);
}
}
親コンポーネントにあたるAdmissionFeeCalculatorでは、Detailコンポーネントにプロパティを渡す必要があります。DetailProps型の各プロパティが、即ちJSXのDetail要素の属性となりますので、以下のようにコードを修正してプロパティを渡してください。
class AdmissionFeeCalculator extends React.Component {
private detail: DetailProps = {
classification: {
name: "大人",
description: "",
unitPrice: 1000,
numOfPeople: 0,
totalPrice: 0
}
};
render() {
return (
<>
<Detail classification={this.detail.classification} />
<Summary />
</>
);
}
}
以下のように画面が表示されるようになったはずです。
正しく表示されない場合は、CodePenのConsoleに何かエラーが出ていないか確認しましょう。
この時点でのコードはこのようになっているはずです。
コンポーネントを繰り返し表示する
次にDetailコンポーネントを複数個繰り返して表示するようにしましょう。
まずは繰り返し表示するデータを、AdmissionFeeCalculatorコンポーネント内に準備します。
class AdmissionFeeCalculator extends React.Component {
private details: DetailProps[] = [
{
classification: {
name: "大人",
description: "",
unitPrice: 1000,
numOfPeople: 0,
totalPrice: 0
}
},
{
classification: {
name: "学生",
description: "中学生・高校生",
unitPrice: 700,
numOfPeople: 0,
totalPrice: 0,
}
},
{
classification: {
name: "子ども",
description: "小学生",
unitPrice: 300,
numOfPeople: 0,
totalPrice: 0,
}
},
{
classification: {
name: "幼児",
description: "未就学",
unitPrice: 0,
numOfPeople: 0,
totalPrice: 0,
}
},
];
renderメソッドでは、まずは複数行の明細を表すJSX(の配列)を作成します。コードサンプルでは配列のmapメソッドを使ってJSX要素の配列へ変換しています。定数detailsJsxの実際の型はJSX.Element[]となります。
ここで注意すべき点は、繰り返し表示するコンポーネントに対してはkey属性を付与する必要があるということです。これは、繰り返し表示する対象のモデルに変更が加えられたときに、変更が発生したアイテムを特定するために必要な識別子です。
key属性には、アイテムを特定可能な任意の値をセットします。今回のサンプルはアイテムの入れ替りは発生しないので、単純にインデックスをキーとしています。
あとは本体のJSX内に明細のJSX配列を展開({detailJsx}の部分)するだけです。
render() {
const detailsJsx = this.details.map((fc, idx) => {
return (
<Detail key={idx.toString()} classification={fc.classification} />
);
});
return (
<>
{detailsJsx}
<Summary />
</>
);
}
画面表示は以下のように変わったはずです。
この時点でのコードはこのようになっているはずです。
コンポーネントをインタラクティブにする
ここまでの実装で画面に4行の明細を表示できるようになりましたが、画面から人数のセレクトの値を変更しても画面上の表示は何も変わりません。変更を反映するためには、変更イベントを拾ってモデルを更新する必要があります。
ここで重要なのは、コンポーネントのプロパティは親コンポーネントから渡されますが、それは読取り専用であるということです。
何らかのイベントによって変更が発生しうる状態は、コンポーネント自身のstateとして管理します。
まずはpropsと同様、stateに対応する型定義を行います。
type DetailState = {
numOfPeople: number;
}
定義した型をReact.Componentを継承する際の二つ目の型パラメータに指定し、コンストラクタではstateを初期化します。
class Detail extends React.Component<DetailProps, DetailState> {
constructor(props: DetailProps) {
super(props);
this.state = {
numOfPeople: props.classification.numOfPeople,
}
}
コンストラクタに続いて、Changeイベントのハンドラを作成し、変更された値でstateを更新します。
この時、stateオブジェクトを直接更新してはいけません。新しいstateオブジェクトを作成して、setStateメソッドを呼び出して更新をするのがReactにおける作法となります。
onNumOfPeopleChange(e: React.ChangeEvent<HTMLSelectElement>): void {
const num: number = Number(e.target.value);
this.setState({
numOfPeople: num,
});
}
renderメソッドのJSXでは、select要素にイベントハンドラを記述します。以下のサンプルコードのように、アロー関数を使った記述がよいでしょう。
<select value={this.state.numOfPeople}
onChange={e => this.onNumOfPeopleChange(e)}>
これで、セレクトの選択値変更が実際に画面上に反映されるようになりました。
この時点でのコードはこのようになっているはずです。
stateを親コンポーネントへリフトアップする
コンポーネント間の連携の流れ
次に、Detailコンポーネントの人数変更に呼応してSummaryコンポーネントの合計人数と合計額を変更するように修正しましょう。
コンポーネントの親子構造を模式的に記述すると以下のようになっていました。
<AdmissionFeeCalculator>
<Detail />
<Detail />
<Detail />
<Detail />
<Summary />
</AdmissionFeeCalculator>
さて、Reactコンポーネントが参照や更新をできるデータは以下の二種類です。
- 親コンポーネントから渡されたプロパティ(props) 〜参照のみ可
- 自分自身の管理するステート(state) 〜参照・更新とも可
Summaryコンポーネントは、兄弟関係にあたるDetailの内部状態を見ることはできません。
ではどうすればよいかというと、以下のような流れでコンポーネント間の連携を行うことになります。
-
Detailコンポーネントのイベントハンドラは、props経由で親コンポーネントに変更を通知する -
AdmissionFeeCalculatorコンポーネントは、自身のstateを更新する -
AdmissionFeeCalculatorコンポーネントのstateの一部は、Summaryコンポーネントのpropsとしてバインドされるため、変更がSummaryコンポーネントへ伝搬して再描画される
順番に実装していきましょう。
AdmissionFeeCalculatorコンポーネントの修正
まず、Detailコンポーネントで管理していた状態を、親にあたるAdmissionFeeCalculatorコンポーネントに引き上げて管理するようにします(リフトアップ)。
type AdmissionFeeCalculatorState = {
feeClassifications: FeeClassification[];
}
class AdmissionFeeCalculator extends React.Component<{}, AdmissionFeeCalculatorState> {
コンストラクタでstateの初期値を設定します。(もともと明細データを格納していたインスタンス変数は削除します)。
constructor(props: {}) {
super(props);
const adults: FeeClassification = {
name: "大人",
description: "",
unitPrice: 1000,
numOfPeople: 0,
totalPrice: 0,
};
const students: FeeClassification = {
name: "学生",
description: "中学生・高校生",
unitPrice: 700,
numOfPeople: 0,
totalPrice: 0,
};
const children: FeeClassification = {
name: "子ども",
description: "小学生",
unitPrice: 300,
numOfPeople: 0,
totalPrice: 0,
};
const infants: FeeClassification = {
name: "幼児",
description: "未就学",
unitPrice: 0,
numOfPeople: 0,
totalPrice: 0,
};
this.state = { feeClassifications: [adults, students, children, infants] };
}
続いて、Detailコンポーネントで発生したChangeイベントを処理するハンドラメソッドをAdmissionFeeCalculatorコンポーネントに作成します。
ここでも、stateの直接更新は禁じられているので、新しいstateオブジェクトを作成してsetStateメソッドを呼び出す手順となります。
handleNumOfPeopleChange(idx: number, num: number) {
const currentFC = this.state.feeClassifications[idx];
const newTotalPrice = currentFC.unitPrice * num;
// 人数と合計額以外は既存の値をコピー
const newFC: FeeClassification =
Object.assign({}, currentFC, { numOfPeople: num, totalPrice: newTotalPrice });
// 新たな配列を生成
const feeClassifications = this.state.feeClassifications.slice();
feeClassifications[idx] = newFC;
// stateの更新
this.setState({ feeClassifications: feeClassifications });
}
次にrenderメソッドを修正します。
-
DetailコンポーネントのonNumOfPeopeChange属性には、先ほど定義したhandleNumOfPeopleChangeメソッドを呼び出すアロー関数を記述します。 -
stateで管理している情報から合計人数と合計額を計算し、Summaryコンポーネントの属性に渡します。 - これらのコンポーネントの属性は、このあと作成するものです。
render() {
const details = this.state.feeClassifications.map((fc, idx) => {
return (
<Detail key={idx.toString()} classification={fc}
onNumOfPeopleChange={n => this.handleNumOfPeopleChange(idx, n)} />
);
});
const numOfPeople = this.state.feeClassifications
.map(fc => fc.numOfPeople).reduce((p, c) => p + c);
const totalAmount = this.state.feeClassifications
.map(fc => fc.totalPrice).reduce((p, c) => p + c);
return (
<>
{details}
<Summary numOfPeople={numOfPeople} totalAmount={totalAmount} />
</>
);
}
Detailコンポーネントの修正
DetailPropsに、親コンポーネントに変更を通知するための関数onNumOfPeopleChangeを定義します。関数の実体は親コンポーネントから渡されることになります。
stateは親コンポーネントに引き上げて不要になったので、DetailStateは削除します。
type DetailProps = {
classification: FeeClassification;
onNumOfPeopleChange: (num: number) => void;
}
自身のonNumOfPeopleChangeメソッドでは、propsのonNumOfPeopleChangeを呼び出すように修正します。
class Detail extends React.Component<DetailProps, {}> {
onNumOfPeopleChange(e: React.ChangeEvent<HTMLSelectElement>): void {
const num: number = Number(e.target.value);
this.props.onNumOfPeopleChange(num);
}
renderメソッド内のJSXのselect要素を以下のように修正します。
<select value={this.props.classification.numOfPeople}
onChange={e => this.onNumOfPeopleChange(e)}>
Summaryコンポーネントの修正
Summaryコンポーネントのpropsの型定義を作成しましょう。
type SummaryProps = {
numOfPeople: number;
totalAmount: number;
}
SummaryPropsをReact.Component継承時の型パラメータに指定するとともに、propsのデータを参照するようにJSXを修正します。
class Summary extends React.Component<SummaryProps, {}> {
render() {
return (
<div>
<div className="party">
<input type="text" className="party" value={this.props.numOfPeople} />
<span>名様</span>
</div>
<div className="total-amount">
<span>合計</span>
<input type="text" className="total-amount" value={this.props.totalAmount} />
<span>円</span>
</div>
</div>
);
}
}
これで、明細の人数を変更すると再計算が行われて、結果がサマリ欄に反映されるようになりました。実際に画面から動作を確認してみてください。
この時点でのソースコードはこのようになっているはずです。
関数コンポーネントに置き換える
最後にリファクタリングを行います。DetailおよびSummaryコンポーネントは自分自身では何の状態も管理していません(言い換えれば、stateを管理していません)。
このような場合は、React.Componentを継承したクラスを作成するのではなく、関数としてコンポーネントを定義することができます。
Detailコンポーネントは以下のように修正します。
- 型は
React.FC<propsの型>とします - propsを受け取ってJSXを返却するアロー関数として定義します
- これまで
this.propsを参照していた箇所は(引数の)propsを参照する形になりますので注意してください
const Detail: React.FC<DetailProps> = props => {
const onNumOfPeopleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const num: number = Number(e.target.value);
props.onNumOfPeopleChange(num);
}
return (
<div >
<div className="classification-name">{props.classification.name}</div>
<div className="description">{props.classification.description}</div>
<div className="unit-price">{props.classification.unitPrice}円</div>
<div className="num-people">
<select value={props.classification.numOfPeople}
onChange={e => onNumOfPeopleChange(e)}>
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<span>名</span>
</div>
</div>
);
}
同様にSummaryコンポーネントも関数コンポーネントに修正します。
const Summary: React.FC<SummaryProps> = props => {
return (
<div>
<div className="party">
<input type="text" className="party" value={props.numOfPeople} />
<span>名様</span>
</div>
<div className="total-amount">
<span>合計</span>
<input type="text" className="total-amount" value={props.totalAmount} />
<span>円</span>
</div>
</div>
);
}
これでチュートリアルはすべて完了です!:最終結果
次に学ぶこと
React 16.8.0でフックが導入されて(React Hooks)以降は、あらゆるコンポーネントを関数型コンポーネントで実装するやり方が主流となっています。クラスコンポーネントも引き続きサポートされ続ける予定なので、本チュートリアルで学んだ内容はこの先も有効ですが、今後はWebや書籍のサンプルもHooksを用いたものが多くなると予想されます。
公式のHooksのドキュメントや、本チュートリアルの続編にあたるTypeScriptでReact Hooksに入門するチュートリアルでHooksにもチャレンジしてみてください。
(Appendix) ローカル開発環境のセットアップ
前提条件
-
nodeとnpmがインストール済みであること
create-react-appでアプリケーションを作成する
$ npx create-react-app ts-tutorial --template typescript
※--typescriptは非推奨となり、--template typescriptとなりました(2020/2/4修正済)
アプリケーション起動
$ cd ts-tutorial
$ yarn start
http://localhost:3000/ でアプリケーションにアクセスできるようになります。(Ctrl+Cで終了)
CSSの修正
src/App.css を以下の内容で置き換えてください。
div.main {
margin: 10px;
}
div.classification-name {
height: 30px;
width: 100px;
float: left;
clear: both;
}
div.description {
height: 30px;
width: 150px;
float: left;
}
div.unit-price {
height: 30px;
width: 100px;
float: left;
}
div.num-people {
height: 30px;
width: 100px;
float: left;
}
div.party {
margin-top: 10px;
height: 30px;
width: 150px;
background-color: azure;
float: left;
clear: both;
}
div.total-amount {
margin-top: 10px;
height: 30px;
width: 200px;
background-color: azure;
float: left;
}
input.party {
width: 50px;
text-align: right;
margin-left: 5px;
margin-right: 5px;
}
input.total-amount {
width: 80px;
text-align: right;
margin-left: 5px;
margin-right: 5px;
}
App.tsxの修正
src/App.tsxを以下の内容で置き換えます。
これで、チュートリアル本編のスターターコードと同等の状態になります。
import React from 'react';
import './App.css';
class Detail extends React.Component {
render() {
return (
<div >
<div className="classification-name">名前</div>
<div className="description">説明</div>
<div className="unit-price">0円</div>
<div className="num-people">
<select value="0">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
<span>名</span>
</div>
</div>
);
}
}
class Summary extends React.Component {
render() {
return (
<div>
<div className="party">
<input type="text" className="party" value="0" />
<span>名様</span>
</div>
<div className="total-amount">
<span>合計</span>
<input type="text" className="total-amount" value="0" />
<span>円</span>
</div>
</div>
);
}
}
class AdmissionFeeCalculator extends React.Component {
render() {
return (
<>
<Detail />
<Summary />
</>
);
}
}
const App: React.FC = () => {
return (
<div className="main">
<AdmissionFeeCalculator />
</div>
);
}
export default App;
ソースコード
ローカル開発版のチュートリアルの最終形はGitHubで公開してあります。