LoginSignup
9
11

React + TypeScript: ユーザーインタフェースを組み立てる

Last updated at Posted at 2023-03-20

React公式サイトのドキュメントが2023年3月16日に改訂されました(「Introducing react.dev」参照)。本稿は、入門解説の第1章「Describing the UI」をかいつまんでまとめた記事です。ただし、TypeScriptをコードに加えました。反面、初心者向けのJavaScriptの基礎的な説明は省いています。

なお、本シリーズ解説の他の記事については「React + TypeScript: React公式ドキュメントの基本解説『Learn React』を学ぶ」をご参照ください。

この記事の主題は、Reactのコンポーネントと戻り値JSXの書き方です。Reactアプリケーションはコンポーネントを組み合わせて、JSXでページを描画します。コード例はCodeSandboxに公開していますので、ご参照ください。

コンポーネントをつくる(Your First Component)

Reactコンポーネントのつくり方は、つぎのとおりです。

  1. コンポーネントはexportしなければなりません。
  2. 関数コンポーネントを定めます。
    • exportdefaultでも名前つきでも構いません。
  3. 戻り値はJSXで加えます。

Reactの(関数)コンポーネントは、標準JavaScriptの関数として定めます。ただし、名前はアッパーキャメルケース(パスカルケース)でなければなりません(「Step 2: Define the function」参照)。

つぎのコードがコンポーネントの例です(JavaScriptモジュール)。TypeScriptを使った場合、戻り値は推論されます。明示したいときは、JSX.Elementで型づけしてください。

App.js
export default function Profile() {
	return (
		<img
			src='https://i.imgur.com/MK3eW3Am.jpg'
			alt='Katherine Johnson'
		/>
	)
}

Reactは小さなUI部品からページ全体にいたるまで、コードとマークアップが備わったコンポーネントで組み立てます。そして、ページはReactがJavaScriptで描くのです。Next.jsのようなフレームワークはさらに一歩進んで、ReactコンポーネントからHTMLページを自動生成します。つまり、JavaScriptコードが読み込まれる前に、アプリケーションのコンテンツの一部を表示できるのです(「Components all the way down」参照)。

この記事で示されたコード例を、React + TypeScriptに改め、少し手直ししてつぎのサンプル001に掲げました。

サンプル001■React + TypeScript: Describing the UI 01

コンポーネントのimportおよびexport(Importing and Exporting Components)

Reactはアプリケーションをコンポーネントで組み立てます。利点は、組み合わせて使い回しでき、管理やメンテナンスがしやすいことです。前傾サンプル001のアプリケーションで、ルートモジュール(src/App.tsx)をもっと簡素化しましょう。JSXの実質のマークアップを、つぎのように別モジュールsrc/Gallery.tsxに切り出します(サンプル002)。

src/Gallery.tsx
import { FC } from 'react';
import { Profile } from './Profile';

export const Gallery: FC = () => {
	return (
		<section>
			<h1>Amazing scientists</h1>
			<Profile />
		</section>
	);
};
src/App.tsx
import React from 'react'
import { Gallery } from './Gallery';
import './styles.css';

export default function App(): JSX.Element {
	return <Gallery />;
}

サンプル002■React + TypeScript: Describing the UI 02

Reactコンポーネントのexportは、defaultでも名前つきでも構いません。サンプルでは、ルートコンポーネント(App)はexport default、子コンポーネント(GalleryProfile)は名前つきにしました。importの構文が少し変わりますのでご注意ください。チームでどちらか一方に統一するのもひとつのアイデアです(「Default vs named exports」参照)。なお、defaultexportはひとつのモジュールからひとつしかできません。

JSXでマークアップする(Writing Markup with JSX)

JSXはJavaScriptの構文拡張です。JavaScriptコードの中に、HTMLのようなマークアップが記述できます。

Webがインタラクティブになるにつれ、コンテンツはロジックによって決まるようになってきました。そこで、ReactはHTMLをJavaScriptに委ね、レンダリングロジックとマークアップがともにコンポーネントに含められたのです。

JSXの構文はほぼHTMLにしたがっています。ただし、規則はもう少し厳格です。

  1. 戻り値はひとつのルート要素にまとまっている必要があります。
  2. タグはすべて閉じなければなりません。
  3. 要素に加えるプロパティ(属性)はほとんどの場合キャメルケースです(「Common components (e.g. <div>)」参照)。
    • 属性のハイフン(-)は除いて、キャメルケースで記述します。
    • 予約語のたとえばclassは使えないので、classNameです。
    • 歴史的な事情から、aria-*data-*には、HTMLと同じくハイフンを用います(aria-*およびdata-*参照)。

JSXは見たところHTMLです。けれど、内部的にJavaScriptの標準オブジェクトに変換されます。ふたつのオブジェクトは、配列に含めないかぎり関数から返せません。そのため、コンポーネントの戻り値のJSXはひとつにまとめなければならないのです(「Why do multiple JSX tags need to be wrapped?」参照)。

すでに書かれたHTMLのマークアップをJSXに変換したいときは、transformツール(「Pro-tip: Use a JSX Converter」)も使えます。

JSX内に波かっこ({})でJavaScriptを記述する(JavaScript in JSX with Curly Braces)

JSXの中にJavaScriptの式を書くとき用いる構文が波かっこ{}です。前掲サンプル002のコンポーネントProfileをつぎのように書き替えると、jSX内でJavaScriptの変数(photodescription)が参照できます(サンプル003)。

src/Profile.tsx
import { FC } from 'react';

export const Profile: FC = () => {
	const photo = 'https://i.imgur.com/MK3eW3As.jpg';
	const description = 'Katherine Johnson';
	return <img src={photo} alt={description} />;
};

サンプル003■React + TypeScript: Describing the UI 03

JSXに中かっこ{}構文で記述できるのは、JavaScriptの変数にかぎりません。関数の呼び出しも含めたすべてのJavaScriptの式が参照できます。なお、モジュールをいつ読み込みんでも変わることがない変数(以下のcopyrightOwner)は、コンポーネントの外に定めて結構です。

src/App.tsx
import { Gallery } from './Gallery';
import { Footer } from './Footer';
import './styles.css';

export default function App(): JSX.Element {
	return (
		<>
			<Gallery />
			<Footer />
		</>
	);
}
src/Footer.tsx
import { FC } from 'react';

const copyrightOwner = 'Fumio Nonaka';
export const Footer: FC = () => {
	const getYear = () => new Date().getFullYear();
	return (
		<footer>
			Copyright &#169;2000-{getYear()} {copyrightOwner}
		</footer>
	);
};

JSXにJavaScriptコードのオブジェクトを与える場合には、リテラルの波かっこ{}も加わって二重になります。なお、CSSのプロパティはキャメルケース(backgroundColorfontFamilyおよびlineHeight)になることにお気をつけください。

src/Footer.tsx
import { FC } from 'react';

const copyrightOwner = 'Fumio Nonaka';
export const Footer: FC = () => {
	const getYear = () => new Date().getFullYear();
	return (
		<footer
			style={{
				backgroundColor: 'paleturquoise',
				fontFamily: 'Helvetica Neue',
				lineHeight: '2rem'
			}}
		>
			Copyright &#169;2000-{getYear()} {copyrightOwner}
		</footer>
	);
};

さらに、複数の変数(copyrightOwner)やオブジェクト(styles)をひとまとまりで扱うために、ひとつのオブジェクト(footerInfo)に収めることもあるでしょう(サンプル004)。

src/Footer.tsx
import { FC } from 'react';

const footerInfo = {
	copyrightOwner: 'Fumio Nonaka',
	styles: {
		backgroundColor: 'paleturquoise',
		fontFamily: 'Helvetica Neue',
		lineHeight: '2rem'
	}
};
export const Footer: FC = () => {
	const getYear = () => new Date().getFullYear();
	const { copyrightOwner, styles } = footerInfo;
	return (
		<footer style={styles}>
			Copyright &#169;2000-{getYear()} {copyrightOwner}
		</footer>
	);
};

サンプル004■React + TypeScript: Describing the UI 04

コンポーネントにプロパティを渡す(Passing Props to a Component)

Reactのコンポーネントは、propsと呼ばれるプロパティで子にデータを渡します。渡し方はHTMLタグの属性と同じ構文です。ただし、データはテキストだけでなく、オブジェクトや配列、関数などJavaScriptの値すべてが含まれます。さらに、子コンポーネントに含めるノード(テキスト)も渡しましょう。

src/App.tsx
import { Gallery } from './Gallery';
import { Footer } from './Footer';
import './styles.css';

export type FooterInfo = {
	copyrightOwner: string;
	styles: {
		backgroundColor: string;
		fontFamily: string;
		lineHeight: string;
	};
};
const footerInfo: FooterInfo = {
	copyrightOwner: 'Fumio Nonaka',
	styles: {
		backgroundColor: 'paleturquoise',
		fontFamily: 'Helvetica Neue',
		lineHeight: '2rem'
	}
};
export default function App(): JSX.Element {
	const getYear = () => new Date().getFullYear();
	return (
		<>
			<Gallery />
			<Footer footerInfo={footerInfo}>
				Copyright &#169;2000-{getYear()} {footerInfo.copyrightOwner}
			</Footer>
		</>
	);
}

子コンポーネントが引数に受け取るのは、親から渡されたすべてのプロパティを収めたオブジェクト(props)です。つぎのコード例のFooterコンポーネントは、オブジェクトの分割代入で必要なプロパティ(copyrightOwnerstyles)を取り出しています。また、受け取る子ノードのプロパティはchildren(型React.ReactNode)です(「childrenとしてJSXを渡す」参照)。コード全体は以下のサンプル005に掲げました。

src/Footer.tsx
import React, { FC } from 'react';
import type { FooterInfo } from './App';

type Props = {
	children: React.ReactNode;
	footerInfo: FooterInfo;
};
export const Footer: FC<Props> = ({
	children,
	footerInfo: { copyrightOwner, styles }
}) => {
	return (
		<footer style={styles}>
			{children}
		</footer>
	);
};

サンプル005■React + TypeScript: Describing the UI 05

ひとつのオブジェクトに収められたプロパティすべてを分けて子コンポーネントに渡したいときは、スプレッド構文が使えます。前掲サンプルのプロパティ(footerInfo)をスプレッド構文で与えるように書き替えたのがつぎに抜き出したコードです。

src/App.tsx
export default function App(): JSX.Element {

	return (
		<>

			{/* <Footer footerInfo={footerInfo}> */}
			<Footer {...footerInfo}>
				Copyright &#169;2000-{getYear()} {footerInfo.copyrightOwner}
			</Footer>
		</>
	);
}
src/Footer.tsx
type Props = {

	// footerInfo: FooterInfo;
	copyrightOwner: FooterInfo['copyrightOwner'];
	styles: FooterInfo['styles'];
};
export const Footer: FC<Props> = ({

	// footerInfo: { copyrightOwner, styles }
	copyrightOwner,
	styles
}) => {
	return <footer style={styles}>{children}</footer>;
};

コンポーネントが受け取るプロパティ(props)の値は、それぞれ必要に応じて変えられます。ただし、propsそのものは不変(immutable)です。コンポーネントに与えるプロパティの値を改めたいとき(たとえば、ユーザーインタラクションやデータの変更時)には、親コンポーネントに頼まなければなりません。すると、新たな値のオブジェクトが別につくられて渡されます。古いプロパティは破棄され、JavaScriptにより新たなオブジェクトにメモリが割り当てられるのです(「How props change over time」参照)。

条件つきレンダリング(Conditional Rendering)

条件によって、表示する中身を変えたいことがあるでしょう。Reactでは、レンダーするJSXがJavaScriptの条件の構文を用いて変えられます。まずは、if文です。つぎのコード例は、子コンポーネント(Item)に渡したプロパティ(isPacked)のブール(論理)値によって、返されるJSXの値が異なります。

src/App.tsx
import { Item } from './Item';
import './styles.css';

export default function PackingList() {
	return (
		<section>
			<h1>Sally Ride's Packing List</h1>
			<ul>
				<Item isPacked={true} name="Space suit" />
				<Item isPacked={true} name="Helmet with a golden leaf" />
				<Item isPacked={false} name="Photo of Tam" />
			</ul>
		</section>
	);
}
src/Item.tsx
import { FC } from 'react';

type ItemDescriptions = { isPacked: boolean; name: string };
export const Item: FC<ItemDescriptions> = ({ isPacked, name }) => {
	if (isPacked) {
		return <li className="item">{name}</li>;
	}
	return <li className="item">{name}</li>;
};

もっとも、条件分岐して返されるJSXはほとんど同じです。共通するコードはひとつにした方が、修正・変更するとき無駄な手間が省けるでしょう。この場合に使えるのは、条件(三項)演算子?:です(サンプル006)。

src/Item.tsx
import { FC } from 'react';

type ItemDescriptions = { isPacked: boolean; name: string };
export const Item: FC<ItemDescriptions> = ({ isPacked, name }) => {
	return <li className="item">{isPacked ? name + '' : name}</li>;
};

サンプル006■React + TypeScript: Describing the UI 06

論理積(&&)演算子を用いると、つぎのようにコードはさらに短く書けます。左辺の式の値がブール値のとき、trueであれば返されるのは右辺の式です。falseの場合は、左辺値falseが戻り値となります。そして、ReactはJSXツリーの中のfalse(nullundefinedも同様)はレンダリングしません(「Logical AND operator (&&)」参照)。

src/Item.tsx
export const Item: FC<ItemDescriptions> = ({ isPacked, name }) => {
	// return <li className="item">{isPacked ? name + ' ✔' : name}</li>;
	return <li className="item">{name} {isPacked && ''}</li>;
};

ご注意いただきたいのは、論理積演算子&&の条件としての評価と戻り値が別であることです。左辺の式は条件としてはブール値評価されます。けれど、戻り値は左辺または右辺の式の値です。左辺に数値の式を与えた場合、0以外の評価はtrueとなり、右辺の式の値が返ります。問題は値が0のときです。条件としての評価はfalseでも、返される値が0になってしまいます。これは、ほとんどの場合意図しない結果でしょう。それを避けるには、左辺を条件式にするか、Boolean()関数または二重の論理否定演算子!でブール値に変換してください。

リストのレンダリング(Rendering Lists)

形式の決まった複数のデータから、コンポーネントをリスト表示したいことがあるでしょう。そのようなときに、処理するデータを収めるのは配列です。Array.prototype.filter()Array.prototype.map()などのメソッドにより、データからコンポーネントのリスト(配列)をつくります。

Reactはコンポーネントの戻り値にJSXノードの配列が差し込まれると、要素のJSXを順に並べてレンダリングするのです。このとき各ノードのルート要素には、一意のkeyプロパティを与えなければなりません。Reactはもとの配列のどのデータが、JSXのどの要素に対応するのか、このkeyによって識別するからです(「Keeping list items in order with key」参照)。keyには守るべきふたつの決まりがあります(「Rules of keys」参照)。

  • keyの値はJSXノードの配列の中で一意でなければなりません
    • 他のJSXノードの配列と重複するのは結構です。
  • 一度与えたkeyの値は変えないでください
    • もとデータとJSXノードの対応が識別できなくなるからです。
    • レンダリングしているときに動的にkeyの値を生成するのもいけません。

配列からそれぞれの要素に対応した新たな要素の配列をつくるのは、Array.prototype.map()メソッドです。もとデータの配列にはつぎのモジュールsrc/data.tspeopleを使うことにします。

src/data.ts
export type Person = {
	id: number; // JSXでkeyとして用いる
	name: string;
	profession: string;
	accomplishment: string;
	imageId: string;
};
export const people: Person[] = [
	{
		id: 0,
		name: 'Creola Katherine Johnson',
		profession: 'mathematician',
		accomplishment: 'spaceflight calculations',
		imageId: 'MK3eW3A'
	},
	// ...[略]...
];

ルートモジュールsrc/App.tsxのコンポーネントListは、つぎのように配列peopleからJSXノードの配列listItemsをつくって返します。

src/App.tsx
import { people } from ./data;
import { getImageUrl } from ./utils;
import type { Person } from ./data;
import ./styles.css;

export default function List() {
	const listItems = people.map((person: Person) => {
		const { accomplishment, id, name, profession } = person;
		return (
			<li key={id}>
				<img src={getImageUrl(person)} alt={name} />
				<p>
					<b>{name}</b>
					{` ${profession} `}
					known for {accomplishment}
				</p>
			</li>
		);
	});
	return <ul>{listItems}</ul>;
}
src/utils.ts
import { Person } from "./data";

export function getImageUrl(person: Person) {
	return `https://i.imgur.com/${person.imageId}s.jpg`;
}

作例のコード全体は、つぎのサンプル007に掲げました。

サンプル007■React + TypeScript: Describing the UI 07

配列要素のデータ形式は変えることなく、条件に合った要素からなる新たな配列を返すのがArray.prototype.filter()メソッドです。前掲サンプル007のListコンポーネントをつぎのように書き替えれば、化学者のリストが表示されます。

src/App.tsx
export default function List() {
	const chemists = people.filter((person) => person.profession === 'chemist');
	// const listItems = people.map((person: Person) => {
	const listItems = chemists.map((person: Person) => {

	});
}

JSXノードのルートをフラグメントの構文<>...</>で包むと、keyは加えられません。<div>などの要素を用いるのがよいでしょう。どうしてもフラグメントでなければならない場合には、<Fragment>構文でkeyを与えてください(「Displaying several DOM nodes for each list item」参照)。

keyの値は一意であることが決まりです。では、データの配列インデックスを使えばよいと考えるかもしれません。実は、keyが与えられないと、Reactは仕方なく配列インデックスを内部的に用いるのです。けれど、データの追加や削除、並べ替えがあると、インデックスは振り直されます。すると、一度与えたkeyの値は変えないというふたつめの決まりが破られるのです(「Why does React need keys?」参照)。

コンポーネントを純粋に保つ(Keeping Components Pure)

純粋な関数は与えられたデータだけを演算します。Reactのコンポーネントも、純粋な関数で書くことにより、コードが増えていっても、バグや意図しない動作に煩わされることは減らせるのです。「関数型プログラミング」の関数には、つぎのふたつの特徴があります(「Purity: Components as formulas」参照)。

  • 関数内のデータだけを加工する: 関数が呼び出される前に外にあったオブジェクトや変数は変えません。
  • 入力が同じなら出力も同じ: 同じ入力を与えられたら、純粋な関数の出力する結果はつねに同じだということです。

つまり、Reactの純粋なコンポーネントは、2度呼び出されても同じJSXを返さなければなりません。それを確かめるために備わっているのがStrictModeです。開発時にデフォルトでは、コンポーネントを初期レンダリングする関数は2度呼ばれますStrictModeは、そうしてコンポーネント関数が純粋であるかどうか試すのです(「Detecting impure calculations with StrictMode」参照)。なお、詳しくは「React + TypeScript: リアクティブなエフェクト(useEffect)のライフサイクル」をお読みください。

純粋な関数は、スコープ外のすでにある変数やオブジェクトは書き替えません(そのような変更は「ミューテーション」と呼ばれます)。けれど、関数がレンダリング時にスコープ内でオブジェクトをつくって変更することは差し支えないのです。つぎのコードがその例で、「ローカルミューテーション」といいます(「Local mutation: Your component’s little secret」参照)。

src/App.tsx
import { FC } from 'react';

type Props = {
	guest: number;
};
const Cup: FC<Props> = ({ guest }) => {
	return <h2>Tea cup for guest #{guest}</h2>;
};
export default function TeaGathering() {
	const cups = [];
	for (let i = 1; i <= 12; i++) {
		cups.push(<Cup key={i} guest={i} />);
	}
	return cups;
}

関数は純粋に保たなければなりません。それでも、画面の更新やアニメーション、データの変更などが求められることはあります。これらが 副作用(side effects) です(「Where you can cause side effects」参照)。

Reactでは、多くの場合副作用はイベントハンドラが扱います。もっとも、コンポーネントに定められたイベントハンドラは、レンダリング中には実行されません。したがって、ハンドラ関数は純粋でなくてよいのです。イベントハンドラに書くことがどうしてもむずかしい副作用には、useEffectをお使いください。副作用の許されたレンダリング後に処理は行われます(「React + TypeScript: リアクティブなエフェクト(useEffect)のライフサイクル」参照)。ただし、使用はできるだけ控えるようにしましょう(「React: エフェクト(useEffect)を使わなくてよい場合とは」参照)。

関数コンポーネントとJSXを中心に、Reactの基本についてご説明しました。文中、TypeScriptの型づけそのものについてはあまり触れていないので、CodeSandboxサンプルのコードをご参照ください。

9
11
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
11