2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

⚛️ フロントエンド基礎講座【第四回】React × TypeScriptの基礎!コンポーネント・Props・Stateを型安全に学ぼう

2
Posted at

1. 👋 はじめに

前回は生TypeScriptでのDOM操作を学びました。そして最後にこんな「辛さ」を体感しましたね。

データが変わるたびに「全消し→全再描画」という力技…
「変更された部分だけ自動で更新してほしい!」

その願いを叶えるのが React です🚀

この記事を読めば:

  • ✅ なぜReactが必要かがわかる
  • ✅ TSXコンポーネントの書き方がわかる
  • ✅ Props・Stateに型をつける方法がわかる
  • useState の基本が使えるようになる

2. 🤔 なぜReactが必要なのか?

DOM操作の問題点をおさらい

前回のToDoリストを思い出してください。

❌ TypeScriptでは:

データ(配列)          画面(HTML)
    │                      │
    │  変更があるたびに     │
    │──────────────────→   │ 全消し
    │                      │ ↓
    │                      │ 全再描画
    │                      │
  手動で同期し続ける必要がある 😩
✅ Reactの世界:

データ(State)         画面(JSX)
    │                      │
    │  データが変わると     │
    │──────────────────→   │ 自動で差分だけ更新!
    │                      │
  Reactが自動で同期してくれる 🎉

Reactの3つの特徴

特徴 説明
🧩 コンポーネント UIを部品(コンポーネント)に分割して再利用できる
🔄 自動更新 データ(State)が変わると、関係する部分だけ自動で画面を更新
🌳 仮想DOM 実際のDOMを操作する前に差分を計算して最小限の更新に抑える

3. 🛠️ 開発環境を準備しよう

Viteで新しいプロジェクトを作成

# React + TypeScript のプロジェクトを作成
npm create vite@latest my-app -- --template react-ts

# プロジェクトフォルダに移動
cd my-app

# 依存パッケージをインストール
npm install

# 開発サーバーを起動
npm run dev

生成されるファイル構造

my-app/
├── src/
│   ├── App.tsx          ← メインのコンポーネント
│   ├── main.tsx         ← エントリーポイント
│   └── index.css        ← グローバルCSS
├── index.html
├── package.json
├── tsconfig.json        ← TypeScriptの設定
└── vite.config.ts       ← Viteの設定

4. 🧩 コンポーネントとは?

一言で言うと

コンポーネントとは「UIの部品」です。
ボタン・カード・ナビゲーションバーなど、
画面を小さな部品に分割して組み合わせます。

コンポーネントのイメージ

【コンポーネントに分割する前】           【コンポーネントに分割した後】

<div>                                    <App>
  <header>...</header>                           <Header />
  <main>                                   <Main>
    <div class="card">...</div>              <Card />
    <div class="card">...</div>              <Card />
    <div class="card">...</div>              <Card />
  </main>                                  </Main>
  <footer>...</footer>                     <Footer />
</div>                                    </App>

→ 同じカードを3回書く                   → Card を3回使いまわす!

最初のTSXコンポーネント

// src/components/Greeting.tsx
// Greetingを返すコンポーネント

const Greeting = () => {
  return (
    <div>
      <h1>こんにちは!</h1>
      <p>Reactへようこそ。</p>
    </div>
  );
};

export default Greeting;
// src/App.tsx で使う
// Greetingをインポート
import Greeting from "./components/Greeting";

const App = () => {
  return (
    <div>
      <Greeting />  {/* コンポーネントを使う */}
    </div>
  );
};

export default App;

💡 FC について

Reactのコードで FC(FunctionComponent)という型を見かけることがあります。
これはReactが提供する「関数コンポーネントであること」を示す型です。

// FC を使う書き方(従来スタイル)
import { FC } from "react";
const Greeting: FC = () => <h1>こんにちは</h1>;

ただし React 18以降は FC なしでシンプルに書くスタイルが主流 になっています。
このシリーズでも FC を使わない現代スタイルで統一します。

Reactのルール

💡 JSX と TSX について
ReactでJavaScriptを使う場合は JSX.jsx)、
TypeScriptを使う場合は TSX.tsx)と呼びます。
このシリーズではTypeScript前提なのでTSXを使いますが、
ルール自体はJSX・TSX共通です。

// ✅ ルール① 必ず1つの要素を返す
const Good = () => {
  return (
    <div>          {/* 1つのルート要素 */}
      <h1>タイトル</h1>
      <p>本文</p>
    </div>
  );
};

// ✅ フラグメント(<>)で余分なdivを避ける
const WithFragment = () => {
  return (
    <>              {/* divを使わずに複数要素を返せる */}
      <h1>タイトル</h1>
      <p>本文</p>
    </>
  );
};

// ✅ ルール② 自己閉じタグが必要
const SelfClose = () => {
  return (
    <div>
      <img src="photo.jpg" alt="写真" />  {/* / が必要! */}
      <br />
    </div>
  );
};

// ✅ ルール③ class ではなく className を使う
const WithClass = () => {
  return <div className="card">カード</div>;
  //          ↑ HTMLのclassではなくclassName!
};

5. 📨 Props:コンポーネントに値を渡す

Propsとは?

Props(プロップス)とは「コンポーネントに渡す引数」です。
親コンポーネントから子コンポーネントへデータを渡すときに使います。

Propsに型をつける

// src/components/UserCard.tsx

// Propsの型を type で定義する(現代スタイル)
type UserCardProps = {
  name: string;
  email: string;
  age: number;
  isAdmin?: boolean;  // ? をつけると省略可能(オプショナル)
};

// 型を引数に指定する
const UserCard = ({ name, email, age, isAdmin = false }: UserCardProps) => {
  return (
    <div className="card">
      <h2>{name}</h2>
      <p>メール:{email}</p>
      <p>年齢:{age}</p>
      {isAdmin && <span className="badge">管理者</span>}
    </div>
  );
};

export default UserCard;
// src/App.tsx で使う
import UserCard from "./components/UserCard";

const App = () => {
  return (
    <div>
      {/* Propsを渡す */}
      <UserCard name="田中 太郎" email="taro@example.com" age={25} isAdmin={true} />
      <UserCard name="鈴木 花子" email="hanako@example.com" age={30} />
      {/* isAdmin を省略 → デフォルトの false が使われる */}
    </div>
  );
};

よく使うPropsの型パターン

type ExampleProps = {
  // 基本的な型
  title: string;
  count: number;
  isVisible: boolean;

  // 配列
  items: string[];
  users: { id: number; name: string }[];

  // 関数(イベントハンドラー)
  onClick: () => void;
  onChange: (value: string) => void;

  // 子コンポーネント(children)
  children: React.ReactNode;

  // オプショナル
  description?: string;
};

children Props:コンポーネントの中身を渡す

// src/components/Card.tsx

type CardProps = {
  title: string;
  children: React.ReactNode;  // 中身を受け取る
};

const Card = ({ title, children }: CardProps) => {
  return (
    <div className="card">
      <h2 className="card-title">{title}</h2>
      <div className="card-body">
        {children}  {/* 受け取った中身をここに表示 */}
      </div>
    </div>
  );
};

export default Card;
// 使う側
<Card title="ユーザー情報">
  <p>田中 太郎</p>
  <p>taro@example.com</p>
</Card>

<Card title="お知らせ">
  <ul>
    <li>新機能が追加されました</li>
    <li>メンテナンスのお知らせ</li>
  </ul>
</Card>

6. 🔄 State:コンポーネントの状態管理

Stateとは?

State(ステート)とは「コンポーネントが持つ内部データ」です。
Stateが変わると、Reactが自動でそのコンポーネントを再レンダリングします。

Props:親から受け取る・変更できない(読み取り専用)
State:自分が持つ・自分で変更できる

useState の基本

import { useState } from "react";

const Counter = () => {
  // useState<型>(初期値) → [現在の値, 値を更新する関数]
  const [count, setCount] = useState<number>(0);

  return (
    <div>
      <p>カウント:{count}</p>
      <button onClick={() => setCount(count + 1)}>増やす</button>
      <button onClick={() => setCount(count - 1)}>減らす</button>
      <button onClick={() => setCount(0)}>リセット</button>
    </div>
  );
};
【useState の仕組み】

const [count, setCount] = useState<number>(0);
        ↑         ↑                    ↑
  現在の値    更新する関数           初期値(0)

setCount(5) を呼ぶ
    ↓
count が 5 に変わる
    ↓
Reactがコンポーネントを自動で再レンダリング
    ↓
画面が新しい値(5)で更新される!

💡 TypeScriptの知恵袋:型推論(型の省略)

初期値が 0(数値)・""(文字列)・false(真偽値)のように明確な場合、
TypeScriptが自動で型を推測してくれるため、<number> などは省略してもOKです!

const [count, setCount] = useState(0);    // 自動的に number 型になる
const [name, setName] = useState("");     // 自動的に string 型になる
const [flag, setFlag] = useState(false);  // 自動的に boolean 型になる

ただし 空の配列を初期値にするとき は省略できません。
「何が入る配列か」をTypeScriptが判断できないため、
必ずジェネリクスで型を教えてあげる必要があります。

const [items, setItems] = useState([]);        // ❌ never[] 型になってしまう
const [items, setItems] = useState<Todo[]>([]); // ✅ Todo[] 型として扱われる

よく使う State のパターン

import { useState } from "react";

const Examples = () => {
  // 文字列のState
  const [name, setName] = useState("");

  // 真偽値のState(表示/非表示の切り替えに便利)
  const [isOpen, setIsOpen] = useState(false);

  // 配列のState
  const [items, setItems] = useState<string[]>([]);

  // オブジェクトのState(type で別に定義するとスッキリ)
  type User = {
    name: string;
    age: number;
  };

  const [user, setUser] = useState<User>({
    name: "",
    age: 0,
  });

  return (
    <div>
      {/* 入力フォームとStateを連動させる */}
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="名前を入力"
      />
      <p>入力中:{name}</p>

      {/* 表示/非表示の切り替え */}
      <button onClick={() => setIsOpen(!isOpen)}>
        {isOpen ? "閉じる" : "開く"}
      </button>
      {isOpen && <div className="panel">パネルの中身</div>}
    </div>
  );
};

⚠️ Stateを直接変更してはいけない

// Stateの宣言(初期値は空の配列)
const [items, setItems] = useState<string[]>([]);

// どこかでデータが追加されて items = ["A", "B"] になっているとする

// ❌ 絶対にやってはいけない:直接変更する
items.push("C");  // ❌ 直接変更 → 画面が更新されない!
items[0] = "Z";   // ❌ 直接変更 → 画面が更新されない!

💡 なぜ直接変更してはいけないのか?
ReactはStateが「新しいオブジェクトに変わった」ことで再レンダリングを判断します。
直接変更すると「同じオブジェクト」のままなので、変化に気づけません。
必ず新しい配列・オブジェクトを作って setter に渡しましょう。

✅ 配列Stateの正しい操作方法

① 要素を追加する

// items = ["A", "B"] の状態から追加する例

// 「...items」は「今の配列を全部展開する」という意味
// [...items, "C"] = ["A", "B", "C"] という新しい配列を作る
setItems([...items, "C"]);

// 実行前:["A", "B"]
// 実行後:["A", "B", "C"]

💡 ...(スプレッド構文)とは?
配列の中身をバラバラに展開する記法です。
[...items, "C"] は「今の配列の全要素 + "C"」という新しい配列を作ります。

② 特定の要素を削除する

// items = ["A", "B", "C"] の状態から "A" を削除する例

// filter:条件に「一致しない」要素だけを残した新しい配列を返す
// 「"A" 以外の要素だけを残す」= "A" を削除する
setItems(items.filter((i) => i !== "A"));

// 実行前:["A", "B", "C"]
// 実行後:["B", "C"]("A"だけ消えた)

💡 filter とは?
配列の各要素を1つずつチェックして、
条件が true の要素だけを集めた新しい配列を返します。
i !== "A" は「"A" ではない」という条件なので、"A" 以外が残ります。

③ 特定の要素を更新する

// items = ["A", "B", "C"] の状態から "A" を "Z" に変更する例

// map:配列の各要素を変換した新しい配列を返す
// 「"A" なら "Z" に変換・それ以外はそのまま」
setItems(items.map((i) => (i === "A" ? "Z" : i)));

// 実行前:["A", "B", "C"]
// 実行後:["Z", "B", "C"]("A"だけ"Z"に変わった)

💡 map とは?
配列の各要素を1つずつ変換して、新しい配列を返します。
i === "A" ? "Z" : i は「"A" なら "Z" を返す・それ以外は元の値のまま」という意味です。

3つの操作をまとめて整理

やりたいこと 使うメソッド イメージ
追加 [...items, 新要素] 今の配列をコピーして末尾に追加
削除 items.filter(条件) 条件に合う要素だけを残す
更新 items.map(変換) 各要素を変換して新しい配列を作る

7. 🏗️ 実践:ReactでToDoリストを作る

前回の生DOM操作と比べてみましょう!

// src/App.tsx

import { useState } from "react";

// 型定義
type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

// ✅ コンポーネントの外側で定義する(再レンダリングでリセットされない)
// ❌ コンポーネントの内側に書くと、Stateが変わるたびに let nextId = 1 に
//    リセットされてしまい、2つ目以降のIDがすべて同じになるバグが発生する!
let nextId = 1;

const App = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [inputText, setInputText] = useState("");

  // ToDoを追加する
  const addTodo = (): void => {
    if (!inputText.trim()) return;
    const newTodo: Todo = {
      id: nextId++,
      text: inputText.trim(),
      completed: false,
    };
    setTodos([...todos, newTodo]);
    setInputText("");
  };

  // 完了/未完了を切り替える
  const toggleTodo = (id: number): void => {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  // ToDoを削除する
  const deleteTodo = (id: number): void => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <div className="app">
      <h1>📝 ToDoリスト</h1>

      {/* 入力エリア */}
      <div className="input-area">
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && addTodo()}
          placeholder="やることを入力..."
        />
        <button onClick={addTodo}>追加</button>
      </div>

      {/* ToDoリスト */}
      <ul>
        {todos.map((todo) => (
          <li
            key={todo.id}
            className={todo.completed ? "completed" : ""}
          >
            <span>{todo.text}</span>
            <button onClick={() => toggleTodo(todo.id)}>
              {todo.completed ? "戻す" : "完了"}
            </button>
            <button onClick={() => deleteTodo(todo.id)}>削除</button>
          </li>
        ))}
      </ul>

      {/* 件数表示 */}
      <p>{todos.filter((t) => !t.completed).length}件が未完了</p>
    </div>
  );
};

export default App;

💡 コラム:Reactが速い理由(仮想DOM)

実際のブラウザの画面(リアルDOM)を書き換える処理は非常に重い処理です。
Reactはまずメモリ上の軽い 「仮想DOM」 で差分を計算し、
本当に変わったピンポイントな部分だけをリアルDOMに適用します。

【前回の生DOM操作】           【Reactの仮想DOM】

データ変更                    データ変更
  ↓                            ↓
全要素を削除                  仮想DOM上で差分を計算
  ↓                            ↓
全要素を再生成                変わった部分だけを特定
  ↓                            ↓
リアルDOMに反映(重い!)     その部分だけリアルDOMに反映(速い!)

生DOM操作との比較

生TypeScript(第三回) React(第四回)
データの管理 配列を手動で管理 useState で管理
画面の更新 innerHTML = "" で全消し→再描画 Stateが変わると自動で差分更新
イベント登録 addEventListener を手動で設定 JSX内に onClick を直接書く
コード量 多い・複雑になりやすい 少ない・読みやすい

「全消し→再描画」という力技が、
setTodos([...todos, newTodo]) の1行で済むようになりました! 🎉


8. 🎯 まとめ

概念 一言で言うと
🧩 コンポーネント UIの部品・関数として定義してタグのように使う
📝 TSX TypeScript + JSX・HTMLに似た記法でUIを書く
📨 Props 親から子へデータを渡す・type で型を定義
🔄 State コンポーネントの内部データ・変わると自動で画面が更新
🪝 useState [値, setter] のペアを返すReactのフック

Reactの核心:
「データ(State・Props)が変わると、Reactが自動で画面を更新する」
この仕組みを理解すると、Reactの設計がスムーズに理解できます💪

次回は React × TypeScriptの実践 を解説します!
useEffect でのAPI連携・フォームのバリデーション・
コンポーネントの分割設計を学びます🌟

💬 質問や感想があれば、コメント欄でお気軽にどうぞ!
👍 役に立ったら、いいね&ストックをお願いします!
🎓 ここまで読んでくださって、本当にありがとうございました!


🔗 シリーズ記事

  • 【第一回】Webページの仕組み・HTML基礎
  • 【第二回】CSSで見た目を整える
  • 【第三回】TypeScriptでインタラクションを作る
  • 【第四回】React × TypeScriptの基礎(この記事)
  • 【第五回】React × TypeScriptの実践(近日公開)
  • 【第六回】まとめ・開発環境・次のステップ(近日公開)
2
2
0

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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?