初級13分で読める

Reactコンポーネントの基本:JSXと関数コンポーネントを理解する

Reactの基本単位であるコンポーネントの作り方を解説します。JSXの書き方から関数コンポーネントの定義、propsの受け渡し、childrenの使い方まで実例付きで学べます。

Reactを使うことで何が変わるのか、まずそこから話したい。

HTMLで同じようなボタンを10個作るとき、普通はこうなる。

<button class="btn btn-primary" id="btn-1">送信</button>
<button class="btn btn-primary" id="btn-2">キャンセル</button>
<button class="btn btn-primary" id="btn-3">削除</button>

文字だけが違って、あとはほぼコピーだ。10個が100個になったとき、ボタンのスタイルを変えたければ100箇所を直す。

Reactのコンポーネントは「繰り返し使える部品」だ。ボタンを一度定義すれば、何度でも使える。変更も一箇所で済む。

function Button({ label }: { label: string }) {
  return <button className="btn btn-primary">{label}</button>
}

// 使うとき
<Button label="送信" />
<Button label="キャンセル" />
<Button label="削除" />

これがコンポーネントの根本的な価値だ。

関数コンポーネントの基本形

Reactのコンポーネントは、JSXを返す関数として書く。

function Greeting() {
  return <h1>こんにちは、React!</h1>
}

export { Greeting }

命名ルール: コンポーネント名は必ず大文字始まり(PascalCase)で書く。小文字で始めると、ReactはそれをHTMLタグだと解釈してしまう。

// ✅ PascalCase:Reactコンポーネントとして扱われる
function MyButton() { return <button>クリック</button> }

// ❌ 小文字始まり:HTMLの <mybutton> タグとして解釈される(存在しないタグ)
function myButton() { return <button>クリック</button> }

JSXの書き方:HTMLに似ているが違う

returnの中に書かれているHTMLのような記法が JSX だ。見た目はHTMLに近いが、実際にはJavaScriptに変換されるため、いくつかルールが違う。

ルール1:複数の要素を並べて返せない

JSXは1つのルート要素しか返せない。複数の要素をまとめるときは <>...</>(フラグメント)を使う。

// ❌ 複数のルート要素はエラー
function UserInfo() {
  return (
    <h2>山田 太郎</h2>
    <p>フロントエンドエンジニア</p>
  )
}

// ✅ フラグメントでまとめる(DOMには余分な要素が追加されない)
function UserInfo() {
  return (
    <>
      <h2>山田 太郎</h2>
      <p>フロントエンドエンジニア</p>
    </>
  )
}

ルール2:classはclassNameと書く

class はJavaScriptの予約語なので、JSXではCSSクラスを className と書く。

// ❌ class はJavaScriptと衝突する
<button class="btn btn-primary">クリック</button>

// ✅ className を使う
<button className="btn btn-primary">クリック</button>

同様に for 属性は htmlFor になる(<label htmlFor="email">)。

ルール3:JavaScriptの式は {} で埋め込む

変数や計算結果をJSXに埋め込むときは {} で囲む。

function UserCard() {
  const name = '山田太郎'
  const age = 28

  return (
    <div>
      <p>{name}({age}歳)</p>
      <p>成人: {age >= 18 ? 'はい' : 'いいえ'}</p>
    </div>
  )
}

{} の中に書けるのは (値を返すもの)だけだ。if文for文 は式ではないので書けない。

// ❌ if文は式ではない
return <div>{if (isAdmin) { '管理者' }}</div>

// ✅ 三項演算子(式)なら書ける
return <div>{isAdmin ? '管理者' : '一般ユーザー'}</div>

// ✅ または && で「trueなら表示する」
return <div>{isAdmin && <AdminBadge />}</div>

ルール4:自己閉じタグは / で閉じる

HTMLでは <br><img> は閉じタグ不要だが、JSXでは必ず閉じる。

// ❌ JSXでは閉じないとエラー
<br>
<img src="/logo.png" alt="ロゴ">

// ✅ 自己閉じタグを使う
<br />
<img src="/logo.png" alt="ロゴ" />

propsでデータを渡す

同じ構造で違う内容を表示したいとき、props を使ってデータを外から渡せる。

type GreetingProps = {
  name: string
  role?: string  // ? で省略可能にする
}

function Greeting({ name, role = 'ユーザー' }: GreetingProps) {
  return (
    <div>
      <h2>こんにちは、{name}さん!</h2>
      <p>ロール: {role}</p>
    </div>
  )
}

使うときは属性のように書く。

function App() {
  return (
    <>
      <Greeting name="田中" role="管理者" />
      <Greeting name="鈴木" />  {/* role は省略できる */}
    </>
  )
}

TypeScriptで型を定義すると、型に合わない値を渡したときにコンパイルエラーで気づける。

<Greeting name={123} />  // ❌ number は string に代入できない
<Greeting />             // ❌ 必須の name が指定されていない

「実行して画面が壊れる前に気づける」というのがTypeScriptの価値だ。

childrenでコンテンツを内側に渡す

HTMLの <div>テキスト</div> のように、コンポーネントのタグの間に要素を書きたいとき、children を使う。

import type { ReactNode } from 'react'

type CardProps = {
  title: string
  children: ReactNode  // タグの間に書いたものが渡ってくる
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h3 className="card-title">{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  )
}
function App() {
  return (
    <Card title="お知らせ">
      <p>新機能がリリースされました。</p>
      <a href="/news">詳細を見る</a>
    </Card>
  )
}

ReactNode はテキスト・JSX要素・配列・nullなど何でも受け取れる型だ。レイアウトやラッパーコンポーネントでよく使うパターンになる。

コンポーネントを組み合わせる

小さなコンポーネントを組み合わせて、複雑なUIを作るのがReactの使い方だ。

// 小さな部品
function Tag({ label }: { label: string }) {
  return <span className="tag">{label}</span>
}

// 部品を使う中くらいの部品
function ArticleCard({ title, excerpt, publishedAt, tags }: ArticleCardProps) {
  return (
    <article className="card">
      <h3>{title}</h3>
      <p>{excerpt}</p>
      <div>
        {tags.map(tag => <Tag key={tag} label={tag} />)}
      </div>
      <time dateTime={publishedAt}>{publishedAt}</time>
    </article>
  )
}

// さらに組み合わせて全体を作る
function ArticleList() {
  return (
    <section>
      {articles.map(article => (
        <ArticleCard key={article.title} {...article} />
      ))}
    </section>
  )
}

ツリー構造になっていくイメージだ。

App
├── Header
│   └── NavLink × 3
├── ArticleList
│   └── ArticleCard × n
│       └── Tag × n
└── Footer

各コンポーネントが一つの役割を持つので、修正やテストが一箇所で済むようになる。

コンポーネントの定義とexport

コンポーネントは1ファイルに1つを基本として、named export で書くのが一般的だ。

// article-card.tsx
export function ArticleCard({ title }: { title: string }) {
  return <h3>{title}</h3>
}

// 使う側
import { ArticleCard } from './article-card'

default exportはimport時に好きな名前が付けられてしまい、チームでコードを読むときに混乱しやすい。named exportなら名前が統一される。

よくある初心者のミス3つ

コンポーネント内でコンポーネントを定義する

// ❌ 毎レンダリングで新しいコンポーネントが作られてしまう
function Parent() {
  function Child() {  // ← これはNG。Parentが再描画されるたびに新しいChildが作られる
    return <p>子コンポーネント</p>
  }
  return <Child />
}

// ✅ トップレベルに定義する
function Child() {
  return <p>子コンポーネント</p>
}

function Parent() {
  return <Child />
}

リストでkeyを忘れる

// ❌ key がないと警告が出る
{items.map(item => <li>{item}</li>)}

// ✅ 一意のkeyを付ける
{items.map(item => <li key={item.id}>{item.name}</li>)}

propsを直接変更する

// ❌ propsは読み取り専用。変更してもReactに伝わらない
function UserCard({ user }: { user: User }) {
  user.name = '変更'  // NG。Reactは気づかない
  return <div>{user.name}</div>
}

// ✅ stateに入れてから変更する
function UserCard({ user }: { user: User }) {
  const [name, setName] = useState(user.name)
  return (
    <input value={name} onChange={e => setName(e.target.value)} />
  )
}

コンポーネントの考え方

Reactコンポーネントは「入力(props)を受け取って、表示(JSX)を返す関数」だ。同じpropsが渡れば同じ表示が返る、という単純なルールで動いている。

最初は「なぜHTMLを関数で書くんだ」と感じるかもしれない。でも部品化と再利用という考え方に慣れると、状態管理やデータフローの理解も自然とついてくる。次のステップとして useState を学ぶと、コンポーネントにインタラクティブな動作を加えられるようになる。