空雲 Blog

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

publication: 2021/11/23
update:2023/06/22

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;

{"width":"642px","height":"329px"}

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

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

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

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

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

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

1const Component01 = () => <></>
2const Component02 = () => null
3const 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プログラミング1
9 </div>
10 <div>
11 React.jsプログラミング2
12 </div>
13 </>

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

タグとパラメータ

1const Component01 = () =>
2 <div className="a" style={{ width: "100px", height: "200px" }}>
3 React.jsプログラミング
4 </div>
5
6const 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";
2
3interface Props {
4 children?: ReactNode;
5}
6const Component01 = (props: Props) => <div>入力された内容は「{props.children}</div>;
7
8const Component02 = ({ children }: Props) => <div>入力された内容は「{children}</div>;
9
10const Component03: FC = (props) => <div>入力された内容は「{props.children}</div>;
11
12const Component04: FC = ({ children }) => <div>入力された内容は「{children}</div>;
13
14const 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);
22
23export default Page;
24

{"width":"583px","height":"260px"}

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

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

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

一般的なパラメータ

  • src/pages/test02.tsx

1import { FC, ReactNode, ReactElement } from "react";
2
3interface 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);
13
14const Page = () => (
15 <>
16 <Component01 num={1}>React.jsプログラミング</Component01>
17 </>
18);
19
20export default Page;

{"width":"582px","height":"260px"}

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

childrenを使わずにReactNodeを渡す

  • src/pages/test03.tsx

1import { FC, ReactNode, ReactElement } from "react";
2
3interface 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);
13
14const Page = () => <Component01 num={1} node={<h1>React.jsプログラミング</h1>} />;
15
16export default Page;

{"width":"682px","height":"331px"}

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

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

  • src/pages/test04.tsx

1import { VFC } from "react";
2
3interface 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);
14
15const Page = () => (
16 <>
17 <Component01 num={1} />
18 <Component01 num={3} />
19 </>
20);
21
22export default Page;

{"width":"682px","height":"221px"}

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

マークアップ内のループ

  • src/pages/test05.tsx

1import { VFC } from "react";
2
3interface 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);
13
14const Page = () => (
15 <>
16 <Component01 num={5} />
17 </>
18);
19
20export default Page;

{"width":"682px","height":"299px"}

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

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

マークアップと変数

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

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

  • src/pages/test08.tsx

1import { ElementType } from "react";
2
3const 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};
31
32export default Page;

{"width":"565px","height":"527px"}

  • Test01
    文字列はHTMLパースされた上で表示

  • Test02
    数値は文字列に変換された上で表示

  • Test03 ~ Test06
    表示はスキップされる

  • Test07
    配列は順序通り表示されますが、本来必要なkeyが設定されていないので警告が出ます

  • Test08
    関数コンポーネントなのでタグとして利用

  • Test09
    文字列をエレメントタイプ(型は省略可能)として利用

  • Test10
    生タグとして表示(リスクが伴うので推奨はされない)

イベントの処理

イベントの使い方

  • src/pages/test06.tsx

1const Page = () => (
2 <form
3 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);
14
15export default Page;

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

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

1import { ChangeEventHandler, FormEventHandler } from "react";
2
3const 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};
21
22export default Page;

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

{"width":"1222px","height":"565px"}