React勉強しなおしてみた #2
前回記事はこちら
本記事の内容
元JavaエンジニアがReactを再学習する記録。
前回はクイックスタートで大まかに概要を掴んだ。今回以降、より詳細に学習していく。
本記事内では下記セクションを学習する。
- UIの記述
UIの記述
公式学習ページ
前回作成したプロジェクトを引き続き使用して学習していく。
初めてのコンポーネント
コンポーネント:UIの構成部品
<h1>
や<li>
などのHTML、CSS、JavaScriptをまとめた再利用可能なUI要素をコンポーネントと呼ぶ。
ReactではHTMLタグを使用するようにコンポーネントを組み合わせたり、ネストしたりしてページ全体をデザインすることが出来る。
例としてHomeを下記のように変更してみた。
import Head from "next/head";
import styles from "@/styles/Home.module.css";
import { Inter } from "next/font/google";
import React from "react";
const inter = Inter({ subsets: ["latin"] });
type Props = {
title: string;
children: React.ReactNode;
};
export function PageLayout(props: Props) {
return (
<>
<Head>
<title>{props.title}</title>
<meta name="description" content={props.title} />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={`${styles.main} ${inter.className}`}>
<h1>{props.title}</h1>
{props.children}
</main>
</>
);
}
import MyButton from "@/components/Button/MyButton";
import MyInput from "@/components/Input/MyInput";
import { MyList } from "@/components/List/MyList";
import { useState } from "react";
import { PageLayout } from "@/layouts/PageLayout";
export default function Home() {
const [value, setValue] = useState("I`m a input");
function handleChange(value: string) {
setValue(value);
}
return (
<PageLayout title="React Learn">
<MyInput value={value} disable={true} onChange={handleChange} />
<MyInput value={value} disable={false} onChange={handleChange} />
<MyList />
<MyButton />
</PageLayout>
);
}
これでHead
の内容とmain
タグを共通化することができた。
コンポーネントの定義
Reactコンポーネントとは、HTMLマークアップを添えることができるJavaScript関数である。
(前回さらっと定義してしまっているが)定義の内容を詳しく見ていく。
export
コンポーネントを定義した際頭につけたexport
(export default
とも記述可能)は、ファイル内の関数(あるいは定数)を他ファイルからimportできるようにする。
export default function MyButton() {
// ...
// ...
export function MyList() {
// ...
importする際の違いは下記の通り。
export default
は一つのファイルにつき一つのみ定義可能なのに対して、export
は複数定義可能。
詳しい違いは「【React/Next.js】「export default」と「export」の違い」
// export defaultの場合
import MyButton from "@/components/Button/MyButton";
// exportの場合
import { MyList } from "@/components/List/MyList";
関数を定義
Reactコンポーネントは普通のJavaScript関数だが、名前は大文字から始まらなければならない。
export function MyList() {
// ...
マークアップ
return
でタグ(以下の場合はli
タグ)を返している。
一行で収まる場合はreturn <></>
で問題ないが、複数行になる場合はreturn (<></>)
としなければならない。
例は前回作成したMyListの中身をコンポーネントとして切り出したもの。
type ListItemProps = Color;
export function MyListItem(props: ListItemProps) {
const { id, title } = props;
return (
<li style={{ display: "flex", gap: 10 }}>
<div style={{ width: 20, height: 20, backgroundColor: id }}></div>
{title}
</li>
);
}
コンポーネントを使う
定義したコンポーネントを実際に使用する。
例として先ほど切り出したMyListItemコンポーネントをMyListコンポーネント内に配置する。
export function MyList() {
return (
<ul>
{colors.map((color) => {
return <MyListItem key={color.id} {...color} />;
})}
</ul>
);
}
これを実際にブラウザで確認すると下記のようになる。
...
<ul>
<li style="display: flex; gap: 10px;">
<div style="width: 20px; height: 20px; background-color: red;"></div>
赤
</li>
<li style="display: flex; gap: 10px;">
<div style="width: 20px; height: 20px; background-color: blue;"></div>
青
</li>
<li style="display: flex; gap: 10px;">
<div style="width: 20px; height: 20px; background-color: yellow;"></div>
黄色
</li>
</ul>
...
Reactは<main>
や<h1>
など小文字で始まるものをHTMLタグ、<MyButton>
や<PageLayout>
など大文字で始まるものをコンポーネントだと解釈する。
コンポーネントのネストと整理方法
コンポーネントは普通のJavaScript関数であるため、MyList, MyListItemコンポーネントのように同一ファイル内に複数定義することも可能。
type Color = {
id: string;
title: string;
};
const colors: Color[] = [
{ id: "red", title: "赤" },
{ id: "blue", title: "青" },
{ id: "yellow", title: "黄色" },
];
export function MyList() {
return (
<ul>
{colors.map((color) => {
return <MyListItem key={color.id} {...color} />;
})}
</ul>
);
}
type ListItemProps = Color;
function MyListItem(props: ListItemProps) {
const { id, title } = props;
return (
<li style={{ display: "flex", gap: 10 }}>
<div style={{ width: 20, height: 20, backgroundColor: id }}></div>
{title}
</li>
);
}
MyListItemコンポーネントはMyListコンポーネント内でレンダーされている。そのため、MyListは親コンポーネントであり、MyListItemコンポーネントを子としてレンダーしているということができる。
ただし、コンポーネントの定義はネストできない。
// Bad
export function MyList() {
function MyListItem() {
// ...
}
// ...
}
// OK
export function MyList() {
// ...
}
function MyListItem() {
// ...
}
コンポーネントのインポートとエクスポート
ルートコンポーネントファイル
今回使用しているサンプルプロジェクトはNext.jsを利用しているためscr/pages/index.tsx
がルートコンポーネントファイルとなる。
コンポーネントのインポートとエクスポート
先ほどsrc/components/List/MyList.tsx
内に作成したMyListItemコンポーネントを別ファイルに分割する。
まずsrc/components/List/MyListItem.tsx
を作成、MyListItemの内容を移動させる。先ほどは同ファイル内でしか使用していなかったため、MyListItemをexportしていなかったが、別ファイルに切り出したためexportする。
import { Color } from "./MyList";
type ListItemProps = Color;
export function MyListItem(props: ListItemProps) {
const { id, title } = props;
return (
<li style={{ display: "flex", gap: 10 }}>
<div style={{ width: 20, height: 20, backgroundColor: id }}></div>
{title}
</li>
);
}
MyListItem内で使用するためColor
をexportし、MyListにMyListItemをimportして完了。
import { MyListItem } from "./MyListItem";
export type Color = {
id: string;
title: string;
};
const colors: Color[] = [
{ id: "red", title: "赤" },
{ id: "blue", title: "青" },
{ id: "yellow", title: "黄色" },
];
export function MyList() {
return (
<ul>
{colors.map((color) => {
return <MyListItem key={color.id} {...color} />;
})}
</ul>
);
}
同じファイルから複数のコンポーネントをエクスポート・インポートする
先述の通りdefault export
は複数存在できないため、Named export(default
を付けないただのexport
)を使用する。default export
とNamed exportは共存できる。
例としてsrc/components/Button/MyButton.tsx
にCancelButtonを追加してみた。
export default function MyButton() {
const props = {
className: "normal-button",
backgroundColor: "blue",
color: "white",
title: "I`m a normal button",
};
function handleClick() {
alert("You clicked me!");
}
return (
<button
className={props.className}
style={{ backgroundColor: props.backgroundColor, color: props.color }}
onClick={handleClick}
>
{props.title}
</button>
);
}
export function CancelButton() {
return (
<button className="cancel-button" style={{ backgroundColor: "red", color: "white" }}>
I'm a cancel button
</button>
);
}
importはこうなる。
import MyButton, { CancelButton } from "@/components/Button/MyButton";
JSX でマークアップを記述する
JSXとはJavaScriptファイル内にHTMLのようなマークアップを書けるよう拡張したもの。
JSX: JavaScript にマークアップを入れ込む
従来のweb開発ではコンテンツをHTML、デザインはCSS、ロジックをJavaScriptで書き、それらを別ファイルにしていたが、Reactではロジックとマークアップを同じ場所に書く。
レンダリングロジックとマークアップを同じ場所に書くことで、毎回の編集時に同期されることが保証される。逆に直接関係ないコンポーネントの内容は分離されるため、それぞれをより安全に独立して更新できるようになる。
個々のReactコンポーネントはJavaScript関数であり、内部にマークアップを含めることができる。そのマークアップを表現するため、ReactコンポーネントはJSXを使用する。
JSXはHTMLと似ているが、より構文が厳密である。
HTMLをJSX に変換する
以降公式ページに載っているサンプルを使用して試してみる。
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
>
<ul>
<li>Invent new traffic lights
<li>Rehearse a movie scene
<li>Improve the spectrum technology
</ul>
このままコピペしても当然エラーになる。
JSX のルール
単一のルート要素を返す
コンポーネントは一つの要素しか返せない。そのため、複数の要素を返すには<div>
タグで囲む。余分な要素を加えたくない場合は、<></>
(Fragment)を使用する。
今回サンプルではFragmentを使用した。
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
>
<ul>
<li>Invent new traffic lights
<li>Rehearse a movie scene
<li>Improve the spectrum technology
</ul>
</>
すべてのタグを閉じる
JSXではすべてのタグを明示的に閉じなければならない。<img>
のような自己完結タグは<img />
となり、囲みタグは必ず閉じタグを書かなければならない。
以上の点に留意してサンプルを修正する。
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
/> <!-- 自己完結タグの場合"/>"とする -->
<ul>
<li>Invent new traffic lights</li> <!-- 閉じタグを必ず書く -->
<li>Rehearse a movie scene</li>
<li>Improve the spectrum technology</li>
</ul>
</>
(ほぼ)すべてキャメルケースで!
JSXはJavaScriptに変換され、中に書かれた属性はJavaScriptのキーとなる。そのため属性名はJavaScriptの制約を受けることになる。例えばハイフンを含めた名前や、class
などの予約語は使用できない。そのためReactではキャメルケースで書く。
例えばstroke-width
はstrokeWidth
、class
はclassName
となる。
ただし、area-*
とdata-*
はHTML属性と同じようにハイフン付きで書く。
以上の点に留意してサンプルを修正する。
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo" <!-- "class"は予約語のため、代わりに"className"を使う -->
/>
<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
<li>Improve the spectrum technology</li>
</ul>
</>
ヒント:JSX コンバータを使う
上記のことをいちいち手動でやるのは面倒くさいので、HTMLやSVGをJSXに変換する際にはコンバータが利用したい。
ちなみにサンプルは以下のようになる。
export default function TodoList() {
return (
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
<li>Improve the spectrum technology</li>
</ul>
</>
);
}
JSXに波括弧でJavaScriptを含める
JSX内で動的なプロパティを利用したい場合、{}
を使用することで実現できる。
引用符で文字列を渡す
先ほど作成したCancelButtonを例に見ていく。
JSXに文字列の属性を渡したい場合、''
もしくは""
で囲む。例ではclassName
に"cancel-button
、style
のbackgroundColor, color
にそれぞれ"red", "white"
を渡している。
export function CancelButton() {
return (
<button className="cancel-button" style={{ backgroundColor: "red", color: "white" }}>
I'm a cancel button
</button>
);
}
これを動的に変更したい場合は、下記のMyButtonのように{}
で囲む。こうすることでJSX内で直接JavaScriptを使用できる。
export default function MyButton() {
const props = {
className: "normal-button",
backgroundColor: "blue",
color: "white",
title: "I`m a normal button",
};
function handleClick() {
alert("You clicked me!");
}
return (
<button
className={props.className}
style={{ backgroundColor: props.backgroundColor, color: props.color }}
onClick={handleClick}
>
{props.title}
</button>
);
}
波括弧は JavaScript 世界への窓口
JSXはJavaScriptを書く方法の一つであり、その中でJavaScriptを使用することも当然できる。その手段が{}
である。
試しにpropsで受け取ったページタイトルの横にアプリのタイトルを入れてみた。
// ...
export function PageLayout(props: Props) {
return (
<>
<Head>
<title>{props.title} | React Learn</title>
{// ...}
</Head>
{// ...}
</>
);
}
{}
内には関数の呼び出し等あらゆるJavaScriptが動作する。
例ではPageLayoutに当日の日付を表示させた。
// ...
export function PageLayout(props: Props) {
const today = new Date();
return (
<>
{// ...}
<main className={`${styles.main} ${inter.className}`}>
<h1>{props.title}</h1>
<div>{new Intl.DateTimeFormat("ja-JP").format(today)}</div>
{props.children}
</main>
</>
);
}
波括弧を使える場所
JSX内で{}
を使用できるのは2パターン。
- テキストとして、JSXタグ内で直接使用(例:
<title>{props.title} | React Learn</title>
) - 属性として、
=
の後に使用(例:<button className={props.className}></button>
「ダブル波括弧」でJSX内にCSS やその他のオブジェクトを含める
文字列、数字、JavaScriptの式の他、オブジェクトをJSXに渡すことも可能。
下記の例のstyleのようにオブジェクト{ backgroundColor: "red", color: "white" }
を{}
でラップして{{ backgroundColor: "red", color: "white" }}
のようにする必要がある。
またbackground-color
をbackgroundColor
としているように、style属性もキャメルケースで書く必要がある。
export function CancelButton() {
return (
<button className="cancel-button" style={{ backgroundColor: "red", color: "white" }}>
I'm a cancel button
</button>
);
}
コンポーネントにpropsを渡す
Reactコンポーネントは互いにやり取りをする際propsを使用する。親コンポーネントは子コンポーネントにpropsを渡すことで情報を伝えることができる。propsではオブジェクトや配列、関数等あらゆるJavaScriptの値を渡すことができる。
お馴染みのprops
propsとはJSXタグに渡す情報のこと。例えばimg
にはclassName, src, alt, width, height
をpropsとして渡すことができる。
img
等HTML標準に準拠したReactDOMなら渡すことのできるpropsは事前に決められているが、独自のコンポーネントの場合任意のpropsを渡すことができる。
コンポーネントにpropsを渡す
例としてMyButtonを使用する。現在のMyButtonは以下のように定義されている。
export default function MyButton() {
const props = {
className: "normal-button",
backgroundColor: "blue",
color: "white",
title: "I`m a normal button",
};
function handleClick() {
alert("You clicked me!");
}
return (
<button
className={props.className}
style={{ backgroundColor: props.backgroundColor, color: props.color }}
onClick={handleClick}
>
{props.title}
</button>
);
}
子コンポーネントにpropsを渡す
MyButton内で定義されているprops
の内容を親コンポーネントからpropsとして渡す。
// ...
export default function Home() {
// ...
return (
<PageLayout title="React Learn">
{// ...}
<MyButton
className="normal-button"
backgroundColor="blue"
color="white"
title="I`m a normal button"
/>
</PageLayout>
);
}
子コンポーネントからpropsを読み出す
引数として先ほど渡したpropsを受け取る。
propsを使用することで、親と子コンポーネントを独立して考えることができるようになる。
type Props = {
className: string;
backgroundColor: string;
color: string;
title: string;
};
export default function MyButton({ className, backgroundColor, color, title }: Props) {
function handleClick() {
alert("You clicked me!");
}
return (
<button
className={className}
style={{ backgroundColor: backgroundColor, color: color }}
onClick={handleClick}
>
{title}
</button>
);
}
propsのデフォルト値を指定する
propsに値が渡されなかった際使用するデフォルト値を設定したい場合、分割代入の中でパラメータ名の直後に=
と続けて値を指定できる。
export default function MyButton({
className,
backgroundColor,
color = "white", // colorが指定されなかった場合、デフォルト値"white"が使用される
title }: Props) {
// ...
}
JSXスプレッド構文でpropsを転送する
propsの受け渡しにはスプレッド構文を使用することも可能。先ほどのMyButtonの例なら下記のようにできる。
// ...
export default function Home() {
// ...
const buttonProps = {
className: "normal-button",
backgroundColor: "blue",
color: "white",
title: "I`m a normal button",
};
return (
<PageLayout title="React Learn">
{// ...}
<MyButton {...buttonProps} />
</PageLayout>
);
}
childrenとしてJSXを渡す
組み込みタグと同様に独自コンポーネントもネストできる。その場合、親コンポーネントは中身をchildren
というpropsとして受け取る。
// ...
type Props = {
title: string;
children: React.ReactNode;
};
export function PageLayout(props: Props) {
return (
<>
{// ...}
<main className={`${styles.main} ${inter.className}`}>
<h1>{props.title}</h1>
{props.children}
</main>
</>
);
}
propsは時間とともに変化する
propsとはコンポーネントの最初の時点ではなく、任意の時点でのコンポーネントのデータを反映するものである。しかし、propsは不変でなければならない。そのためコンポーネントのpropsがユーザーの操作等で変わらなければならない場合、親コンポーネントから新しいオブジェクトを渡してもらう必要がある。
条件付きレンダー
条件を満たす場合にJSXを返す
if文によって返すJSXを変更する。
例としてMyTitleコンポーネントを作成した。withMark
の値によって返す内容が変わる。
type Props = {
title: string;
withMark: boolean;
};
export function MyTitle(props: Props) {
if (props.withMark) {
return <h1>◇ {props.title}</h1>;
}
return <h1>{props.title}</h1>;
}
null
を使って何も返さないようにする
何もレンダーしたくない場合、null
を返してしまえばいい。
例としてMyListItemを変更してみた。isDislike
がtrue
であれば何も表示されない。
import { Color } from "./MyList";
type ListItemProps = Color & {
isDislike: boolean;
};
export function MyListItem(props: ListItemProps) {
const { id, title, isDislike } = props;
if (isDislike) {
return null;
}
return (
<li style={{ display: "flex", gap: 10 }}>
<div style={{ width: 20, height: 20, backgroundColor: id }}></div>
{title}
</li>
);
}
条件付きでJSXを含める
条件(三項)演算子 (?:)
JSX内で三項演算子を使用することもできる。
先ほど作成したMyTitleを三項演算子に変更すると以下のようになる。
export function MyTitle(props: Props) {
return <h1>{props.withMark ? `◇ ${props.title}` : props.title}</h1>;
}
論理AND演算子(&&)
&&
を使用した例。条件が真の場合レンダーされ、それ以外の場合何もレンダーされない。
ただし&&
の左辺が0
の場合、式全体が0
と評価されてしまうため0
が表示されてしまう。count && <p>message</>
とするのではなく、count > 0 && <p>message</>
とする必要がある。
export function MyTitle(props: Props) {
return (
<h1>
{props.withMark && "◇ "}
{props.title}
</h1>
);
}
条件付きでJSXを変数に割り当てる
if文とlet
を使用して記述することもできる。MyTitleをこの形に変更してみると下記のようになる。
export function MyTitle(props: Props) {
let h1Title = props.title;
if (props.withMark) {
h1Title = "◇ " + props.title;
}
return <h1>{h1Title}</h1>;
}
リストのレンダー
配列からデータをレンダー
mapを利用して配列からアイテムのリストを生成する。
下記は以前作成したMyListの例。colors
配列の内容をリストにして表示する。
import { MyListItem } from "./MyListItem";
export type Color = {
id: string;
title: string;
};
const colors: Color[] = [
{ id: "red", title: "赤" },
{ id: "blue", title: "青" },
{ id: "yellow", title: "黄色" },
];
export function MyList() {
return (
<ul>
{colors.map((color) => {
return <MyListItem {...color} />;
})}
</ul>
);
}
アイテムの配列をフィルタする
filter
を使用して配列を絞り込んで表示させる。
下記の例ではColor
にtone
を増やし、warm
のものだけ表示するよう変更した。
import { MyListItem } from "./MyListItem";
export type Color = {
id: string;
title: string;
tone: "warm" | "cold";
};
const colors: Color[] = [
{ id: "red", title: "赤", tone: "warm" },
{ id: "blue", title: "青", tone: "cold" },
{ id: "yellow", title: "黄色", tone: "warm" },
];
export function MyList() {
const warmColors = colors.filter((color) => color.tone === "warm");
return (
<ul>
{warmColors.map((color) => {
return <MyListItem {...color} />;
})}
</ul>
);
}
key
によるリストアイテムの順序の保持
配列の各アイテムにはkey
を渡す必要がある。これは配列内の他アイテムと区別するための一意な文字列あるいは数値のことである。key
は配列のどの要素がどのコンポーネントに対応するのかをReactが判断し、後で更新するために必要になる。配列内の要素が移動、削除、追加された場合、適切なkey
を指定することで、DOMツリーに正しい更新を反映させることができる。
// ...
export function MyList() {
const warmColors = colors.filter((color) => color.tone === "warm");
return (
<ul>
{warmColors.map((color) => {
return <MyListItem key={color.id} {...color} />; // idをkeyとして指定した
})}
</ul>
);
}
key
をどこから得るのか
- データベースからのデータ:データベースから取得したデータの場合、キーやIDは必然的に一位であるためそれを利用できる。
- ローカルで生成されたデータ:ローカルで生成したデータの場合、アイテムを作成する際に、インクリメンタルなカウンタあるいは
crypto.randomUUID()
、uuid
などのパッケージを使用する。
key
のルール
- キーは兄弟間で一位でなければならない。ただし、別の配列に対応するJSXノードには同じキーを使用できる。
- キーは変更してはいけない。
key={Math.random()}
など、レンダーの最中にキーを生成してはいけない。
key
を必要とするのか
なぜReactは兄弟間で項目を識別するため。並べ替えや削除、追加されたとしてもkey
のおかげでReactは正しく識別することができる。
コンポーネントを純粋に保つ
純粋性:コンポーネントとは数式のようなもの
純関数(pure function)とは以下のような特徴を持つ関数を指す。
- 自分の仕事に集中する。呼び出される前に存在していたオブジェクトや変数を変更しない。
- 同じ入力には同じ出力。同じ入力を与えると純関数は同じ結果を返す。
Reactはすべてのコンポーネントが純関数であると仮定して設計されている。同じ入力をされたら常に同じJSXを返さなければならない。
副作用:意図せぬ(?)付随処理
上記だけだといまいちピンとこないため、公式ページに載っていた悪い例を見ていく。
let guest = 0;
function Cup() {
// Bad: changing a preexisting variable!
guest = guest + 1;
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup />
<Cup />
<Cup />
</>
);
}
外部で宣言されたguest
を読み書きしているせいで、Cupコンポーネントを呼び出すたびに(propsは同じにもかかわらず)違うJSXが返される。
同じ入力には同じ結果が返されなければならないため、正しくは下記のようになる。
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaSet() {
return (
<>
<Cup guest={1} />
<Cup guest={2} />
<Cup guest={3} />
</>
);
}
ローカルミューテーション:コンポーネントの小さな秘密
上記の例のようにコンポーネントがレンダーの最中に既存の変数を変更してしまうことをミューテーションと呼ぶ。純関数は関数のスコープ外の変数や、呼び出し前に作成されたオブジェクトをミュ―テートしてはならない。
しかしレンダー中に関数内で作成した変数やオブジェクトであれば、書き換えても問題ない。
function Cup({ guest }) {
return <h2>Tea cup for guest #{guest}</h2>;
}
export default function TeaGathering() {
let cups = [];
for (let i = 1; i <= 12; i++) {
cups.push(<Cup key={i} guest={i} />);
}
return cups;
}
上記の例は関数内で作成されたcups
にpush
している(TeaGathering
内で完結している)ため問題ない。これをローカルミューテーションと呼ぶ。
副作用を引き起こせる場所
ではどこで変化させればいいのか。Reactでは副作用(スクリーンの更新、アニメーションの開始、データの変更などの変化)は通常イベントハンドラ内に属する。イベントハンドラはコンポーネント内に定義されてはいるが、レンダーの最中に実行されるわけではない。そのためイベントハンドラは純粋である(純関数である)必要がない。
副作用を書くのに適切なイベントハンドラがJSXにない場合、最終手段としてuseEffect
を付加することも可能。
UIをツリーとして理解する
このセクションはアプリが小さいうちはあまり恩恵はないが、大きくなってきた際に真価を発揮しそうな内容である。
正直今はちんぷんかんぷんであるため、後々見返したい。
UIをツリーとして理解する
ツリーとはアイテム間の関係を表すモデルの一種。例えばブラウザはHTMLやCSSをモデル化するためにツリー構造を使用する。モバイルプラットフォームもビューの階層構造を表現するためツリー構造を使用する。Reactも同様にツリー構造を使用して、アプリ内のコンポーネント間の関係を管理しモデル化する。
レンダーツリー
コンポーネントの主要な特徴の一つ、コンポーネント同士を組み合わせること。コンポーネントをネストすることで、親コンポーネント、子コンポーネントという概念が発生する。その親コンポーネントも別のコンポーネントの子であることもある。
Reactアプリをレンダーする際、この関係性をツリーとしてモデル化したものをレンダーツリーと呼ぶ。
練習用に使用しているプロジェクトをレンダーツリーを構築すると下記のようになる。
Home
└ PageLayout
├ MyInput
├ MyButton
└ MyList
└ MyListItem
ツリー構造はノードで構成されており、各ノード(MyInput, MyButton等)はコンポーネントを表す。
Reactレンダーツリーのルートノードはアプリのルートコンポーネントとなる。上記の例の場合はHomeがルートコンポーネントであり、Reactが最初にレンダーするコンポーネントである。
条件付きレンダーでレンダーツリーが異なることがあるにせよ、このようなツリーは一般的にReactアプリケーションにおいてトップレベルコンポーネントとリーフ(葉, 末端) コンポーネントがどれなのかを理解するのに役立つ。トップレベルコンポーネントとはルートコンポーネントに最も近い(上記ではPageLayout)コンポーネントを指し、下位すべてのコンポーネントのレンダーパフォーマンスに影響を与える。リーフコンポーネントはツリーの下層にあり、子コンポーネントを持たず、通常頻繁に再レンダーされる。
これらを特定することにより、アプリケーションのデータの流れとパフォーマンスの理解に役立つ。
モジュール依存関係ツリー
Reactアプリにおいてもう一つツリー構造でモデル化できるものがアプリのモジュールの依存関係。この場合、枝はimportを表す。
下記は練習用アプリをモジュール関係ツリーにしたもの。これではコンポーネント以外のモジュールを読み込んでいないため、上記のレンダーツリーと全く同じになってしまっているが、本来はレンダーツリーにはコンポーネントのみ、モジュール依存関係ツリーにはコンポーネントを含まないモジュールも含む。
Home
└ PageLayout
├ MyInput
├ MyButton
└ MyList
└ MyListItem
依存関係ツリーはReactアプリを実行するためどのモジュールが必要か判断するのに役立つ。本番環境用にビルドする際、クライアントに送信するために必要なJavaScriptをすべてバンドルにまとめるビルドステップが存在する。これを担当するツールであるバンドラは、依存関係ツリーを使用することでどのモジュールを含めるべきか決定する。アプリが大きくなるにつれてバンドルサイズも大きくなり、クライアントがダウンロードして実行するコストもかかるようになる。UIが描画されるまでの時間も遅くなる。その際のデバッグとして役立つ。
Discussion