はじめに
本投稿の背景と目的
Reactの公式チュートリアルは大変よくできているので、初学者がReactの基本を学ぶためには、まずは公式チュートリアルに沿って手を動かしていくのが一番だと思います。
私自身、公式チュートリアルをやってみて基本的な部分の理解はできたものの、以下の点が自分にとっては課題に感じました。
- TypeScriptで書きたい場合にどうすればよいか分からず、いろいろ調べる必要があった
- 三目並べという題材が若干トリッキーに感じた
というわけで、TypeScriptを使ってReactに入門するための、よりシンプルな題材のチュートリアルを作成することが本投稿の目的です。
私自身が初学者なので、初学者目線での説明を心がけていますが、わかりにくい箇所や間違っている箇所などありましたら忌憚のないコメントを頂けると有り難いです。
(2020-09-22追記)
TypeScriptでReact Hooksに入門するチュートリアルも投稿しましたので、本チュートリアルを終えた後はそちらもご参照頂けると幸いです。
(2020-10-11追記)
「次に学ぶこと」の章を追加しました。
前提とする知識
- HTMLの基本知識
- TypeScriptの基本知識
開発環境
チュートリアル本編は、CodePenを使ってブラウザ上でステップ・バイ・ステップでアプリケーションを開発していく流れとなっています。
ローカル開発環境で動かしたい場合は、(Appendix) ローカル開発環境のセットアップ
を参考にしてください。
動作確認済みの環境
Mac OS X 10.15.2
node v11.10.1
npm 6.12.0
react 16.12.0
typescript 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で公開してあります。