空雲リファレンス

Next.js(12系)によるReact+TypeScript入門 2

TSX(JSX)の基本

TSX(JSX)とは

JSXとはReact.jsで採用されているJavaScriptの構文中にHTMLタグに類似した形でコンポーネントの状態を記述する形式です。様々な要素がHTMLのように記述できるので、プログラミングが容易になります。実際に動作させるにはJavaScriptへのコンパイルが必要です。

JSXを拡張してTypeScriptに対応させたものがTSXとなります。以降、基本的な解説はTSXを前提に行います。

TSXの記述例

const Page = () => ( <> <div>あいうえお</div> <div> {new Array(10).fill("").map((_, index) => ( <span key={index}>あ</span> ))} </div> </> ); export default Page;

詳しい内容は後述しますが、HTMLタグのように簡単に書ける反面、ループ処理のように見た目が分かりにくくなる部分もあります。

TSXによるコンポーネントの作成

何もしない関数コンポーネント

以下、何もしない関数コンポーネントです。なお、React.jsにはクラスコンポーネントという書き方もありますが、現在は使用されなくなってきているため、本書では解説しません。

function関数とアロー関数式の書き方を載せていますが、現在はアロー関数式を使うのが一般的です。また、functionとアロー関数式は仕組みが一部異なってますが、その原因となっているthisを関数コンポーネントで用いることはないので、違いを意識するケースはほぼありません。

また、一般のタグと作成したコンポーネントを区別するため、頭文字は大文字必須です。

const Component01 = () => <></> const Component02 = () => null const Component03 = () => { return <></> } const Component04 = () => { return null } const Component05 = function () { return <></> } const Component06 = function () { return null }

関数コンポーネントは仮想DOMを構築するためのReactNodeを返します。その際、何もする必要が無ければ空のNodeを<></>もしくはnullで返します。

HTMLタグを返す

const Component01 = () => <div> React.jsプログラミング </div> const Component02 = () => <> <div> React.jsプログラミング1 </div> <div> React.jsプログラミング2 </div> </>

TSXによるReactNodeの記述はHTMLの記述ルールに近いものになっています。トップレベルに配置でいるノードは一つだけで、複数並べる必要がある場合は、何らかのノードの下に置くか、<></>を使ってグループ化します。

タグとパラメータ

const Component01 = () => <div className="a" style={{ width: "100px", height: "200px" }}> React.jsプログラミング </div> const style = { width: "100px", height: "200px" } const Component02 = () => <div className={"a"} style={style}> React.jsプログラミング </div>

TSXでタグのパラメータを記述する際、文字列に関してはそのまま書くことができます。それ以外の肩を持つデータは{}で囲む必要があります。またHTMLならクラス名をclassとするところですが、TSXではJavaScriptの予約語と被らないようにclassNameになっています。その他、styleの記述ではオブジェクトを渡す形式になっているなど、パラメータの種類によって書き方が一般的なHTMLとは異なっていることに注意が必要です。

関数コンポーネントとTypeScript

関数コンポーネントとchildren

  • src/pages/test01.tsx
import { FC, ReactNode } from "react"; interface Props { children?: ReactNode; } const Component01 = (props: Props) => <div>入力された内容は「{props.children}」</div>; const Component02 = ({ children }: Props) => <div>入力された内容は「{children}」</div>; const Component03: FC = (props) => <div>入力された内容は「{props.children}」</div>; const Component04: FC = ({ children }) => <div>入力された内容は「{children}」</div>; const Page = () => ( <> <Component01>React.jsプログラミング1</Component01> <Component02>React.jsプログラミング2</Component02> <Component03>React.jsプログラミング3</Component03> <Component04>React.jsプログラミング4</Component04> </> ); export default Page;

4つのコンポーネントは記述に違いがあるものの同じ内容です。Reactでは子ノードとして送られたデータはchildrenというプロパティになります。

FC型として作成すると、childrenが定義済みとなります。Reactv18ではFCからchildrenの定義が削除されるという話もあるので、使用するかどうかは各々で判断してください。

マークアップ中に変数やJavaScriptの機能を利用する場合は{}で囲みます。今回は{children}で送られてきた子ノードを表示しています。

一般的なパラメータ

  • src/pages/test02.tsx
import { FC, ReactNode, ReactElement } from "react"; interface Props { num: number; children?: ReactNode; } const Component01: FC<Props> = ({ num, children }) => ( <> <div>入力された数値は「{num}」</div> <div>内容は「{children}」</div> </> ); const Page = () => ( <> <Component01 num={1}>React.jsプログラミング</Component01> </> ); export default Page;

childrenの他に数値型のパラメータを渡しています。

childrenを使わずにReactNodeを渡す

  • src/pages/test03.tsx
import { FC, ReactNode, ReactElement } from "react"; interface Props { num: number; node: ReactNode; } const Component01: FC<Props> = ({ num, node }) => ( <> <div>入力された数値は「{num}」</div> <div>内容は「{node}」</div> </> ); const Page = () => <Component01 num={1} node={<h1>React.jsプログラミング</h1>} />; export default Page;

コンポーネントの中にコンポーネントを記述する際、childrenの代わりにパラメータでReactNodeを渡すことも出来ます。

マークアップ内の条件分岐

  • src/pages/test04.tsx
import { VFC } from "react"; interface Props { num: number; } const Component01: VFC<Props> = ({ num }) => ( <div> {num === 0 && "あいうえお"} {num === 1 && "かきくけこ"} {num === 2 && "さしすせそ"} {num === 3 && "たちつてと"} </div> ); const Page = () => ( <> <Component01 num={1} /> <Component01 num={3} /> </> ); export default Page;

マークアップ内で条件を指定することも出来ます。ただし気をつけなければならないのは{num && "なにぬねの"}です。numが0だった場合、0が表示されます。JavaScriptでは&&で真にならなかった場合、最初の値が返るという仕様があるのですがnull,undefined,falseなら何も表示されませんが、0は数値なので出力されることになります。

マークアップ内のループ

  • src/pages/test05.tsx
import { VFC } from "react"; interface Props { num: number; } const Component01: VFC<Props> = ({ num }) => ( <div> {Array(num).fill("").map((_, index) => ( <div key={index}>{index}:あいうえお</div> ))} </div> ); const Page = () => ( <> <Component01 num={5} /> </> ); export default Page;

マークアップ内ではfor分に相当する機能がありません。そのためループのような処理を行うためには配列が必要になります。ここではArrayで配列を作成しています。fillで塗りつぶしているのは、領域だけ作成した状態だと配列サイズが0だと認識されてループ処理が出来ないためです。

また<div key={index}>のように、ループ内のトップノードにはkeyが必要です。これは仮想DOMから実際のDOMに変換する際に、どのノードと結びついているかを判断するためです。また、今回はkeyに相当するものがindexしかないのでそのまま利用していますが、本来ならユニークなIDが設定されていることが前提となります。これを怠ると、送ったデータと表示されている内容が相違することがあります。

マークアップと変数

マークアップで利用される変数

Reactでマークアップを行う際に利用できるデータの種類は以下のような内容になります

  • src/pages/test08.tsx
import { ElementType } from "react"; const Page = () => { const Test01 = "<h1>テスト01</h1>"; const Test02 = 100; const Test03 = true; const Test04 = false; const Test05 = null; const Test06 = undefined; const Test07 = ["T", "E", "S", "T", "0", "7"]; const Test08 = () => <>テスト08</>; const Test09:ElementType = "h1"; const Test10 = "<h2>Test10</h2>"; return ( <> <div>Test01:{Test01}</div> <div>Test02:{Test02}</div> <div>Test03:{Test03}</div> <div>Test04:{Test04}</div> <div>Test05:{Test05}</div> <div>Test06:{Test06}</div> <div>Test07:{Test07}</div> <div> <Test08 /> </div> <Test09>テスト09</Test09> <div dangerouslySetInnerHTML={ { __html: Test10 } } /> </> ); }; export default Page;

  • Test01
    文字列はHTMLパースされた上で表示
  • Test02
    数値は文字列に変換された上で表示
  • Test03 ~ Test06
    表示はスキップされる
  • Test07
    配列は順序通り表示されますが、本来必要なkeyが設定されていないので警告が出ます
  • Test08
    関数コンポーネントなのでタグとして利用
  • Test09
    文字列をエレメントタイプ(型は省略可能)として利用
  • Test10
    生タグとして表示(リスクが伴うので推奨はされない)

イベントの処理

イベントの使い方

  • src/pages/test06.tsx
const Page = () => ( <form onSubmit={(e) => { e.preventDefault(); //ページ切り替えのキャンセル const form = e.target as HTMLFormElement; console.log(`フォーム実行内容:${form["a"].value}/${form["b"].checked}`); }} > <input name="a" onChange={(e) => console.log(`テキスト:${e.target.value}`)} /> <input name="b" type="checkbox" onChange={(e) => console.log(`チェック:${e.target.checked}`)} /> <button>実行</button> </form> ); export default Page;

イベントのデータをconsole.logで表示しています。一般的なJavaScriptのイベント処理と同等ですが、thisを使ったノードの取得が出来ないので気をつけてください。

イベント処理をマークアップの外に出す

import { ChangeEventHandler, FormEventHandler } from "react"; const Page = () => { const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => { e.preventDefault(); //ページ切り替えのキャンセル const form = e.currentTarget; console.log(`フォーム実行内容:${form["a"].value}/${form["b"].checked}`); }; const handleTextChange: ChangeEventHandler<HTMLInputElement> = (e) => console.log(`テキスト:${e.target.value}`); const handleCheckChange: ChangeEventHandler<HTMLInputElement> = (e) => console.log(`チェック:${e.currentTarget.checked}`); return ( <form onSubmit={handleSubmit}> <input name="a" onChange={handleTextChange} /> <input name="b" type="checkbox" onChange={handleCheckChange} /> <button>実行</button> </form> ); }; export default Page;

イベント処理をマークアップに入る前に記述する書き方です。TypeScriptを使う上で問題になるのが、事前に書く場合は、関数の型を自分で設定する必要があります。必要な型を丸暗記する必要はありません。マウスを載せれば教えてくれます。