Next.js(12系)によるReact+TypeScript入門 2
TSX(JSX)の基本
TSX(JSX)とは
JSXとはReact.jsで採用されているJavaScriptの構文中にHTMLタグに類似した形でコンポーネントの状態を記述する形式です。様々な要素がHTMLのように記述できるので、プログラミングが容易になります。実際に動作させるにはJavaScriptへのコンパイルが必要です。
JSXを拡張してTypeScriptに対応させたものがTSXとなります。以降、基本的な解説はTSXを前提に行います。
TSXの記述例
1const Page = () => (2 <>3 <div>あいうえお</div>4 <div>5 {new Array(10).fill("").map((_, index) => (6 <span key={index}>あ</span>7 ))}8 </div>9 </>10);11export default Page;
詳しい内容は後述しますが、HTMLタグのように簡単に書ける反面、ループ処理のように見た目が分かりにくくなる部分もあります。
TSXによるコンポーネントの作成
何もしない関数コンポーネント
以下、何もしない関数コンポーネントです。なお、React.jsにはクラスコンポーネントという書き方もありますが、現在は使用されなくなってきているため、本書では解説しません。
function関数とアロー関数式の書き方を載せていますが、現在はアロー関数式を使うのが一般的です。また、functionとアロー関数式は仕組みが一部異なってますが、その原因となっているthisを関数コンポーネントで用いることはないので、違いを意識するケースはほぼありません。
また、一般のタグと作成したコンポーネントを区別するため、頭文字は大文字必須です。
1const Component01 = () => <></>2const Component02 = () => null3const Component03 = () => { return <></> }4const Component04 = () => { return null }5const Component05 = function () { return <></> }6const Component06 = function () { return null }
関数コンポーネントは仮想DOMを構築するためのReactNodeを返します。その際、何もする必要が無ければ空のNodeを<></>もしくはnullで返します。
HTMLタグを返す
1const Component01 = () =>2 <div>3 React.jsプログラミング4 </div>5const Component02 = () =>6 <>7 <div>8 React.jsプログラミング19 </div>10 <div>11 React.jsプログラミング212 </div>13 </>
TSXによるReactNodeの記述はHTMLの記述ルールに近いものになっています。トップレベルに配置でいるノードは一つだけで、複数並べる必要がある場合は、何らかのノードの下に置くか、<></>を使ってグループ化します。
タグとパラメータ
1const Component01 = () =>2 <div className="a" style={{ width: "100px", height: "200px" }}>3 React.jsプログラミング4 </div>56const style = { width: "100px", height: "200px" }7const Component02 = () =>8 <div className={"a"} style={style}>9 React.jsプログラミング10 </div>
TSXでタグのパラメータを記述する際、文字列に関してはそのまま書くことができます。それ以外の肩を持つデータは{}で囲む必要があります。またHTMLならクラス名をclassとするところですが、TSXではJavaScriptの予約語と被らないようにclassNameになっています。その他、styleの記述ではオブジェクトを渡す形式になっているなど、パラメータの種類によって書き方が一般的なHTMLとは異なっていることに注意が必要です。
関数コンポーネントとTypeScript
関数コンポーネントとchildren
src/pages/test01.tsx
1import { FC, ReactNode } from "react";23interface Props {4 children?: ReactNode;5}6const Component01 = (props: Props) => <div>入力された内容は「{props.children}」</div>;78const Component02 = ({ children }: Props) => <div>入力された内容は「{children}」</div>;910const Component03: FC = (props) => <div>入力された内容は「{props.children}」</div>;1112const Component04: FC = ({ children }) => <div>入力された内容は「{children}」</div>;1314const Page = () => (15 <>16 <Component01>React.jsプログラミング1</Component01>17 <Component02>React.jsプログラミング2</Component02>18 <Component03>React.jsプログラミング3</Component03>19 <Component04>React.jsプログラミング4</Component04>20 </>21);2223export default Page;24
4つのコンポーネントは記述に違いがあるものの同じ内容です。Reactでは子ノードとして送られたデータはchildrenというプロパティになります。
FC型として作成すると、childrenが定義済みとなります。Reactv18ではFCからchildrenの定義が削除されるという話もあるので、使用するかどうかは各々で判断してください。
マークアップ中に変数やJavaScriptの機能を利用する場合は{}で囲みます。今回は{children}で送られてきた子ノードを表示しています。
一般的なパラメータ
src/pages/test02.tsx
1import { FC, ReactNode, ReactElement } from "react";23interface Props {4 num: number;5 children?: ReactNode;6}7const Component01: FC<Props> = ({ num, children }) => (8 <>9 <div>入力された数値は「{num}」</div>10 <div>内容は「{children}」</div>11 </>12);1314const Page = () => (15 <>16 <Component01 num={1}>React.jsプログラミング</Component01>17 </>18);1920export default Page;
childrenの他に数値型のパラメータを渡しています。
childrenを使わずにReactNodeを渡す
src/pages/test03.tsx
1import { FC, ReactNode, ReactElement } from "react";23interface Props {4 num: number;5 node: ReactNode;6}7const Component01: FC<Props> = ({ num, node }) => (8 <>9 <div>入力された数値は「{num}」</div>10 <div>内容は「{node}」</div>11 </>12);1314const Page = () => <Component01 num={1} node={<h1>React.jsプログラミング</h1>} />;1516export default Page;
コンポーネントの中にコンポーネントを記述する際、childrenの代わりにパラメータでReactNodeを渡すことも出来ます。
マークアップ内の条件分岐
src/pages/test04.tsx
1import { VFC } from "react";23interface Props {4 num: number;5}6const Component01: VFC<Props> = ({ num }) => (7 <div>8 {num === 0 && "あいうえお"}9 {num === 1 && "かきくけこ"}10 {num === 2 && "さしすせそ"}11 {num === 3 && "たちつてと"}12 </div>13);1415const Page = () => (16 <>17 <Component01 num={1} />18 <Component01 num={3} />19 </>20);2122export default Page;
マークアップ内で条件を指定することも出来ます。ただし気をつけなければならないのは{num && "なにぬねの"}です。numが0だった場合、0が表示されます。JavaScriptでは&&で真にならなかった場合、最初の値が返るという仕様があるのですがnull,undefined,falseなら何も表示されませんが、0は数値なので出力されることになります。
マークアップ内のループ
src/pages/test05.tsx
1import { VFC } from "react";23interface Props {4 num: number;5}6const Component01: VFC<Props> = ({ num }) => (7 <div>8 {Array(num).fill("").map((_, index) => (9 <div key={index}>{index}:あいうえお</div>10 ))}11 </div>12);1314const Page = () => (15 <>16 <Component01 num={5} />17 </>18);1920export default Page;
マークアップ内ではfor分に相当する機能がありません。そのためループのような処理を行うためには配列が必要になります。ここではArrayで配列を作成しています。fillで塗りつぶしているのは、領域だけ作成した状態だと配列サイズが0だと認識されてループ処理が出来ないためです。
また<div key={index}>のように、ループ内のトップノードにはkeyが必要です。これは仮想DOMから実際のDOMに変換する際に、どのノードと結びついているかを判断するためです。また、今回はkeyに相当するものがindexしかないのでそのまま利用していますが、本来ならユニークなIDが設定されていることが前提となります。これを怠ると、送ったデータと表示されている内容が相違することがあります。
マークアップと変数
マークアップで利用される変数
Reactでマークアップを行う際に利用できるデータの種類は以下のような内容になります
src/pages/test08.tsx
1import { ElementType } from "react";23const Page = () => {4 const Test01 = "<h1>テスト01</h1>";5 const Test02 = 100;6 const Test03 = true;7 const Test04 = false;8 const Test05 = null;9 const Test06 = undefined;10 const Test07 = ["T", "E", "S", "T", "0", "7"];11 const Test08 = () => <>テスト08</>;12 const Test09:ElementType = "h1";13 const Test10 = "<h2>Test10</h2>";14 return (15 <>16 <div>Test01:{Test01}</div>17 <div>Test02:{Test02}</div>18 <div>Test03:{Test03}</div>19 <div>Test04:{Test04}</div>20 <div>Test05:{Test05}</div>21 <div>Test06:{Test06}</div>22 <div>Test07:{Test07}</div>23 <div>24 <Test08 />25 </div>26 <Test09>テスト09</Test09>27 <div dangerouslySetInnerHTML={ { __html: Test10 } } />28 </>29 );30};3132export default Page;
Test01
文字列はHTMLパースされた上で表示Test02
数値は文字列に変換された上で表示Test03 ~ Test06
表示はスキップされるTest07
配列は順序通り表示されますが、本来必要なkeyが設定されていないので警告が出ますTest08
関数コンポーネントなのでタグとして利用Test09
文字列をエレメントタイプ(型は省略可能)として利用Test10
生タグとして表示(リスクが伴うので推奨はされない)
イベントの処理
イベントの使い方
src/pages/test06.tsx
1const Page = () => (2 <form3 onSubmit={(e) => {4 e.preventDefault(); //ページ切り替えのキャンセル5 const form = e.target as HTMLFormElement;6 console.log(`フォーム実行内容:${form["a"].value}/${form["b"].checked}`);7 }}8 >9 <input name="a" onChange={(e) => console.log(`テキスト:${e.target.value}`)} />10 <input name="b" type="checkbox" onChange={(e) => console.log(`チェック:${e.target.checked}`)} />11 <button>実行</button>12 </form>13);1415export default Page;
イベントのデータをconsole.logで表示しています。一般的なJavaScriptのイベント処理と同等ですが、thisを使ったノードの取得が出来ないので気をつけてください。
イベント処理をマークアップの外に出す
1import { ChangeEventHandler, FormEventHandler } from "react";23const Page = () => {4 const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {5 e.preventDefault(); //ページ切り替えのキャンセル6 const form = e.currentTarget;7 console.log(`フォーム実行内容:${form["a"].value}/${form["b"].checked}`);8 };9 const handleTextChange: ChangeEventHandler<HTMLInputElement> = (e) =>10 console.log(`テキスト:${e.target.value}`);11 const handleCheckChange: ChangeEventHandler<HTMLInputElement> = (e) =>12 console.log(`チェック:${e.currentTarget.checked}`);13 return (14 <form onSubmit={handleSubmit}>15 <input name="a" onChange={handleTextChange} />16 <input name="b" type="checkbox" onChange={handleCheckChange} />17 <button>実行</button>18 </form>19 );20};2122export default Page;
イベント処理をマークアップに入る前に記述する書き方です。TypeScriptを使う上で問題になるのが、事前に書く場合は、関数の型を自分で設定する必要があります。必要な型を丸暗記する必要はありません。マウスを載せれば教えてくれます。