ホーム
← 本棚に戻る

React

Study Roomの資料は、keysubがAIを活用して作成・編集しています。


1. reactプロジェクトの作成- 展開/折りたたみ

CRA(Create React App)は、Reactプロジェクトを作成する過去の標準的な方法で、基本的なJavaScriptプロジェクトとTypeScriptテンプレートをサポートしています。プロジェクトの作成に必要なパッケージも一緒にインストールします。

このコースではViteとTypeScriptを使用します。 Viteは、高速で軽量な開発サーバーとビルドツールです。

ステップ 命令と結果
1. プロジェクトの作成 npm create vite@latest 프로젝트이름 -- --template react-ts
2. プログラムのインストール npm install
3. サーバーのオン(実行) npm run dev
4. ビルド npm run build実行時dist/フォルダができた(npm run previewで確認可能)

2. リエクトの基礎- 展開/折りたたみ

Reactプロジェクトには、画面を作るライブラリと開発・ビルドを助けるツールが一緒にインストールされます。package.jsonでパッケージと実行コマンドを確認できます。

区分 説明 はい
dependencies アプリの実行に必要なライブラリ react, react-dom
devDependencies 開発、タイプ検査、ビルドに使用するツール vite, typescript, @vitejs/plugin-react
scripts 頻繁に実行するコマンドを登録する項目 npm run dev, npm run build

npm install銀package.jsonに宣言されたパッケージをインストールし、package-lock.jsonはインストールされているパッケージのバージョンを記録し、開発環境の違いを減らします。

2.1 フォルダ構造と説明- 展開/折りたたみ

ViteとReact、TypeScriptテンプレートで作成された基本的なプロジェクトには、次の構造があります。プロジェクトの設定やアドインによって、ファイルとフォルダは異なる場合があります。

프로젝트/
├── public/              정적 파일 (빌드 과정에서 그대로 복사)
├── src/                 React 애플리케이션 소스
│   ├── assets/          컴포넌트에서 사용하는 이미지 등의 자원
│   ├── App.tsx          최상위 애플리케이션 컴포넌트
│   ├── main.tsx         애플리케이션 진입점
│   └── ...              기능별 컴포넌트와 스타일
├── index.html           브라우저가 처음 받아 보는 HTML
├── package.json         패키지와 npm 명령
├── package-lock.json    설치 패키지 버전 기록
├── tsconfig*.json       TypeScript 설정
└── vite.config.ts       Vite 설정

2.1.1 個別ファイルの説明

2.1.1.1 ようこそページ:index.html

Viteプロジェクトではindex.htmlこのpublic/ではなく、プロジェクトのルートにあります。ブラウザが最初に表示するHTMLで、Reactが画面を配置するDOM要素とTypeScriptエントリファイルを指定します。一方、Create React App(CRA)の基本構造では、開始HTMLpublic/index.htmlにありました。

<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>

public/にはアプリコードからimportせずに、指定されたURLに直接提供する静的ファイルを残します。たとえば、ファビコン(favicon.ico)、検索エンジンクロールルールファイル(robots.txt)、PWAマニフェスト(manifest.webmanifest)、直接パスとして使用する画像などを置くことができます。

public/
├── favicon.ico
├── robots.txt
└── images/
    └── logo.png

Viteはpublic/中のファイルをビルドした結果dist/にパスとファイル名を保持したままコピーします。したがって、public/images/logo.pngアプリで/images/logo.pngパスとして参照できます。

<img src="/images/logo.png" alt="로고" />

コンポーネントから直接import使用する画像などは通常src/assets/に置きます。これらのファイルはViteのビルドプロセスで処理され、結果のファイル名やパスが異なる場合があります。まとめるとpublic/はURLとファイル名をそのまま維持しなければならないファイルに、src/assets/は、ソースコードから取得して使用するリソースに適しています。

2.1.1.2 src/main.tsx:アプリエントリポイントとレンダリング

index.htmlこの呼び出しmain.tsxはReactアプリの開始点です。このファイルは#root要素を見つけてReactルートを作成し、その中に最上位のコンポーネントである<App />をレンダリングします。

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

createRootはReactが管理するDOM領域を設定し、renderは、その領域に渡したReact要素を画面に表示します。<StrictMode>は開発中に潜在的な問題を見つけるのを助けるチェック機能であり、ユーザーに見える別のUIを作成しません。

2.1.1.3 src/App.tsx:トップレベルコンポーネント

App.tsxは、アプリのメイン画面とサブコンポーネントを組み合わせたスタートアップコンポーネントです。 JSXを返します。main.tsx秋<App />をレンダリングすると、ReactはAppとその下のコンポーネントを画面に反映します。

function App() {
  return (
    <main>
      <h1>안녕하세요, React!</h1>
    </main>
  );
}

export default App;

レンダリングは、コンポーネントが返したJSXに基づいて画面を計算してDOMに反映するプロセスです。初めてアプリを起動したときだけでなく、stateが変わるなど画面に影響する変化が生じた場合、Reactはコンポーネントを再実行して必要な部分を更新します。コンポーネント関数の再実行とブラウザDOM全体の新規作成は異なります。リレンダリングが発生する時点は、第3章「コンポーネント開発」のリレンダリングタイミングを参照してください。.

実行フローをクリーンアップすると、次のようになります。

index.html → src/main.tsx → src/App.tsx → 화면에 표시

2.1.2 JavaScriptとTypeScriptファイルの違い

JavaScriptファイルは通常.jsまたは.jsx、TypeScriptファイルは.tsまたは.tsx拡張子を使用します。 JSX文法を一緒に書くReactコンポーネントはそれぞれ.jsx, .tsxで書く。

  • TypeScript:開発中の型を調べるため、誤ったデータ型を事前に確認できます。
  • JavaScript: 別の型チェックがないため、データ型が合わない問題を実行中にしか検出できません。

TypeScriptの型表記は、ビルド中に削除され、JavaScriptに変換されます。最終的にブラウザはJavaScriptを実行します。

2.2 コンポーネントとは?- 展開/折りたたみ

コンポーネントは、画面を構成する再利用可能な単位です。ボタンやタイトルなど、小さなUIからページ全体までコンポーネントで作成でき、複数のコンポーネントを組み合わせて画面を構成します。

このコースでは、関数でコンポーネントを定義します関数コンポーネントを使用します。関数コンポーネントはJSXを返し、コンポーネント名は大文字で始まります。

2.2.1 SPAでコンポーネントとして画面を作成する

Reactアプリは通常、シングルページアプリケーション、シングルページアプリケーション(SPA)方式で動作します。従来のマルチページWebサイトでは、別のページに移動すると、ブラウザがサーバーに新しいHTML文書を要求し、受信した文書で画面を再表示することがよくあります。 SPAは最初にHTML文書を読み込んだ後、画面が変わるたびに文書全体を再要求するのではなく、アプリ内で必要なコンポーネントをレンダリングまたは置き換えて画面を整理します。

たとえば、リスト画面から詳細画面に移動するときは、リストコンポーネントを下げて詳細コンポーネントをレンダリングしたり、2つのコンポーネントを組み合わせて一部の画面のみを変更したりできます。画面切り替えは、ルータが現在のアドレスに合ったコンポーネントを選択するように実装できます。ただし、SPAもサーバーにデータを要求でき、ブラウザの更新や外部ページに移動するときは新しい文書を要求できます。

function Greeting() {
  return <h1>안녕하세요, React!</h1>;
}

2.2.2 コンポーネント名はなぜ大文字で始まるのか?

JSXで小文字で始まるタグは、<div>, <button>同様にHTML要素として解釈されます。一方、大文字で始まるタグはJavaScriptで定義されたコンポーネントとして解釈されます。したがって、自分で作成したコンポーネントは大文字で始まる名前を使用する必要があります。

function Greeting() {
  return <p>반갑습니다.</p>;
}

function App() {
  return (
    <main>
      <Greeting />
    </main>
  );
}

<Greeting />銀Greetingコンポーネントを画面に含めることを意味します。<greeting />同様に小文字で書くと、ReactコンポーネントではなくHTMLタグとして扱われます。

2.2.3 コンポーネントは JSX でタグのように使用する

コンポーネントは通常の関数のように直接呼び出すよりも<Greeting />のようなJSXタグの形式で使用します。 JSXはこの表記をReactが解釈できる要素に変換し、Reactはその要素を画面にレンダリングします。

function Greeting() {
  return <p>안녕하세요!</p>;
}

function App() {
  return (
    <section>
      <h1>인사말</h1>
      <Greeting />
      <Greeting />
    </section>
  );
}

同じコンポーネントを複数回配置することも、コンポーネントが返すJSX内に他のコンポーネントを組み合わせることもできます。<Greeting />同様に、スラッシュで閉じる表記は、内容のないコンポーネントに使用するJSX文法です。

コンポーネントGreeting()同様に直接呼び出すと、Reactがコンポーネントを管理するフローをスキップするので、JSXタグとして使用する必要があります。コンポーネントはReactにレンダリングする<Greeting />形で配置します。

2.2.4 exportとimport

コンポーネントを他のファイルでも使用するには、そのファイルからexportそして、使用するファイルからimportします。同じファイル内でのみ書き込むコンポーネントであれば、必ずエクスポートする必要はありません。

// Greeting.tsx
export default function Greeting() {
  return <p>안녕하세요!</p>;
}
// App.tsx
import Greeting from './Greeting';

function App() {
  return <Greeting />;
}

export default App;

export defaultはファイルからデフォルトにエクスポートする値を指定し、import Greeting from './Greeting'はその値を取得します。exportは、コンポーネントを他のファイルで使用できるようにするJavaScriptモジュール文法であり、コンポーネントになるための必須条件ではありません。

2.2.5 関数コンポーネントの戻り値

コンポーネントは、画面に表示するJSXを返します。複数の要素を返すには、1 つの親要素でラップするか、<>...</>文法のフラグメントで囲みます。

function Profile() {
  return (
    <>
      <h2>홍길동</h2>
      <p>React 학습 중</p>
    </>
  );
}

コンポーネントは条件に応じて何も表示しないときnullを返すこともできます。 JSXを返す関数だと理解すれば、始めるのに十分です。

2.2.6 関数コンポーネントとクラスコンポーネント

Reactコンポーネントは関数またはクラスとして定義できます。過去には、クラスコンポーネントが状態とライフサイクル機能を担当していましたが、現在は関数コンポーネントとフックを使用する方法が一般的です。このコースでは、関数コンポーネントに基づいて説明します。

2.2.7 JSX文法と規則- 展開/折りたたみ

JSX(JavaScript XML)は、JavaScriptコード内でHTMLに似た文法でUI構造を表現できるようにするJavaScript拡張文法です。 Reactを作っていたFacebook(現在Meta)で導入しており、JSX自体がブラウザが理解するHTMLやJavaScriptではない。 Viteなどのビルドツールは、JSXをブラウザが実行できるJavaScriptに変換します。

const title = 'React';

function Welcome() {
  return <h1>안녕하세요, {title}!</h1>;
}

JSX内でJavaScript式を使用するには中括弧{}で包みます。変数や関数呼び出し、算術式などを入れることができますが、ifドアforドアのようにステートメントを直接置くことはできません。条件によって異なる内容を表示するときは、三項演算子や&&使用できます。

function Greeting({ isLoggedIn }: { isLoggedIn: boolean }) {
  return <p>{isLoggedIn ? '환영합니다!' : '로그인해 주세요.'}</p>;
}

JSXを作成するときは、次の規則を守ってください。

  • 返すJSX要素は1つの親要素で囲みます。実際のDOM要素を追加したくない場合は、Fragment(<>...</>)を使用できます。
  • すべてのタグは閉じる必要があります。内容のないタグは<img />, <input />のように書く。
  • HTML属性名とは異なるJSX属性名を使用します。例えばclass代わりにclassName, for代わりにhtmlForを使用します。
  • 複数の要素からなるJSXreturnするときは、括弧で囲むことをお勧めします。必須のルールではありませんが、改行やインデントを変更するときにJavaScriptの自動セミコロン挿入で発生する可能性があるエラーを防ぎ、構造を読みやすくします。
function Profile() {
  return (
    <section className="profile">
      <h2>홍길동</h2>
      <label htmlFor="email">이메일</label>
      <input id="email" />
    </section>
  );
}
2.2.7.1 JSX変換とReact.createElement

JSXはブラウザですぐに実行されないため、ビルドツールはJavaScriptに変換します。たとえば、過去のJSX変換方式では、React.createElement呼び出しに似たコードに置き換えました。

const element = <h1 className="title">안녕하세요</h1>;
const element = React.createElement(
  'h1',
  { className: 'title' },
  '안녕하세요',
);

これは変換原理を理解するための例です。最新のReactプロジェクトでは、自動JSX変換を使用することが多く、内部的にreact/jsx-runtimeの関数を使用するので、ソースコードで毎回React.createElementを直接書くかReactJSXのためにインポートする必要はありません。

JSX変換結果を確認したい場合は、BabelのオンラインコンバーターにJSXコードを入力してみてください。実際のプロジェクトでは、Viteはビルドプロセス中に自動的に変換します。

2.2.7.2 JSXのエスケープ処理とHTML挿入注意

JSXのテキスト位置に文字列または値を入れると、ReactはデフォルトでそれをHTMLとして解釈せずにプレーンテキストとして表示します。このときHTML特殊文字をエスケープするので、ユーザー入力<script>同じタグで実行されないようにするのに役立ちます。

const userInput = '<script>alert("hello")</script>';

function Message() {
  return <p>{userInput}</p>;
}

上記のコードは、文字列をタグとして実行せずに文字として表示します。 HTML文字列を実際の要素として挿入する必要がある特別な場合dangerouslySetInnerHTMLを使用できます。

function Article() {
  const html = '<strong>중요한 안내</strong>';

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

dangerouslySetInnerHTMLの値は{ __html: 문자열 }型のオブジェクトです。信頼できない入力をそのまま入れるとスクリプト挿入(XSS)の脆弱性が生じる可能性があるため、必要な場合でも入力を検証・浄化した後に使用し、一般的な画面出力にはJSXのテキストレンダリングを使用します。

2.2.7.3 JSXでコメントを作成する

JSXの外側のJavaScript領域では、通常のJavaScriptコメントを書き、JSX要素の間にコメントを入れるときは、中括弧内にコメントを作成します。

function Notice() {
  // JavaScript領域の注釈
  return (
    <section>
      {/* JSX中のコメント*/}
      <p>공지사항입니다.</p>
    </section>
  );
}

2.2.8 コンポーネントの分離と組み合わせ

画面が大きくなると、1つのコンポーネントにすべてのJSXを作成するよりも、役割が明確な小さなコンポーネントに分けるほうが読みやすく管理しやすくなります。こうしてUIを分けることをコンポーネントの分離と言います。分離したコンポーネントは、親コンポーネントのJSXでタグのように配置して1つの画面で構成できます。コンポーネントの組み合わせといいます。

type GreetingCardProps = {
  name: string;
};

function PageHeader() {
  return <header><h1>React 학습 노트</h1></header>;
}

function GreetingCard({ name }: GreetingCardProps) {
  return (
    <section>
      <h2>환영합니다, {name}님!</h2>
      <p>오늘도 React를 함께 배워 봅시다.</p>
    </section>
  );
}

function PageFooter() {
  return <footer>학습을 응원합니다.</footer>;
}

function App() {
  return (
    <main>
      <PageHeader />
      <GreetingCard name="민지" />
      <PageFooter />
    </main>
  );
}

Appはページの大きな構造を担当し、PageHeader, GreetingCard, PageFooterはそれぞれ担当するUIを表現します。GreetingCardはnameをpropsに渡して他の名前でも再利用できます。このように、親コンポーネントは子コンポーネントを組み合わせ、必要なデータはpropsに渡します。 propsの詳細な使い方は第3章「コンポーネント開発」で扱います。

最初からすべての要素を細かく分割する必要はありません。画面の一部が独立した役割を持っている、繰り返し使用されている、または別々に読んで修正するのに便利な場合は、コンポーネントに分離することをお勧めします。小さなコンポーネントが多すぎると、むしろ全体の構造を理解するのが難しくなる可能性があるため、役割と再利用性に基づいて適切なサイズを選択します。

2.3条件、分岐、繰り返し処理

2.3.1 条件付きレンダリング

条件付きレンダリングは、条件の結果に基づいて画面に表示するJSXを選択する方法です。 JavaScriptのifドアはJSXの外で使用でき、JSXの中では三項演算子&&を頻繁に使用します。

type GreetingProps = {
  isLoggedIn: boolean;
};

function Greeting({ isLoggedIn }: GreetingProps) {
  if (!isLoggedIn) {
    return <p>로그인해 주세요.</p>;
  }

  return <p>환영합니다!</p>;
}

2つの結果のいずれかをJSX内で選択する場合は、三項演算子を使用できます。

function LoginStatus({ isLoggedIn }: GreetingProps) {
  return <p>{isLoggedIn ? '로그인 상태' : '로그아웃 상태'}</p>;
}
function Inbox({ unreadCount }: { unreadCount: number }) {
  return (
    <section>
      <h2>받은 편지함</h2>
      {unreadCount > 0 && <p>읽지 않은 메시지: {unreadCount}</p>}
    </section>
  );
}

どの方法を書くかは条件が複雑なのか、レンダリングする内容がどれくらいになるかによって選択し、複雑な条件は JSX 外で計算すると読みやすくなります。

2.3.2 リストのレンダリング

配列に含まれるデータを画面に繰り返し表示するときは、通常map()で各データをJSX要素に変換します。リストが空のときの画面も条件付きで処理できます。

type Course = {
  id: number;
  title: string;
};

const courses: Course[] = [
  { id: 1, title: '컴포넌트 기초' },
  { id: 2, title: '조건부 렌더링' },
];

function CourseList() {
  if (courses.length === 0) {
    return <p>강좌가 없습니다.</p>;
  }

  return (
    <ul>
      {courses.map((course) => (
        <li key={course.id}>{course.title}</li>
      ))}
    </ul>
  );
}

filter()で条件に合ったデータだけを推測した後map()で表示できます。sort()は配列自体の順序を変更するので、 state または共有中のソース配列を直接ソートしないようにコピーをソートします。

const visibleCourses = courses
  .filter((course) => course.title.includes('렌더링'))
  .sort((a, b) => a.title.localeCompare(b.title));

上記の例ではfilter()は新しい配列を作成し、続いて呼び出したsort()はその新しい配列をソートするので、元のcourses配列は変更されません。元の配列にすぐにcourses.sort(...)を呼び出すのは避けてください。

map()結果で各項目が複数のJSX要素で構成されている場合は、Fragmentで囲むことができます。このときリスト項目を区別するkeyを指定するには、短縮文法人<>...</>代わりにFragmentを明示的に使用します。

import { Fragment } from 'react';

function CourseDetails() {
  return courses.map((course) => (
    <Fragment key={course.id}>
      <h3>{course.title}</h3>
      <hr />
    </Fragment>
  ));
}
2.3.2.1 リストレンダリングのキー

keyは、Reactがリストの各項目を互いに区別し、項目が追加・削除・移動されたときにどの要素が変わったかを把握するために使用する特別な値です。リストをレンダリングするときmap()この返す最上位要素にkeyを指定する必要があります。

可能であれば、データに含まれる一意で信頼性の高い識別子(データベースIDなど)を使用してください。配列のインデックスは、項目の順序が変わったり、中間項目が追加・削除されるリストで適切なキーではありません。この場合、Reactは既存の項目と新しい項目を間違ってリンクして、入力値またはコンポーネントの状態が不適切な項目に残ることがあります。リストが固定されていて順序と項目が変わらない場合にのみ、インデックスをキーとして書き込むことを検討してください。

{courses.map((course) => (
  <li key={course.id}>{course.title}</li>
))}

同じリスト内でキーは互いに異なる必要があり、レンダリングするたびに新しい値を作成してキーとして使用しないでください。keyはReactがリストを管理するための値なので、コンポーネントの通常のpropに渡されません。コンポーネントの中でもIDが必要な場合keyとは別に渡します。

3. コンポーネント開発- 展開/折りたたみ

3.1 リレンダリングタイミング

リレンダリングは、Reactがコンポーネントを再実行して画面に表示する内容を新たに計算するプロセスです。通常、次のような場合に始まります。

  1. コンポーネントの state が変わった場合
    state設定関数で値を更新すると、Reactはそのコンポーネントを再レンダリングします。新しい値が以前の値と同じであると判断した場合は、レンダリングをスキップすることもできます。
  2. 親コンポーネントが再レンダリングされた場合
    親が再レンダリングされると、子もデフォルトで再レンダリングされます。このとき、子は親が渡したpropsを受け取り、画面を再計算します。memoを使用すると、propsが同じ場合に子のレンダリングをスキップできますが、ステートやコンテキストの変更など、他の理由がある場合は再レンダリングできます。
  3. 使用中のコンテキスト値が変わった場合
    useContext()でコンテキスト値を読み取るコンポーネントは、その値を提供するProviderの値が変わると再レンダリングされます。 ContextとuseContext()の使用方法は6.4節「useContext」で説明します。

propsの変更は、親レンダリングとは別の開始原因ではなく、親が再レンダリングされたときに子に渡される値が異なる状況と理解すれば簡単です。たとえば、親が再び実行され、子に渡す名前が変更された場合、子は新しい名前を使用して画面を再計算します。

再レンダリングは、画面全体を最初から再作成することを意味しません。 Reactは、コンポーネントが新しく計算した結果を以前の結果と比較し、実際に異なる部分のみをブラウザDOMに反映します。つまり、コンポーネントの再実行と画面のすべての要素が再作成されることは異なることです。

3.2 props- 展開/折りたたみ

props(properties) は、親コンポーネントが子コンポーネントに渡す値を含むオブジェクトです。 JSXから<Greeting name="민지" />のように書くとnameはpropの名前であり、"민지"はそのpropに渡す値です。 Reactはこの値を{ name: '민지' }型のオブジェクトとしてGreetingに渡します。

type GreetingProps = {
  name: string;
};

function Greeting(props: GreetingProps) {
  return <p>안녕하세요, {props.name}님!</p>;
}

function App() {
  return <Greeting name="민지" />;
}

この例ではGreetingPropsは渡されるオブジェクトの外観を調べるTypeScript型の名前であり、渡される値自体ではありません。propsはコンポーネントが受け取ったオブジェクトです。props.nameはそのオブジェクトから取り出したname値です。子は props を直接変更せず、値の変更は親が管理する state を更新して新しい props を渡す方法で処理します。次のセクションでは、propsのデータ型と構造分解、デフォルト値などについて説明します。

3.2.1 propsのデータ型

propsには、文字列だけでなく、数値、ブール、配列、オブジェクト、関数など、さまざまなJavaScript値を渡すことができます。

<UserCard
  name="민지"
  age={20}
  isActive={true}
  hobbies={['독서', '산책']}
  user={{ id: 1, role: 'admin' }}
/>

文字列propsname="민지"のように引用符を使用できます。数値やブール、配列、オブジェクト、変数、関数などのJavaScript式を渡すときは、中かっこを使用します。

3.2.2 props構造分解とデフォルト値

構造分解割り当ては、オブジェクトで指定されたプロパティの値を取り出し、同じ名前の変数に格納するJavaScript文法です。コンポーネントパラメータで使用すると、propsオブジェクトを受け取った後に必要な値を直接取り出して書き込むことができます。

type GreetingProps = {
  name: string;
};

//propsオブジェクトをパラメータとして受け取り、その中で名前を構造分解します。
function Greeting({ name }: GreetingProps) {
  return <h2>안녕하세요, {name}님!</h2>;
}

function App() {
  //JSXでは、name propをGreetingコンポーネントに渡します。
  return <Greeting name="민지" />;
}

App銀<Greeting name="민지" />ローnamepropを渡します。 ReactGreetingに渡すオブジェクトは{ name: '민지' }のような形です。パラメータの{ name }このオブジェクトからnameプロパティを取り出してローカル変数nameに入れるので、関数の中でprops.name代わりにnameとして使用できます。 構造分解を使用しない場合function Greeting(props: GreetingProps)でオブジェクトを受け取った後props.nameにアクセスできます。

propを省略したときに使用するデフォルト値も、パラメータの構造分解割り当てで指定できます。

type GreetingProps = {
  name?: string;
};

function Greeting({ name = '손님' }: GreetingProps) {
  return <h2>안녕하세요, {name}님!</h2>;
}

function App() {
  return <Greeting />;
}

この例では<Greeting />銀nameを渡さないのでnameはデフォルトです'손님'になります。デフォルトはpropが省略されているか、undefinedの場合に適用されます。 Reactがすべてのpropsに共通のデフォルト値を定めておくわけではないので、コンポーネントに必要な値を直接指定します。

3.2.3 関数propsとイベント処理

親コンポーネントは関数を props に渡し、子コンポーネントで親の動作を実行させることができます。イベント処理関数を渡すときは通常onClick, onChange, onSaveようにonで始まる名前を使用します。

type ActionButtonProps = {
  onAction: () => void;
};

function ActionButton({ onAction }: ActionButtonProps) {
  return <button onClick={onAction}>실행</button>;
}

function App() {
  function handleAction() {
    console.log('버튼을 눌렀습니다.');
  }

  return <ActionButton onAction={handleAction} />;
}

onClick={handleAction}クリックすると実行する関数を渡します。onClick={handleAction()}同様に括弧を付けるとレンダリング中に関数を直接呼び出すので注意します。 Reactイベントの種類と伝播、基本動作のキャンセルは第4章「イベント処理」で詳しく説明します。

3.2.4 children

JSXから<Card>同様に、コンポーネント名だけを包んだ部分をオープンタグ, </Card>のように名前の前に/を付けて閉じる部分を閉じるタグと言います。 2つのタグの間に置いた内容は特別なpropです。childrenとしてコンポーネントに渡されます。カードのように外観は同じで、中に入る内容だけが変わるUIを作成するときに便利です。

import type { ReactNode } from 'react';

type CardProps = {
  //ReactNodeは、Reactがレンダリングできる内容を表すタイプです。
  children: ReactNode;
};

//children propで受け取った内容をカード内に表示します。
function Card({ children }: CardProps) {
  return <section className="card">{children}</section>;
}

function App() {
  // <Card>と</Card>間のJSXは子供に渡されます。
  return (
    <Card>
      <h2>공지사항</h2>
      <p>새로운 소식이 있습니다.</p>
    </Card>
  );
}

この例では<Card>が開くタグです。</Card>が閉じるタグ。タイトルと段落を2つのタグの間に作成したので、childrenに渡されます。Cardコンポーネントは{children}カード<section>中に入れて画面に表示します。そのため、カードの外観は再利用しながら、中に入れる内容は使用する場所ごとに異なるように構成できます。

3.3 state 基本 useState- 展開/折りたたみ

state は、コンポーネントがレンダリングの間に記憶する値です。画面に影響を与える値をstateで管理すると、その値を更新したときにReactは新しい値で画面を再計算します。関数コンポーネントではuseStateHookでstateと更新関数を作成します。

import { useState } from 'react';

function Counter() {
  //0 は、最初のレンダリングで使用する初期状態です。
  const [count, setCount] = useState(0);

  function handleClick() {
    //setterで次のstate値を要求します。
    setCount(count + 1);
  }

  return <button onClick={handleClick}>클릭한 횟수: {count}</button>;
}

useState(0)銀[현재 state, state setter]型の値を返します。配列構造分解による最初の値はcount,2番目の値はsetCountに入れます。0は、最初のレンダリング時に使用する初期値です。ボタンを押すとsetCount(count + 1)この次の値を要求し、Reactが更新されましたcountでコンポーネントを再レンダリングします。 state変数は関数ではなく現在のレンダリングの値を含み、直接代入せずにセッターで更新します。 setterの名前は通常set後ろにstateの名前を付けます(例:countとsetCount).

stateはその値を使用する最も近いコンポーネントによって管理されます。複数の子が同じ状態を共有する必要がある場合は、共通の親に状態を上げ、子が変更を要求すると、親は関数をpropsに渡すことができます。このパターンは3.2.3 関数propsとイベント処理で扱った方法です。

3.3.1 関数型更新

state を置き換えるとき、setter に新しい値を直接渡すか、前の state を受け取り次の値を返す関数を渡すことができます。古い値に基づいて新しい値を計算するときは、関数型更新を使用します。

function Counter() {
  const [count, setCount] = useState(0);

  function increaseFromCurrentRender() {
    //3回とも、現在のレンダリングの同じカウントに基づいて計算します。
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  }

  function increaseWithUpdater() {
    //各関数は、前の更新の結果を引き継いで計算します。
    setCount((current) => current + 1);
    setCount((current) => current + 1);
    setCount((current) => current + 1);
  }

  return (
    <>
      <p>현재 값: {count}</p>
      <button onClick={increaseFromCurrentRender}>현재 값으로 세 번 증가</button>
      <button onClick={increaseWithUpdater}>함수형으로 세 번 증가</button>
    </>
  );
}

初期値0各ボタンを1回押すと、次のように動作します。

方法 3 回の呼び出しが計算する値 画面に残る最終値
setCount(count + 1) 1, 1, 1 1
setCount(current => current + 1) 1, 2, 3 3

直接値を渡す方法は、3つの呼び出しがすべて同じレンダリングのcountを使うので1を要求します。関数型更新は、前の更新の結果を次の関数が引き継ぐ1, 2, 3を順に計算します。これらの数字が画面にそれぞれ別々に表示されるわけではなく、ボタンを一度押した後に画面に表示される最終値がそれぞれ1と3です。次の例では、2つのボタンが同じですcountを使用するので、結果を比較するときは各方式を初期値0で別々に実行する必要があります。

同じ方法を使用するボタンの初期値0で2回連続押したときの結果も比較してみます。 2つの方法はそれぞれ別々に実行した結果です。

方法 最初のクリック セカンドクリック ダブルクリック後の最終値
setCount(count + 1) 1, 1, 1を求めて1 2, 2, 2を求めて2 2
setCount(current => current + 1) 1 → 2 → 3 4 → 5 → 6 6

値を特定の数値に置き換えるように、以前のステートが必要ない場合setCount(10)同様に値を渡すこともできます。前の値に依存する計算、あるイベント内の連続更新にはsetCount(current => ...)形が適しています。

3.3.2 Stateの不変性

オブジェクトや配列を state として使用するときは、既存の値を直接変更せず、変更した内容を含む新しいオブジェクトや配列を作成して setter に渡します。これをstateの不変性を守るという。

type Profile = {
  name: string;
  city: string;
};

function ProfileCard() {
  const [profile, setProfile] = useState<Profile>({ name: '민지', city: '서울' });

  function changeCity() {
    //既存のオブジェクトを変更せずに、変更する値を含む新しいオブジェクトを作成します。
    setProfile((currentProfile) => ({ ...currentProfile, city: '부산' }));
  }

  return <button onClick={changeCity}>{profile.name}: {profile.city}</button>;
}

{ ...currentProfile, city: '부산' }は既存のプロパティをコピーしますcityのみ変更した新しいオブジェクトを作成します。逆にprofile.city = '부산'同様に、既存のオブジェクトを直接変更して同じオブジェクトを再渡すと、Reactが変更を正しく検出できない可能性があります。配列も同じ原則で新しい配列を渡します。

setItems((currentItems) => [...currentItems, newItem]);
setItems((currentItems) => currentItems.filter((item) => item.id !== targetId));

最初の例は既存の項目をコピーし、新しい項目を追加し、2番目の例は条件に合った項目だけを含む新しい配列を作成します。オブジェクト内のオブジェクトを置き換える場合でも、変更パスにあるオブジェクトをコピーして新しい値を作成する必要があります。

4. イベント処理- 展開/折りたたみ

4.1 Reactイベントとハンドラ

イベントは、クリック、入力、キーボード操作など、ユーザーの動作がブラウザで発生したことです。 Reactでは、JSX要素にonClick, onChange同様にイベント名を指定し、実行する関数を渡して処理します。イベント名はキャメルケースとして書き込まれ、関数は呼び出さずに渡されます。

イベントprop 発生する状況
onClick 要素をクリックしたとき
onChange 入力値が変更されたとき
onSubmit フォームを送信するとき
onKeyDown キーを押すと
onFocus, onBlur 要素にフォーカスが入ったりずれたりしたとき
function SaveButton() {
  function handleClick() {
    console.log('저장 버튼을 눌렀습니다.');
  }

  //onClickに実行する関数を渡します。
  return <button onClick={handleClick}>저장</button>;
}

onClick={handleClick}クリックすると実行する関数を渡します。onClick={handleClick()}同様に括弧を付けるとレンダリング中にすぐに呼び出されるので注意します。イベントハンドラには発生したイベント情報が伝達され、これを利用して入力値などを読み取ることができます。

4.2 イベントの伝播

イベントが発生した要素の外側の要素にも同じイベントハンドラがある場合、イベントは親要素に渡される可能性があります。これをイベントの伝播と言います。ネストされた要素では、この動作により、子クリックで親のハンドラも実行される可能性があります。

4.2.1 キャプチャとバブルステップ

イベントは外側の要素から内側の要素に降りてくるキャプチャステップを経た後、実際のイベント対象から外に上がるバブルステップを経ます。 ReactのonClickは基本的にバブル段階で実行され、onClickCaptureはキャプチャフェーズで実行されます。

function ClickOrder() {
  return (
    <div
      onClickCapture={() => console.log('부모 캡처')}
      onClick={() => console.log('부모 버블')}
    >
      <button
        onClickCapture={() => console.log('자식 캡처')}
        onClick={() => console.log('자식 버블')}
      >
        클릭
      </button>
    </div>
  );
}

ボタンをクリックすると부모 캡처 → 자식 캡처 → 자식 버블 → 부모 버블順番に実行されます。onClickCaptureは、キャプチャフェーズで最初に処理する必要があるロジックがある場合に使用します。

4.2.2 stopPropagationに伝播をやめる

イベントが親ハンドラまで送出されないようにするには、ハンドラからevent.stopPropagation()を呼び出します。

function NestedClick() {
  return (
    <div onClick={() => console.log('부모 클릭 핸들러')}>
      <button
        onClick={(event) => {
          //クリックイベントが親要素に伝播しないようにします。
          event.stopPropagation();
          console.log('자식 클릭 핸들러');
        }}
      >
        부모 동작 없이 클릭
      </button>
    </div>
  );
}

ボタンを押しても子ハンドラのみが実行され、親ハンドラは実行されません。stopPropagation()はイベント配信を防ぐだけで、リンク移動などブラウザ基本動作までキャンセルしません。

4.3 preventDefaultでデフォルトの動作をキャンセル

リンクの移動やフォームの送信など、ブラウザがデフォルトで実行する動作を防ぐにはevent.preventDefault()を使用します。

function DetailLink() {
  return (
    <a
      href="/detail"
      onClick={(event) => {
        //リンクのデフォルトの移動動作をキャンセルします。
        event.preventDefault();
        console.log('이동 대신 다른 처리를 합니다.');
      }}
    >
      상세 보기
    </a>
  );
}

このリンクをクリックしても/detailに移動せずにハンドラのみが実行されます。preventDefault()はブラウザのデフォルト動作を防ぎ、stopPropagation()は、イベントが親に伝播するのを防ぎます。どちらも目的が異なり、必要に応じて一緒に使用できます。

5. フォーム- 展開/折りたたみ

フォームは、ユーザーが入力した値を収集して検証して送信するUIです。 Reactでは、入力値をstateで管理する制御コンポーネントとDOMに入力値を代入する非制御コンポーネントを使用できます。どの方法を選択するかは、入力値を画面ですぐに活用する必要があるか、フォームライブラリを書くかなどによって異なります。

5.1 フォーム入力値の管理- 展開/折りたたみ

5.1.1 targetとcurrentは何が違うのか?

event.targetはイベントが実際に始まった要素であり、event.currentTargetは現在ハンドラが接続されている要素です。入力要素にハンドラを直接接続すると、両方とも通常その入力要素を指します。フォームでは、ハンドラが接続されている入力要素を明確に指すcurrentTargetを書くとタイプと動作がわかりやすいです。

区分 制御コンポーネント 非制御コンポーネント
入力値を保存 React state DOM入力要素
入力値を読む event.currentTarget.valueなど 提出時にrefcurrent.value
デフォルト valueまたはchecked defaultValueまたはdefaultChecked
入力中に画面を反映 state が変わると再レンダリング 入力自体はstate更新を引き起こさない

5.1.2 制御コンポーネント

制御コンポーネントは入力値をReact stateに関連付けます。入力値が変わるたびにstateを更新するので、入力値に応じて画面を即座に切り替えたり検証したりするのに適しています。

import { useState } from 'react';

function ControlledName() {
  const [name, setName] = useState('');

  return (
    <label>
      이름
      {/*valueはstateによって制御され、onChangeは新しい入力をstateに反映します。*/}
      <input
        value={name}
        onChange={(event) => setName(event.currentTarget.value)}
      />
      <p>입력 중: {name}</p>
    </label>
  );
}

value={name}は、入力要素に現在の状態を表示し、onChangeユーザーが入力した値setNameとしてstateに保存します。したがって、stateと入力ウィンドウの値が一緒に更新されます。

5.1.3 非制御コンポーネント

非制御コンポーネントは入力値をDOMによって保持します。入力するたびにReact stateを更新せず、値が必要なときにrefを介して読み込みます。

import { useRef } from 'react';
import type { FormEvent } from 'react';

function UncontrolledName() {
  //refは実際<input>要素を指し、最初はまだ接続されていない null です。
  const nameRef = useRef<HTMLInputElement>(null);

  //提出イベントが発生した要因は、<form>です。
  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    //ブラウザがフォームを送信し、ページを再度開くデフォルトの動作を防ぎます。
    event.preventDefault();
    console.log(nameRef.current?.value ?? '');
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        이름
        {/*defaultValueは初期入力値であり、refの型はHTMLInputElementです。*/}
        <input ref={nameRef} defaultValue="민지" />
      </label>
      <button type="submit">확인</button>
    </form>
  );
}

defaultValueは最初に表示する値で、ユーザーが変更した後はDOMの入力値が基準になります。チェックボックスにdefaultChecked使用できます。 Reactがstateを介して入力値を制御しないことを意味し、コンポーネントが他の理由でレンダリングされるまでブロックするわけではありません。

5.2 入力要素によるフォームの実装例- 展開/折りたたみ

5.2.1 入力要素をstateとして管理する

テキスト入力、複数行入力、選択ボックス、チェックボックスは、コントロールコンポーネントで作成したときにそれぞれvalueまたはcheckedとonChangeを接続します。チェックボックスの値はvalueではなくcheckedから読みます。

import { useState } from 'react';
import type { FormEvent } from 'react';

type ContactFormState = {
  name: string;
  message: string;
  topic: string;
  agreed: boolean;
};

function ContactForm() {
  const [form, setForm] = useState<ContactFormState>({
    name: '',
    message: '',
    topic: 'question',
    agreed: false,
  });

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    //送信後にページが更新されるブラウザのデフォルトの動作を防ぎます。
    event.preventDefault();
    console.log(form);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        이름
        <input
          value={form.name}
          onChange={(event) =>
            setForm((current) => ({ ...current, name: event.currentTarget.value }))
          }
        />
      </label>

      <label>
        문의 내용
        <textarea
          value={form.message}
          onChange={(event) =>
            setForm((current) => ({ ...current, message: event.currentTarget.value }))
          }
        />
      </label>

      <label>
        문의 종류
        <select
          value={form.topic}
          onChange={(event) =>
            setForm((current) => ({ ...current, topic: event.currentTarget.value }))
          }
        >
          <option value="question">질문</option>
          <option value="feedback">의견</option>
        </select>
      </label>

      <label>
        <input
          type="checkbox"
          checked={form.agreed}
          onChange={(event) =>
            setForm((current) => ({ ...current, agreed: event.currentTarget.checked }))
          }
        />
        개인정보 수집에 동의합니다.
      </label>

      <button type="submit">보내기</button>
    </form>
  );
}

各入力要素はstateの対応するフィールドvalueまたはcheckedに接続し、変更時に新しいstateオブジェクトを作成して更新します。フォーム提出時handleSubmitは現在の状態を使用できます。実際のサービスでは、提出前に必須入力と形式を検証し、サーバー要求の成功・失敗もユーザーに知らせる必要があります。

5.2.2 ネストされたオブジェクトの状態の更新

フォームステート内にオブジェクトや配列が含まれていても、既存の値を直接変更しないで新しいオブジェクト・配列を作成します。たとえば、入れ子になったオブジェクトの1つのフィールドを置き換えるときは、変更するパスの各オブジェクトをコピーします。

setForm((current) => ({
  ...current,
  address: {
    ...current.address,
    city: '부산',
  },
}));

5.2.3 配列の状態の更新

Reactは、stateの変更を確認するときに古い値と新しい値の参照も比較します。配列を直接変更すると同じ配列オブジェクトが保持されるため、変更を検出できず、画面が更新されないことがあります。したがって、配列項目を追加・修正・削除するときは、元の配列を変更するpush()代わりに、新しい配列を作成してstateに渡します。以下の例はitems, setItems、各項目のid, newItem, updatedItem, targetIdがすでに定義されているとします。

//追加:既存のアイテムと新しいアイテムを含む配列を作成します。
setItems((current) => [...current, newItem]);

//修正:ターゲットアイテムのみを新しいオブジェクトに置き換えます。
setItems((current) =>
  current.map((item) => (item.id === updatedItem.id ? updatedItem : item)),
);

//削除:残す項目だけを含む新しい配列を作成します。
setItems((current) => current.filter((item) => item.id !== targetId));

5.2.4 Immerでネストした状態を更新する

ネストされたオブジェクトを複数ステップコピーするコードが複雑になったら、Immerを使用できます。use-immerパッケージのuseImmerは draft を直接修正するように作成しても変更された新しい state を作成します。

import { useImmer } from 'use-immer';

type Profile = {
  name: string;
  address: { city: string };
};

function ProfileEditor() {
  const [profile, updateProfile] = useImmer<Profile>({
    name: '민지',
    address: { city: '서울' },
  });

  function changeCity() {
    //draft は、Immer が updater に提供する一時的な作業用オブジェクトです。
    //ここで修正した内容は、Immerが新しい不変状態として反映します。
    updateProfile((draft) => {
      draft.address.city = '부산';
    });
  }

  return <button onClick={changeCity}>{profile.address.city}</button>;
}

Immerのdraftは通常のstateオブジェクトではないので、この方法はuseImmerの updater 内でのみ使用します。基本useStateのオブジェクトや配列を直接変更しないでください。

5.3 React Hook Form- 展開/折りたたみ

5.3.1 インストール

React Hook Formは、入力値の登録、フォームの送信、検証を支援するライブラリです。入力の多いフォームでは、各フィールドの状態と変更ハンドラを繰り返し作成することを減らすことができます。

Viteプロジェクトで使用するには、パッケージをインストールします。

npm install react-hook-form

5.3.2 基本的な使い方

import { useForm } from 'react-hook-form';

type SignupValues = {
  email: string;
  password: string;
};

function SignupForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignupValues>({
    defaultValues: { email: '', password: '' },
  });

  function onSubmit(values: SignupValues) {
    console.log(values);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>
        이메일
        <input
          {...register('email', { required: '이메일을 입력해 주세요.' })}
          type="email"
        />
      </label>
      {errors.email && <p role="alert">{errors.email.message}</p>}

      <label>
        비밀번호
        <input
          {...register('password', {
            required: '비밀번호를 입력해 주세요.',
            minLength: { value: 8, message: '8자 이상 입력해 주세요.' },
          })}
          type="password"
        />
      </label>
      {errors.password && <p role="alert">{errors.password.message}</p>}

      <button type="submit">가입</button>
    </form>
  );
}

register()は各入力をReact Hook Formに登録し、handleSubmit()は提出時に検証され、成功した値onSubmitに渡します。このプロセスでは、フォームのメインページの移動も処理するため、例では別々にpreventDefault()を呼び出しません。errorsには検証失敗情報が含まれています。パスワード長の検証はユーザーの便宜のためであり、実際のセキュリティ検証はサーバーでも実行する必要があります。

6. Hookの活用- 展開/折りたたみ

6.1 useState: state と setter

useStateはレンダリングの間に保持する値を作成し、[현재 값, 갱신 함수]を返します。詳細な関数型更新と不変性3.3節を参照してください。

const [count, setCount] = useState(0);

Hook はコンポーネントやカスタム Hook の最上位から呼び出し、条件文や繰り返し文の中では呼び出しません。

6.2 useEffect: 外部システムとの同期

useEffectは、タイマー、イベントのサブスクリプション、ネットワーク接続など、React以外のシステムとコンポーネントを同期するために使用します。

useEffect(() => {
  //外部システムと同期するタスク
}, [의존성]);

最初の引数は実行する関数であり、2番目の引数はその関数が使用するpropsやstateなどのレスポンシブ値の配列(依存性倍)です。 依存配列を省略すると、レンダリングがコミットされるたびに実行され、 []これにより、最初の画面に表示された後に実行され、通常の再レンダリングには再実行されません。 [value]同様に値を入れると、最初に実行された後、その値が変わるたびに再実行されます。

Effectが返す関数はクリーンアップ関数です。クリーンアップとは、Effect が開始したタスクを終了または解放することです。たとえば、タイマーを起動した場合は、クリーンアップ関数でタイマーを停止します。

useEffect(() => {
  const timer = setInterval(() => setCount((current) => current + 1), 1000);
  return () => clearInterval(timer);
}, []);

ReactはEffectを再実行する前に前のクリーンアップ関数を呼び出し、コンポーネントが画面から削除されたときにも呼び出します。開発環境Strict Modeでは、クリーンアップロジックを確認するために、初期設定とクリーンアップをさらに実行できます。

import { useEffect, useState } from 'react';

function PageTitle({ title }: { title: string }) {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `${title}: ${count}`;
  }, [title, count]);

  return <button onClick={() => setCount((current) => current + 1)}>{count}</button>;
}

documentはブラウザが提供するDOM APIで、Reactコンポーネントの外側のシステムです。この例では、ブラウザタブのタイトル(document.title)をReactのtitlepropとcountstateに合わせて同期します。 2つの値を依存配列に入れたので、最初に実行された後に2つのいずれかが変わると、Effectが再実行されます。

6.3 useRef: レンダリングと分離された値参照

useRefはレンダリングの間に保持されます{ current }オブジェクトを返します。 DOM要素を参照したり、タイマーIDのように画面に表示されない値を保持したい場合に使用します。currentを置き換えても再レンダリングされないため、画面に表示する値はstateで管理します。 input refの例は5.1.3節を参照してください。

6.4 useContext: 深いコンポーネントに値を渡す

Contextは、いくつかのステップの下で必要な値をpropに引き続き渡すのを助けます。useContextは最も近い親Providerの値を読み取り、その値が変わるとそれを使用するコンポーネントが再レンダリングされます。

import { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function Toolbar() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>도구 모음</div>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

Toolbarは親Providerによって提供されています"dark"値を読む<div className="dark">をレンダリングします。 Contextは値を渡すだけで、クラスにスタイルを適用しません。暗い背景と明るい文字を表示するには、CSSルールを定義する必要があります。

.dark {
  background-color: #222;
  color: white;
}

Contextは、テーマやログインユーザーのように、複数の深さで共有する値に書き込み、すべての状態を無条件に移動するわけではありません。

6.5 useReducer: 複雑な状態の更新

useReducerは、関連する複数の状態変更規則をreducer関数の1つに集めます。dispatch(action)で更新します。状態遷移が複雑な場合や、複数のイベントが同じ状態を変更する場合に便利です。useStateより簡単です。

6.6 パフォーマンス最適化フック

useMemoは計算結果を、useCallbackは、関数を依存関係の値が変わるまで再利用します。パフォーマンスの問題が特定された計算や最適化された子コンポーネントに渡される値など、必要な場合にのみ使用します。不要なメモは、コードを複雑にする可能性があり、React Compilerを使用するプロジェクトでは手動の使用を減らすことができます。

6.7 useId: アクセシビリティIDの生成

useId入力要素と説明htmlFor, id, aria-describedbyで接続する一意のIDを作成するときに使用します。リストのkeyを作るためのフックではありません。

6.8優先順位のあるUIの更新

useTransitionとuseDeferredValueは、検索結果や大きなリストのように重い画面の更新を遅らせて入力など、緊急なUIが最初に反応するのに役立ちます。ネットワーク要求を一定時間遅延させるデバウンスとは異なる機能です。

6.9 React 19のアクションフック

React 19以降ではuseActionStateで Action の結果と進捗状況を管理し、useOptimisticで、リクエストが完了する前に楽観的なUIを表示できます。プロジェクトのReactバージョンを確認し、その機能が必要な場合に使用します。

7. 組み込みコンポーネント- 展開/折りたたみ

7.1 Fragment

Fragment は、複数の JSX 要素を実際の DOM に不要な親要素を追加することなく囲みます。短い表記<>...</>であり、keyが必要な場合Fragmentを明示的にインポートして使用します。

import { Fragment } from 'react';

function CourseDetails() {
  return (
    <>
      <h2>React 기초</h2>
      <p>컴포넌트와 JSX를 배웁니다.</p>
    </>
  );
}

function CourseRows({ courses }: { courses: { id: string; title: string }[] }) {
  return courses.map((course) => (
    <Fragment key={course.id}>
      <h3>{course.title}</h3>
      <hr />
    </Fragment>
  ));
}

短縮表記ではkeyを指定できません。リストのキーは2.3.2.1節を参照してください。

7.2 StrictMode

StrictModeは、開発中のコンポーネントの潜在的な問題を見つけるために追加のチェックを実行するReactコンポーネントです。本番ビルドの動作や画面には影響しません。開発環境では、純粋でないレンダリングを見つけるためにコンポーネントをさらにレンダリングし、Effectとrefクリーンアップの欠落を見つけるために関連処理を再実行できます。

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

開発中にログやEffectが2回実行されるように見えても、チェックをオフにするよりもレンダリングを純粋に保ち、Effectのクリーンアップ関数を正しく作成したことを確認します。 Strict Modeが本番環境でEffectを2回実行するわけではありません。

7.3 Profiler

Profilerは、包んだコンポーネントツリーのレンダリング時間を測定するReactコンポーネントです。パフォーマンスの問題が疑われる領域を測定するために使用され、測定なしでパフォーマンスの最適化から適用されません。

import { Profiler } from 'react';

function CoursePage() {
  return (
    <Profiler
      id="CourseList"
      onRender={(id, phase, actualDuration) => {
        console.log(`${id} ${phase}: ${actualDuration}ms`);
      }}
    >
      <CourseList />
    </Profiler>
  );
}

onRenderは、包んだツリーのレンダリングがコミットされると呼び出されます。actualDurationは、その更新でツリーをレンダリングするのにかかった時間です。測定にはオーバーヘッドがあり、基本プロダクションビルドではプロファイリングが無効になっているため、開発中にReact Developer ToolsのProfilerも利用できます。

7.4 Suspense

Suspenseは、子コンポーネントがロードのためにレンダリングをしばらく停止する間fallbackUI を示す境界です。読み込みUIを表示する範囲を囲み、準備ができたら子画面を表示します。

import { Suspense } from 'react';

function App() {
  return (
    <Suspense fallback={<p role="status">화면을 불러오는 중입니다...</p>}>
      <LazyCoursePage />
    </Suspense>
  );
}

Suspenseはすべての非同期操作を自動的に検出しません。lazyで呼び出すコンポーネントや Suspense をサポートするフレームワーク・データローディング方式で動作します。useEffect中fetch()は Suspense を自動的に有効にせず、このコースではそれを直接ステータスとして扱います7.7節)。ロードフォールバックとエラー画面も別々であり、エラー処理はError Boundaryなどで構成します。

7.5 コンポーネント遅延のロード

lazy()は、コンポーネントコードを最初にレンダリングするときに呼び出すように指示します。最初の画面にすぐに必要とされない大きな画面や機能のコードを分割して呼び出すときに便利です。遅延ロード中は最も近いSuspense境界のフォールバックが表示されます。

import { lazy, Suspense } from 'react';

const SettingsPage = lazy(() => import('./SettingsPage'));

function App() {
  return (
    <Suspense fallback={<p role="status">설정 화면을 불러오는 중입니다...</p>}>
      <SettingsPage />
    </Suspense>
  );
}

lazy()宣言はコンポーネントの外側に置きます。動的import()が返すモジュールはデフォルトのエクスポート(default export)としてReactコンポーネントを提供する必要があります。遅延ロードしたモジュールを読み込めなかったエラーは Suspense が処理しないため、Error Boundary などでエラー UI を別途用意します。

7.6 ネットワーク経由でデータをインポートする

ネットワーク要求は非同期操作です。ユーザークリックのように、特定のアクションに対する要求はイベントハンドラから開始され、画面に必要なデータを外部システムと同期させる要求は、Effectまたはフレームワークのデータロード機能で処理できます。 propsとstateで計算できる値を取得するためにEffectを追加しません。

fetch()はネットワークエラーには拒否されますが、404または500応答だけでは拒否されません。response.okを確認する必要があります。

async function fetchCourses() {
  const response = await fetch('/api/courses');
  if (!response.ok) {
    throw new Error(`강좌 요청 실패 (${response.status})`);
  }

  return response.json();
}

実際の応答データはTypeScript型宣言だけでは検証されないため、実行時にも応答の外観を確認する必要があります。リクエストキャンセル、ロード・エラー・空き結果表示まで含めた例は、7.7節で扱います。アプリの規模が大きくなると、キャッシュ、冗長要求、および再検証を提供するフレームワークまたはデータ要求ライブラリが考慮されます。

7.7 ロード・エラー・空の結果処理

ネットワークからデータを取得する画面は、要求中、要求失敗、結果なし、データ表示状態を区別する必要があります。ロード中は進行中であることを知らせ、エラーは空の結果と区別メッセージを表示します。必要に応じて再試行機能も提供します。以下の例は/api/courses秋{ id, title }フォームのコース配列を返すとします。

import { useEffect, useState } from 'react';

type Course = { id: string; title: string };
type LoadState =
  | { status: 'loading' }
  | { status: 'error'; message: string }
  | { status: 'success'; courses: Course[] };

function isCourseList(data: unknown): data is Course[] {
  return (
    Array.isArray(data) &&
    data.every(
      (item) =>
        item !== null &&
        typeof item === 'object' &&
        'id' in item &&
        typeof item.id === 'string' &&
        'title' in item &&
        typeof item.title === 'string',
    )
  );
}

function CourseList() {
  const [state, setState] = useState<LoadState>({ status: 'loading' });

  useEffect(() => {
    const controller = new AbortController();

    async function loadCourses() {
      try {
        const response = await fetch('/api/courses', { signal: controller.signal });
        if (!response.ok) {
          throw new Error(`강좌를 불러오지 못했습니다. (${response.status})`);
        }

        const data: unknown = await response.json();
        if (!isCourseList(data)) {
          throw new Error('서버 응답 형식이 올바르지 않습니다.');
        }

        setState({ status: 'success', courses: data });
      } catch (error: unknown) {
        if (controller.signal.aborted) return;
        setState({
          status: 'error',
          message: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
        });
      }
    }

    void loadCourses();
    return () => controller.abort();
  }, []);

  if (state.status === 'loading') return <p role="status">강좌를 불러오는 중입니다...</p>;
  if (state.status === 'error') return <p role="alert">{state.message}</p>;
  if (state.courses.length === 0) return <p>등록된 강좌가 없습니다.</p>;

  return (
    <ul>
      {state.courses.map((course) => (
        <li key={course.id}>{course.title}</li>
      ))}
    </ul>
  );
}

LoadStateを判別ユニオンとして定義し、各画面状態に必要な値だけを持つようにしました。fetch()はネットワーク障害には拒否されますが、404・500などのHTTP応答だ​​けでは拒否されないためresponse.okを確認してください。応答JSONはunknownとして受け取り、外観を調べて、間違ったデータが通常の結果のように使用されないようにします。

Effectのクリーンアップ関数はAbortControllerでリクエストをキャンセルします。コンポーネントが消えた後は、結果を状態に反映せず、要求の失敗とキャンセルを区別します。実際のサービスでは、再試行、キャッシュ、冗長要求処理などが必要な場合があるため、データ要求ライブラリやフレームワーク機能を考慮します。この例のように、Effect 内から直接取得する要求は、それ自体で Suspense の読み込み画面を表示せず、ここでは状態に応じて UI を直接レンダリングします。

8. コンポーネント開発におけるスタイル- 展開/折りたたみ

コンポーネントスタイルは、別々のCSSファイル、CSSモジュール、CSS-in-JSライブラリなどで作成できます。 CSS-in-JSは、JavaScript / TypeScriptコードでスタイルを定義し、コンポーネントと共に管理する方法です。ライブラリごとにフレームワークのサポート、サーバーレンダリング設定、バンドルサイズと使用文法が異なるため、プロジェクト環境に合わせて選択します。

8.1 Styled JSX

Styled JSXは主にNext.jsで使用されるCSS-in-JSツールです。コンポーネント内<style jsx>にCSSを作成すると、そのコンポーネントの範囲に適用されます。一般的なVite Reactプロジェクトにはデフォルトが含まれていないため、Next.js以外の環境では個別の設定と依存関係が必要です。

function Greeting() {
  return (
    <section>
      <h2>안녕하세요</h2>
      <style jsx>{`
        h2 {
          color: navy;
        }
      `}</style>
    </section>
  );
}

コンポーネントスタイルの範囲を分離するのは簡単ですが、グローバルスタイルには別のグローバルスタイルの文法が必要です。 Next.js App Routerでサーバーレンダリングで使用する場合は、スタイルレジストリ設定が必要な場合があるため、フレームワークの公式ドキュメントを確認してください。

8.2 styled-components

styled-componentsは、タグ付きテンプレートリテラルでCSSを作成し、スタイル付きのReactコンポーネントを作成するライブラリです。

npm install styled-components
import styled from 'styled-components';

const Button = styled.button`
  padding: 0.5rem 1rem;
  border: 0;
  border-radius: 0.25rem;
  background: #2457a7;
  color: white;

  &:hover {
    background: #173d78;
  }
`;

function SaveButton() {
  return <Button>저장</Button>;
}

Buttonは実際<button>をレンダリングしながらリンクされたスタイルを適用します。 propsやThemeProviderを使用して、ステータスやテーマに応じてスタイルを変更することもできます。サーバーレンダリングを使用するフレームワークでは、スタイルの抽出と注入の設定が必要な場合があるため、そのフレームワークのインストール手順に従います。

8.3 Emotion

EmotionはCSSをJavaScriptで書くのを助けるCSS-in-JSライブラリです。 Reactでは@emotion/styledで styled-components に似たコンポーネントを作成でき、@emotion/reactのcsspropで要素にスタイルを付けることもできます。ここでは設定が簡単です@emotion/styled方法を使用します。

npm install @emotion/react @emotion/styled
import styled from '@emotion/styled';

const Message = styled.p`
  margin: 0;
  color: #444;
  font-weight: 600;
`;

function Status() {
  return <Message>저장되었습니다.</Message>;
}

Emotionは文字列テンプレートとオブジェクトスタイル、csspropなど、いくつかの作成方法を提供します。csspropはJSX変換設定が必要な場合があるため、プロジェクトのビルドツールとEmotionドキュメントを確認してください。 Next.js App Routerのように、サーバーコンポーネントのストリーミングを使用する環境では、ライブラリ固有のサポート状態と設定を最初に確認する必要があります。

3つの方法はすべてコンポーネント単位でスタイルを設定できますが、文法とフレームワークの統合方法は異なります。あるプロジェクトでは、特別な理由がなければ、スタイルツールを複数混在させるよりも、従来の方法と一貫性を維持します。

9.リアクトルーター- 展開/折りたたみ

9.1 SPA

SPA(Single-Page Application)は、最初に受け取ったHTML文書を保持しながら、JavaScriptが画面の必要な部分を更新する方法です。 React RouterはURLとReactコンポーネントを連結し、新しいHTML文書を毎回要求せずに画面を切り替え、ブラウザの後方・前方移動とアドレス共有をサポートします。サーバーは、アプリのパスに直接アクセスしてもアプリエントリ文書を返すように設定する必要があります。

9.2 ルータなしのコンポーネントの配置

小さな画面では、状態に応じてコンポーネントを条件付きでレンダリングできます。

import { useState } from 'react';

function App() {
  const [page, setPage] = useState<'home' | 'courses'>('home');

  return (
    <>
      <button onClick={() => setPage('home')}>홈</button>
      <button onClick={() => setPage('courses')}>강좌</button>
      {page === 'home' ? <h1>홈</h1> : <h1>강좌 목록</h1>}
    </>
  );
}

この方法では、ページのステータスがURLに反映されないため、アドレス共有やブラウザに戻ることを直接実装する必要があります。画面とパスを接続する必要があるアプリはルーターを使用します。

9.3 ルータを使用した配置

この例では、React Router v7の宣言型APIを使用しています。パッケージをインストールしてアプリのトップにBrowserRouterを置いた後、Routes内にURLと画面をリンクするRouteを宣言します。

npm install react-router
// src/main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import App from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    {/*アプリ全体でURLベースルーティングを使用できるように包みます。*/}
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);
// src/App.tsx
import { Route, Routes } from 'react-router';
import { Navigation } from './Navigation';

function App() {
  //現在のURLに合ったルートを選択してください。
  return (
    <>
      {/*共通のナビゲーションは、ルートによって変わるルートの外に置かれます。*/}
      <Navigation />
      <Routes>
        {/*ホームパスにホーム画面を接続します。*/}
        <Route path="/" element={<Home />} />
        {/*コースリストパスにリスト画面をリンクします。*/}
        <Route path="/courses" element={<CourseList />} />
        {/*:courseIdはURLから読み取るパスパラメータです。*/}
        <Route path="/courses/:courseId" element={<CourseDetail />} />
        {/*上記のパスと一致しない場合に表示するデフォルトのパスです。*/}
        <Route path="*" element={<p>페이지를 찾을 수 없습니다.</p>} />
      </Routes>
    </>
  );
}

BrowserRouterは、ブラウザのアドレスとルーティングステータスを関連付けます。Routesは現在のURLに合ったパスを見つけ、一致しますRouteのelementをレンダリングします。*パスは、一致するパスがない場合に表示される画面です。インストール方法とインポートパスはReact Routerのバージョンによって異なりますので、プロジェクトバージョンの公式文書を確認してください。

stateで画面を直接変える方法は簡単ですが、URLと繋がらなければ画面アドレスの共有やブラウザの後ろ・前への移動を別途実装する必要があります。各画面を一意のURLで開き、ブラウザの履歴と連携するにはRouteとしてURLと画面をリンクします。

9.4 パスパラメータ

パスの:courseId同様に、コロンで始まる部分はパラメータです。useParams()で値を読み取って、そのデータの詳細画面を表示できます。

import { useParams } from 'react-router';

const courses = [
  { id: 'react-basics', title: 'React 기초' },
  { id: 'react-hooks', title: 'React Hooks' },
];

function CourseDetail() {
  const { courseId } = useParams();
  const course = courses.find((item) => item.id === courseId);

  if (!course) return <p>강좌를 찾을 수 없습니다.</p>;
  return <h1>{course.title}</h1>;
}

ルートパターン/courses/:courseIdから:courseIdは実際の値が入る桁を示します。実際のURL/courses/react-basicsにはコロンがなく、useParams()はcourseId値として"react-basics"を返します。パスパラメータは文字列なので、数値IDは変換と検証の後に使用されます。

Linkはアプリ内でパスを移動するリンクです。クリックすると URL を置き換え、ドキュメント全体を再度要求しないようにルーターに移動します。現在のパスに適したアクティブスタイルを自動的に提供するわけではありません。アクティブ状態が必要なナビゲーションにはNavLinkを使用します。

import { Link } from 'react-router';

function Home() {
  return <Link to="/courses">강좌 목록 보기</Link>;
}

アプリ外のサイトやダウンロードファイルなど、通常のブラウザ移動には<a>を使用します。

NavLinkはLink同じように移動しながら、現在のURLとパスが一致することを知らせるリンクです。アクティブな状態に合わせてスタイルを変更しやすく、一致すればアクセシビリティのためにaria-current="page"も設定されます。クリックするとNavLinkそれ自体がページコンポーネントをレンダリングするわけではありません。 URLが変わるとRoutesが一致するRouteのelementを表示します。

// src/Navigation.tsx
import { NavLink } from 'react-router';

export function Navigation() {
  return (
    <nav>
      <NavLink to="/" end className={({ isActive }) => (isActive ? 'active' : '')}>
        홈
      </NavLink>
      <NavLink to="/courses" className={({ isActive }) => (isActive ? 'active' : '')}>
        강좌
      </NavLink>
    </nav>
  );
}

isActive秋true時activeクラスが適用されます。ホームリンクendは/パスのみが正確にアクティブになり、他のパスでもホームリンクがアクティブになるのを防ぎます。 クラスが画面上の色や外観を変更するには、CSSルールも定義する必要があります。

公式文書:React Router 宣言型ルーティング.

10.ポータル- 展開/折りたたみ

10.1 PortalでDOMの位置を分離する

Portalは、Reactコンポーネントツリーの子をDOM内の他の場所にレンダリングします。モーダルやツールチップのような親要素のoverflow積み重ねの文脈に隠れて表示するときに便利です。 Portalでレンダリングしても、Reactツリーの子であることは維持され、Contextを使用でき、イベントはDOMの位置ではなくReactツリーに沿って伝播されます。

import { createPortal } from 'react-dom';

function Modal({ onClose }: { onClose: () => void }) {
  return createPortal(
    <div className="modal-backdrop" onClick={onClose}>
      <section
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        onClick={(event) => event.stopPropagation()}
      >
        <h2 id="modal-title">알림</h2>
        <p>Portal로 document.body에 렌더링한 대화상자입니다.</p>
        <button onClick={onClose}>닫기</button>
      </section>
    </div>,
    document.body,
  );
}

createPortal(children, domNode)の2番目の引数として実際のDOMノード(document.bodyなど)を渡します。背景をタップすると閉じ、ダイアログボックスの内部クリックは電波をブロックしました。実際のモーダルには、キーボードフォーカス管理、Escape処理などのアクセシビリティ動作も実装する必要があり、Portal自体はそれを自動的に提供しません。

10.2 Error Boundaryによるレンダリングエラーの処理

Error Boundaryは、サブコンポーネントのレンダリング中に発生したエラーをキャッチし、アプリ全体がクラッシュするのではなく、代替UIを表示する境界です。現在のReactでは、Error Boundaryはクラスコンポーネントとして実装されています。getDerivedStateFromErrorで fallback 状態を決めてcomponentDidCatchでエラーを記録できます。

import { Component, type ErrorInfo, type PropsWithChildren } from 'react';

type ErrorBoundaryState = { hasError: boolean };

class ErrorBoundary extends Component<PropsWithChildren, ErrorBoundaryState> {
  state: ErrorBoundaryState = { hasError: false };

  static getDerivedStateFromError(): ErrorBoundaryState {
    return { hasError: true };
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    console.error('화면 렌더링 오류:', error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <p role="alert">화면을 표시하는 중 문제가 발생했습니다.</p>;
    }

    return this.props.children;
  }
}

エラーが隔離されたいサブ領域<ErrorBoundary>で包みます。境界を複数の場所に置くと、1画面のエラーがアプリ全体を置き換えないように範囲を分割できます。

<ErrorBoundary>
  <CoursePage />
</ErrorBoundary>

Error Boundaryは子コンポーネントのレンダリングエラーを処理しますが、イベントハンドラで発生したエラー、通常の非同期操作のエラー、境界自体が発生したエラーは自動的にキャッチされません。このエラーはtry/catchまたは要求状態処理など、発生した位置に合った方法で処理します。

注:createPortal 公式ドキュメント, Component 公式文書.

11. リエクトライブラリの活用- 展開/折りたたみ

Reactエコシステムには、UIをすばやく設定したり、コンポーネントを独立して開発したり、サーバーデータを管理したりするためのライブラリがあります。必要な機能とプロジェクトのReact・ビルドツール環境を確認して選択します。

11.1 MUI MaterialによるUIの設定

MUI Materialは、ボタン、入力、メニュー、ダイアログなどの再利用可能なUIコンポーネントとテーマシステムを提供するReactライブラリです。プロジェクトで一貫したデザインの画面をすばやく作成するときに活用できます。 MUIのデフォルトのスタイルエンジンはEmotionです。

npm install @mui/material @emotion/react @emotion/styled
import Button from '@mui/material/Button';

function SaveButton() {
  return (
    <Button variant="contained" color="primary" onClick={() => console.log('저장')}>
      저장
    </Button>
  );
}

variantはボタンの形を、colorはテーマの色の役割を選択します。 MUIコンポーネントはアクセシビリティ属性と対話を提供しますが、画面に適したラベルと使用フローはアプリで設定する必要があります。 ReactとReact DOMはMUIのピア依存性なので、プロジェクトにインストールする必要があります。

公式文書:MUI Materialのインストール, Button.

11.2 Storybookでのコンポーネントの開発

Storybookは、コンポーネントをアプリの残りの画面と分離して実行し、複数の入力値に応じたUIの状態を確認・文書化する開発ツールです。 ReactとViteを使用するプロジェクトでは、プロジェクトルートで初期化コマンドを実行します。

npx storybook@latest init

初期化が完了すると、Storybookはプロジェクトに追加したスクリプトとして実行します。デフォルト設定ではnpm run storybookとして開発サーバーを起動し、npm run build-storybookとして静的結果を構築します。

Storyファイルは、コンポーネントの基本情報(meta)と個々の状態(story)をエクスポートします。次の例はButtonコンポーネントlabelとdisabledpropsを受け取るとします。

// src/components/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';

const meta = {
  title: 'Components/Button',
  component: Button,
  args: { label: '저장' },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = {
  args: { disabled: false },
};

export const Disabled: Story = {
  args: { disabled: true },
};

各ストーリーのargsはコンポーネントpropsに渡されます。 Storybook画面でコンポーネントの状態を独立して確認し、Controlsが設定されている場合はpropsを切り替えて動作を確認できます。 Storyファイルは通常、実際のアプリ画面の一部ではなく、開発・文書化ツールで使用します。

公式文書:Storybookを始めよう, ストーリーを書く.

11.3 TanStack Queryでサーバーの状態を管理する

TanStack Queryは、以前の名前がReact Queryのサーバー状態管理ライブラリです。サーバーから取得したデータの要求、キャッシュ、同期、および更新を管理します。入力ウィンドウの値や開いているかどうかなどのクライアントUIステータスを管理するuseStateと目的が異なります。

npm install @tanstack/react-query

アプリQueryClientProviderで囲むと、サブコンポーネントでquery機能を使用できます。

// src/main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createRoot } from 'react-dom/client';
import App from './App';

const queryClient = new QueryClient();

createRoot(document.getElementById('root')!).render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
);

useQueryはqueryKeyでデータを識別し、queryFnにインポートします。以下の例では、リクエスト・エラー・空き結果・成功画面を区別します。

import { useQuery } from '@tanstack/react-query';

type Course = { id: string; title: string };

async function fetchCourses(): Promise<Course[]> {
  const response = await fetch('/api/courses');
  if (!response.ok) {
    throw new Error(`강좌 요청 실패 (${response.status})`);
  }
  return response.json();
}

function CourseList() {
  const { data, isPending, isError, error } = useQuery({
    queryKey: ['courses'],
    queryFn: fetchCourses,
  });

  if (isPending) return <p role="status">강좌를 불러오는 중입니다...</p>;
  if (isError) return <p role="alert">{error.message}</p>;
  if (data.length === 0) return <p>등록된 강좌가 없습니다.</p>;

  return (
    <ul>
      {data.map((course) => (
        <li key={course.id}>{course.title}</li>
      ))}
    </ul>
  );
}

同じqueryKeyを使用する要求の結果は、QueryClientがキャッシュして再利用できます。実際のAPIレスポンスのデータ形状はTypeScript宣言だけでは検証されないため、ランタイム検証も考慮します。ロード・エラー・空き結果処理と応答検証の詳細は7.7節を参照してください。

公式文書:TanStack Queryの概要, クイックスタート.

12. テスト- 展開/折りたたみ

テストは、コードが要件どおりに動作することを確認する方法です。ユニットロジックから実際のブラウザのユーザーフローまで、目的に合ったレベルでチェックします。

12.1 テストの種類

  • ユニットテスト:小さな関数またはモジュールが期待した結果をもたらすかどうかを確認します。
  • 統合テスト:コンポーネントとステータス、イベントなど、さまざまな部分が一緒に動作することを確認します。
  • E2Eテスト:実際のブラウザでユーザーのコアフローが機能していることを確認してください。

Reactコンポーネントテストは画面とユーザーの動作を確認するため、統合テストの性質も持つことができます。テストレベルの境界は絶対的ではありません。

12.2 テストツールの選択

  • Jest または Vitest:テストを実行して結果を検証するフレームワーク。既存のプロジェクトツールを維持するか、ViteプロジェクトでVitestを選択できます。
  • React Testing Library:コンポーネントを画面にレンダリングしてユーザーの視点で確認するのに役立つツールです。
  • Playwright:実際のブラウザでユーザーフローを確認するE2Eテストツールです。サイプレスも代替として使用できます。
  • Mock Service Worker (MSW):ネットワーク応答を模擬してAPIの成功・エラー状況をテストするのに役立ちます。

公式文書:Jest, Vitest, React Testing Library, Playwright, MSW.

12.3 実務でテストを選ぶ

ユニット・統合テストは複数の動作を素早く確認し、E2Eテストは重要なユーザーフローをブラウザで検証します。すべての動作をE2Eでチェックするよりも、重要度とリスクに応じて適切なレベルを選択してください。

  • 内部実装よりも要件とユーザーが確認する結果をテストします。
  • 重要なテストをCIで実行し、変更による回帰を確認します。
  • カバレッジは参考指標と見なされ、重要な動作を正しく検証するかどうかがわかります。