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コンポーネントのつくり方は、つぎのとおりです。
- コンポーネントは
export
しなければなりません。 - 関数コンポーネントを定めます。
-
export
はdefault
でも名前つきでも構いません。
-
- 戻り値はJSXで加えます。
- JSXの記述は原則1行です。
- 複数行にするときは、必ずかっこ
()
でくくってください。- かっこがないと、
return
文は改行で終了とみなされるからです。
- かっこがないと、
Reactの(関数)コンポーネントは、標準JavaScriptの関数として定めます。ただし、名前はアッパーキャメルケース(パスカルケース)でなければなりません(「Step 2: Define the function」参照)。
つぎのコードがコンポーネントの例です(JavaScriptモジュール)。TypeScriptを使った場合、戻り値は推論されます。明示したいときは、JSX.Element
で型づけしてください。
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)。
import { FC } from 'react';
import { Profile } from './Profile';
export const Gallery: FC = () => {
return (
<section>
<h1>Amazing scientists</h1>
<Profile />
</section>
);
};
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
、子コンポーネント(Gallery
とProfile
)は名前つきにしました。import
の構文が少し変わりますのでご注意ください。チームでどちらか一方に統一するのもひとつのアイデアです(「Default vs named exports」参照)。なお、default
のexport
はひとつのモジュールからひとつしかできません。
JSXでマークアップする(Writing Markup with JSX)
JSXはJavaScriptの構文拡張です。JavaScriptコードの中に、HTMLのようなマークアップが記述できます。
Webがインタラクティブになるにつれ、コンテンツはロジックによって決まるようになってきました。そこで、ReactはHTMLをJavaScriptに委ね、レンダリングロジックとマークアップがともにコンポーネントに含められたのです。
JSXの構文はほぼHTMLにしたがっています。ただし、規則はもう少し厳格です。
- 戻り値はひとつのルート要素にまとまっている必要があります。
- ひとつの要素に含めたくない場合にはフラグメントが使えます(「<Fragment> (<>...</>)」参照)。
- タグはすべて閉じなければなりません。
- 空要素も必ず閉じてください。
- 要素に加えるプロパティ(属性)はほとんどの場合キャメルケースです(「Common components (e.g. <div>)」参照)。
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の変数(photo
とdescription
)が参照できます(サンプル003)。
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
)は、コンポーネントの外に定めて結構です。
import { Gallery } from './Gallery';
import { Footer } from './Footer';
import './styles.css';
export default function App(): JSX.Element {
return (
<>
<Gallery />
<Footer />
</>
);
}
import { FC } from 'react';
const copyrightOwner = 'Fumio Nonaka';
export const Footer: FC = () => {
const getYear = () => new Date().getFullYear();
return (
<footer>
Copyright ©2000-{getYear()} {copyrightOwner}
</footer>
);
};
JSXにJavaScriptコードのオブジェクトを与える場合には、リテラルの波かっこ{}
も加わって二重になります。なお、CSSのプロパティはキャメルケース(backgroundColor
とfontFamily
およびlineHeight
)になることにお気をつけください。
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 ©2000-{getYear()} {copyrightOwner}
</footer>
);
};
さらに、複数の変数(copyrightOwner
)やオブジェクト(styles
)をひとまとまりで扱うために、ひとつのオブジェクト(footerInfo
)に収めることもあるでしょう(サンプル004)。
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 ©2000-{getYear()} {copyrightOwner}
</footer>
);
};
サンプル004■React + TypeScript: Describing the UI 04
コンポーネントにプロパティを渡す(Passing Props to a Component)
Reactのコンポーネントは、props
と呼ばれるプロパティで子にデータを渡します。渡し方はHTMLタグの属性と同じ構文です。ただし、データはテキストだけでなく、オブジェクトや配列、関数などJavaScriptの値すべてが含まれます。さらに、子コンポーネントに含めるノード(テキスト)も渡しましょう。
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 ©2000-{getYear()} {footerInfo.copyrightOwner}
</Footer>
</>
);
}
子コンポーネントが引数に受け取るのは、親から渡されたすべてのプロパティを収めたオブジェクト(props
)です。つぎのコード例のFooter
コンポーネントは、オブジェクトの分割代入で必要なプロパティ(copyrightOwner
とstyles
)を取り出しています。また、受け取る子ノードのプロパティはchildren
(型React.ReactNode
)です(「childrenとしてJSXを渡す」参照)。コード全体は以下のサンプル005に掲げました。
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
)をスプレッド構文で与えるように書き替えたのがつぎに抜き出したコードです。
export default function App(): JSX.Element {
return (
<>
{/* <Footer footerInfo={footerInfo}> */}
<Footer {...footerInfo}>
Copyright ©2000-{getYear()} {footerInfo.copyrightOwner}
</Footer>
</>
);
}
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の値が異なります。
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>
);
}
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)。
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
(null
やundefined
も同様)はレンダリングしません(「Logical AND operator (&&
)」参照)。
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.ts
のpeople
を使うことにします。
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
をつくって返します。
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>;
}
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
コンポーネントをつぎのように書き替えれば、化学者のリストが表示されます。
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」参照)。
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サンプルのコードをご参照ください。