デザインについての学習メモブログ

React入門 #09 – 条件分岐とリスト表示のテクニック

記事内に広告が含まれています。

React入門 #09 – 条件分岐とリスト表示のテクニック

Reactで実用的なアプリケーションを作るには、条件に応じてUIを表示/非表示にしたり、リストからデータを抽出して表示したりする必要があります。

この記事では、条件分岐リスト表示効果的なテクニックを詳しく学んでいきます。

条件分岐の基本

条件分岐には複数のパターンがあります。

場面によって使い分けることで、読みやすく保守しやすいコードが書けます。

ここでは5つのパターンをそれぞれの特徴とともに解説します。

パターン1:三項演算子(最もよく使う)

JSX内で{条件 ? A : B}の形で直接書けるのがポイントです。

returnの中でも使えるため、コンポーネントをシンプルに保てます。

isLoggedInの値によって表示が切り替わる部分に注目してください。

JavaScript版:

JSX
import { useState } from 'react'

function LoginStatus() {
  const [isLoggedIn, setIsLoggedIn] = useState(false)
  
  return (
    <div>
      {isLoggedIn ? (
        <p>ログイン状態です</p>
      ) : (
        <p>ログインしてください</p>
      )}
    </div>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

function LoginStatus(): JSX.Element {
  const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false)
  
  return (
    <div>
      {isLoggedIn ? (
        <p>ログイン状態です</p>
      ) : (
        <p>ログインしてください</p>
      )}
    </div>
  )
}

ネストされた三項演算子(非推奨):

三項演算子の計算結果の中でさらに三項演算子で計算させるイメージです。

JSX
// ❌ 読みにくい(バグに繋がりやすくなる)
{status === 'loading' ? <p>読み込み中...</p> : status === 'error' ? <p>エラー</p> : <p>成功</p>}

パターン2:論理AND演算子(&&)

「条件が真のときだけ表示する」場合は、&&を使うとより簡潔に書けます。

ただし、数値の0falseではなくそのまま表示されてしまう点に注意が必要です。

count > 0 &&のように明示的に真偽値で評価するパターンを確認してください。

JavaScript版:

hasMessageの値を更新する事で「表示」「非表示」の切り替えができる

JSX
import { useState } from 'react'

function Notification() {
  const [hasMessage, setHasMessage] = useState(true)
  
  return (
    <div>
      <h1>ホーム</h1>
      {hasMessage && (
        <div className="notification">
          新しいメッセージがあります
        </div>
      )}
    </div>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

function Notification(): JSX.Element {
  const [hasMessage, setHasMessage] = useState<boolean>(true)
  
  return (
    <div>
      <h1>ホーム</h1>
      {hasMessage && (
        <div className="notification">
          新しいメッセージがあります
        </div>
      )}
    </div>
  )
}

注意:数値の0は表示される

JSX
// ❌ countが0の場合、画面に「0」が表示される
{count && <p>Count: {count}</p>}

// ✅ 数値を明示的に真偽値に変換
{count > 0 && <p>Count: {count}</p>}
{!!count && <p>Count: {count}</p>}

💡 !!(二重否定)とは?

!!は値を強制的に真偽値(true / false)に変換する書き方です。

!を2回重ねることで「否定の否定」、つまり元の値の真偽を保ったままboolean型に変換します。

JavaScript
!!0      // → false(0はfalsy)
!!1      // → true(1はtruthy)
!!""     // → false(空文字はfalsy)
!!"abc"  // → true(文字列はtruthy)

パターン3:if文(コンポーネント外で処理)

returnの前でlet contentに要素を代入し、最後に{content}を返しているのがポイントです。

条件が多い場合はJSXの外で処理することで、return内がすっきりします。

JavaScript版:

JSX
import { useState } from 'react'

function StatusMessage() {
  const [status, setStatus] = useState('loading')
  
  let content
  if (status === 'loading') {
    content = <p>読み込み中...</p>
  } else if (status === 'error') {
    content = <p>エラーが発生しました</p>
  } else if (status === 'success') {
    content = <p>成功しました!</p>
  }
  
  return <div>{content}</div>
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

type Status = 'loading' | 'error' | 'success'

function StatusMessage(): JSX.Element {
  const [status, setStatus] = useState<Status>('loading')
  
  let content: JSX.Element
  if (status === 'loading') {
    content = <p>読み込み中...</p>
  } else if (status === 'error') {
    content = <p>エラーが発生しました</p>
  } else {
    content = <p>成功しました!</p>
  }
  
  return <div>{content}</div>
}

パターン4:オブジェクトマッピング

statusIcons[status]のように、オブジェクトをルックアップテーブルとして使うのがポイントです。

ifswitchが増えてきたらこのパターンに切り替えると可読性が上がります。

TypeScript版ではRecord<Status, string>の型定義にも注目してください。

JavaScript版:

JSX
import { useState } from 'react'

function StatusIcon() {
  const [status, setStatus] = useState('success')
  
  const statusIcons = {
    loading: '⏳',
    error: '❌',
    success: '✅',
    warning: '⚠️'
  }
  
  return <div>{statusIcons[status]}</div>
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

type Status = 'loading' | 'error' | 'success' | 'warning'

function StatusIcon(): JSX.Element {
  const [status, setStatus] = useState<Status>('success')
  
  const statusIcons: Record<Status, string> = {
    loading: '⏳',
    error: '❌',
    success: '✅',
    warning: '⚠️'
  }
  
  return <div>{statusIcons[status]}</div>
}

複雑なマッピング:

単純なマッピングとの違いは、値が文字列ではなくオブジェクトになっている点です。

いくつかの着眼点を順に確認してください。

JavaScript版:

JSX
import { useState } from 'react'

function StatusCard() {
  const [status, setStatus] = useState('loading')
  
  const statusConfig = {
    loading: {
      icon: '⏳',
      message: '読み込み中...',
      color: 'gray'
    },
    error: {
      icon: '❌',
      message: 'エラーが発生しました',
      color: 'red'
    },
    success: {
      icon: '✅',
      message: '成功しました!',
      color: 'green'
    }
  }
  
  const config = statusConfig[status]
  
  return (
    <div style={{ color: config.color }}>
      <span>{config.icon}</span>
      <p>{config.message}</p>
    </div>
  )
}

① 値をオブジェクトにまとめる
iconmessagecolorのように関連する複数のデータをひとつのオブジェクトとして持たせることで、状態ごとにすべての情報を一箇所に集約できます。
新しいステータスを追加するときも、オブジェクトに1エントリ足すだけで済みます。

② 一度変数に受け取る
const config = statusConfig[status]でルックアップ結果を変数に代入しているのがポイントです。
JSX内でstatusConfig[status].iconと毎回書く代わりにconfig.iconと短く書けるため、returnの中がすっきりします。

③ 取得した値をそのままJSXで使う
style={{ color: config.color }}のようにインラインスタイルにも直接使えます。
表示するテキストだけでなくスタイルも一括管理できるのが、複雑なマッピングの強みです。

TypeScript版:

TSX
import { useState, type JSX } from 'react'

type Status = 'loading' | 'error' | 'success'

interface StatusConfig {
  icon: string
  message: string
  color: string
}

function StatusCard(): JSX.Element {
  const [status, setStatus] = useState<Status>('loading')
  
  const statusConfig: Record<Status, StatusConfig> = {
    loading: {
      icon: '⏳',
      message: '読み込み中...',
      color: 'gray'
    },
    error: {
      icon: '❌',
      message: 'エラーが発生しました',
      color: 'red'
    },
    success: {
      icon: '✅',
      message: '成功しました!',
      color: 'green'
    }
  }
  
  const config = statusConfig[status]
  
  return (
    <div style={{ color: config.color }}>
      <span>{config.icon}</span>
      <p>{config.message}</p>
    </div>
  )
}

Record<Status, StatusConfig> キーがStatus型、値がStatusConfigインターフェースに準拠していることを型で保証しています。

新しいステータスをStatus型に追加したとき、statusConfigへの追記漏れをコンパイル時に検出できます。

パターン5:switch文

処理をコンポーネント内の関数getPermissionLevel()に切り出して、JSXでは{getPermissionLevel()}と呼び出すだけにしているのがポイントです。

JavaScript版:

JSX
import { useState } from 'react'

function Permission() {
  const [userRole, setUserRole] = useState('user')
  
  const getPermissionLevel = () => {
    switch (userRole) {
      case 'admin':
        return 'すべての権限があります'
      case 'moderator':
        return 'コンテンツ削除の権限があります'
      case 'user':
        return '基本的な権限があります'
      default:
        return '権限がありません'
    }
  }
  
  return <p>{getPermissionLevel()}</p>
}

TypeScript版:

TypeScript版では全ケースを網羅することでdefaultを省略できる点も確認してください。

TSX
import { useState, type JSX } from 'react'

type UserRole = 'admin' | 'moderator' | 'user' | 'guest'

function Permission(): JSX.Element {
  const [userRole, setUserRole] = useState<UserRole>('user')
  
  const getPermissionLevel = (): string => {
    switch (userRole) {
      case 'admin':
        return 'すべての権限があります'
      case 'moderator':
        return 'コンテンツ削除の権限があります'
      case 'user':
        return '基本的な権限があります'
      case 'guest':
        return '権限がありません'
    }
  }
  
  return <p>{getPermissionLevel()}</p>
}

リスト表示の基本

Reactでリストを表示するには、JavaScriptのmap()メソッドを使うのが基本です。

シンプルな文字列の配列から、複数のプロパティを持つオブジェクト配列まで、段階的に確認していきましょう。

map()を使った基本的なリスト表示

fruits.map((fruit, index) => <li key={index}>...)が最小構成です。

配列の各要素がfruitに渡され、JSXを返しているフローを追ってみてください。

なおkeyindexを使っている点は、次のセクションで改善します。

JavaScript版:

JSX
function FruitList() {
  const fruits = ['りんご', 'バナナ', 'オレンジ']
  
  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>
      ))}
    </ul>
  )
}

🤷‍♂️ map()の戻り値は配列なのになぜJSX動くの?

map()戻り値は配列です。

つまり{fruits.map(...)}は以下のようなJSX要素の配列をJSXの中に埋め込んでいます。

JavaScript
// map()が返すもの
[
  <li key={0}>りんご</li>,
  <li key={1}>バナナ</li>,
  <li key={2}>オレンジ</li>
]

ReactはJSX要素の配列をそのままレンダリングできる仕様になっています。

配列を受け取ると、先頭から順に要素を展開して描画するため、<ul>の中に<li>が並んだ結果になります。

イメージとしては次のように展開される感じです。

JSX
// こう書いたのと同じ結果になる
<ul>
  <li>りんご</li>
  <li>バナナ</li>
  <li>オレンジ</li>
</ul>

これがReactでmap()を使ったリスト表示が成立する理由です。

TypeScript版:

TSX
import { type JSX } from 'react'

function FruitList(): JSX.Element {
  const fruits: string[] = ['りんご', 'バナナ', 'オレンジ']
  
  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>
      ))}
    </ul>
  )
}

オブジェクトの配列を表示

オブジェクトを要素して格納する配列では、引数にオブジェクトを1つずつ取り出し、user.nameuser.emailのように各プロパティへのアクセスがそのままJSX内で使えることを確認してください。

user.idkeyに使えるのがポイントです。(次のセクションでその理由は説明)

JavaScript版:

JSX
function UserList() {
  const users = [
    { id: 1, name: '山田太郎', email: 'yamada@example.com' },
    { id: 2, name: '佐藤花子', email: 'sato@example.com' },
    { id: 3, name: '鈴木次郎', email: 'suzuki@example.com' }
  ]
  
  return (
    <div className="user-list">
      {users.map(user => (
        <div key={user.id} className="user-card">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  )
}

TypeScript版:

TSX
import {type JSX } from 'react'

interface User {
  id: number
  name: string
  email: string
}

function UserList(): JSX.Element {
  const users: User[] = [
    { id: 1, name: '山田太郎', email: 'yamada@example.com' },
    { id: 2, name: '佐藤花子', email: 'sato@example.com' },
    { id: 3, name: '鈴木次郎', email: 'suzuki@example.com' }
  ]
  
  return (
    <div className="user-list">
      {users.map(user => (
        <div key={user.id} className="user-card">
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  )
}

keyの重要性

keyはReactがリストの各要素を正確に識別するために必要なものです。

間違った使い方をすると意図しないバグが起きます。

なぜkeyが重要なのかを、具体的な問題例を交えて理解しましょう。

❌ 悪い例:keyがない、またはindexを使用

keyがないとReactが要素を区別できず、パフォーマンス低下やバグの原因になります。

indexをkeyにしている場合も、リストの順番が変わると問題が起きます。

まず「何がいけないのか」をこの例で把握してください。

JSX
// ❌ keyがない
{items.map(item => <li>{item.name}</li>)}

// ❌ indexをkey使用(リスト順序が変わると問題が生じる)
{items.map((item, index) => <li key={index}>{item.name}</li>)}

✅ 良い例:一意のIDをkeyにする

key={todo.id}のように一意のIDを使うのが正解です。

新しいアイテムを追加するときid: Date.now()で一意のIDを生成している点にも注目してください。

JavaScript版:

JSX
import { useState } from 'react'

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '買い物' },
    { id: 2, text: '洗濯' },
    { id: 3, text: '掃除' }
  ])
  
  const addTodo = () => {
    const newTodo = {
      id: Date.now(),  // 一意のID
      text: '新しいTodo'
    }
    setTodos([...todos, newTodo])
  }
  
  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>  {/* idをkeyにする */}
        ))}
      </ul>
      <button onClick={addTodo}>追加</button>
    </div>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

interface Todo {
  id: number
  text: string
}

function TodoList(): JSX.Element {
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: '買い物' },
    { id: 2, text: '洗濯' },
    { id: 3, text: '掃除' }
  ])
  
  const addTodo = (): void => {
    const newTodo: Todo = {
      id: Date.now(),
      text: '新しいTodo'
    }
    setTodos([...todos, newTodo])
  }
  
  return (
    <div>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <button onClick={addTodo}>追加</button>
    </div>
  )
}

keyが重要な理由

input要素とkey={index}を組み合わせたコードがポイントです。

先頭に要素を追加したときにkeyがどうずれるかをコメントで追っていくと、indexをkeyにしてはいけない理由が具体的にわかります。

indexをkeyにした場合の問題:

JSX
function ListExample() {
  const [items, setItems] = useState(['A', 'B', 'C'])
  
  const addToTop = () => {
    setItems(['NEW', ...items])  // 先頭に追加
  }
  
  return (
    <div>
      <button onClick={addToTop}>先頭に追加</button>
      <ul>
        {items.map((item, index) => (
          <li key={index}>
            {item}
            <input type="text" />  {/* inputの値がずれる */}
          </li>
        ))}
      </ul>
    </div>
  )
}

// indexをkeyにすると:
// 先頭に追加前:key 0=A, key 1=B, key 2=C
// 先頭に追加後:key 0=NEW, key 1=A, key 2=B, key 3=C(Cは消える)
// inputの値が意図せずずれてしまう

keyのその要素にとって不変でかつ、他の要素と重複しない番号にしないとReactが操作対象を間違えてしまいます。

私たちで言うところのマイナンバーのような値みたいに個人を識別できる情報にするべきです。

マイナンバーが勝手に他人の番号と差し変わったら怖いですよね?

配列のフィルタリングとソート

filter()で条件に合うデータだけを絞り込み、sort()で並び替えることができます。

この2つを使いこなすだけで、表示のコントロールの幅が大きく広がります。

filter()を使ったフィルタリング

showOutOfStock在庫切れを含む[表示/非表示]設定)の状態によってproductsをそのまま返すfilter()で絞り込むかを三項演算子で切り替えているのがポイントです。

フィルタリング後のfilteredProductsだけをmap()に渡している流れを確認してください。

JavaScript版:

JSX
import { useState } from 'react'

function ProductFilter() {
  const [products] = useState([//変更しないのでsetProductsは省略
    { id: 1, name: 'ノートPC', price: 120000, inStock: true },
    { id: 2, name: 'マウス', price: 3000, inStock: false },
    { id: 3, name: 'キーボード', price: 8000, inStock: true }
  ])
  
  const [showOutOfStock, setShowOutOfStock] = useState(false)
  
  const filteredProducts = showOutOfStock
    ? products
    : products.filter(p => p.inStock)
  
  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={showOutOfStock}
          onChange={(e) => setShowOutOfStock(e.target.checked)}
        />
        在庫切れも表示
      </label>
      
      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price}
            {!product.inStock && <span> (品切れ)</span>}
          </li>
        ))}
      </ul>
    </div>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

interface Product {
  id: number
  name: string
  price: number
  inStock: boolean
}

function ProductFilter(): JSX.Element {
  const [products] = useState<Product[]>([
    { id: 1, name: 'ノートPC', price: 120000, inStock: true },
    { id: 2, name: 'マウス', price: 3000, inStock: false },
    { id: 3, name: 'キーボード', price: 8000, inStock: true }
  ])
  
  const [showOutOfStock, setShowOutOfStock] = useState<boolean>(false)
  
  const filteredProducts = showOutOfStock
    ? products
    : products.filter(p => p.inStock)
  
  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={showOutOfStock}
          onChange={(e) => setShowOutOfStock(e.target.checked)}
        />
        在庫切れも表示
      </label>
      
      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price}
            {!product.inStock && <span> (品切れ)</span>}
          </li>
        ))}
      </ul>
    </div>
  )
}

sort()を使ったソート

[...products].sort(...)スプレッド構文でコピーしてからソートしているのがポイントです。

元の配列を直接sort()すると状態が意図せず書き換わるため、必ずコピーが必要です。

localeCompare()による文字列比較も確認してください。

JavaScript版:

JSX
import { useState } from 'react'

function SortableList() {
  const [products] = useState([
    { id: 1, name: 'ノートPC', price: 120000 },
    { id: 2, name: 'マウス', price: 3000 },
    { id: 3, name: 'キーボード', price: 8000 }
  ])
  
  const [sortBy, setSortBy] = useState('name')
  
  const sortedProducts = [...products].sort((a, b) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name)
    } else if (sortBy === 'price-asc') {
      return a.price - b.price
    } else {
      return b.price - a.price
    }
  })
  
  return (
    <div>
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
        <option value="name">名前順</option>
        <option value="price-asc">価格が安い順</option>
        <option value="price-desc">価格が高い順</option>
      </select>
      
      <ul>
        {sortedProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price.toLocaleString()}
          </li>
        ))}
      </ul>
    </div>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

interface Product {
  id: number
  name: string
  price: number
}

type SortBy = 'name' | 'price-asc' | 'price-desc'

function SortableList(): JSX.Element {
  const [products] = useState<Product[]>([
    { id: 1, name: 'ノートPC', price: 120000 },
    { id: 2, name: 'マウス', price: 3000 },
    { id: 3, name: 'キーボード', price: 8000 }
  ])
  
  const [sortBy, setSortBy] = useState<SortBy>('name')
  
  const sortedProducts = [...products].sort((a: Product, b: Product) => {
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name)
    } else if (sortBy === 'price-asc') {
      return a.price - b.price
    } else {
      return b.price - a.price
    }
  })
  
  return (
    <div>
      <select 
        value={sortBy} 
        onChange={(e) => setSortBy(e.target.value as SortBy)}
      >
        <option value="name">名前順</option>
        <option value="price-asc">価格が安い順</option>
        <option value="price-desc">価格が高い順</option>
      </select>
      
      <ul>
        {sortedProducts.map(product => (
          <li key={product.id}>
            {product.name} - ¥{product.price.toLocaleString()}
          </li>
        ))}
      </ul>
    </div>
  )
}

検索機能の実装

テキスト入力とfilter()を組み合わせるだけで、シンプルな検索機能が実装できます。

入力値に応じてリアルタイムに結果が絞り込まれる仕組みを作ってみましょう。

JavaScript版:

searchTermの変化(23行目)が再レンダリングを起こし、filter()が自動で再計算される流れに注目してください。

||で複数プロパティをまたいで検索できる点(14行目)と、0件時の分岐をセット(29行目)で確認してください。

JSX
import { useState } from 'react'

function SearchableList() {
  const [users] = useState([
    { id: 1, name: '山田太郎', city: '東京' },
    { id: 2, name: '佐藤花子', city: '大阪' },
    { id: 3, name: '山田次郎', city: '東京' },
    { id: 4, name: '田中三郎', city: '京都' }
  ])
  
  const [searchTerm, setSearchTerm] = useState('')
  
  const filteredUsers = users.filter(user =>
    user.name.includes(searchTerm) ||
    user.city.includes(searchTerm)
  )
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="名前または都市で検索"
      />
      
      <p>検索結果: {filteredUsers.length}</p>
      
      {filteredUsers.length === 0 ? (
        <p>該当する結果がありません</p>
      ) : (
        <ul>
          {filteredUsers.map(user => (
            <li key={user.id}>
              {user.name} - {user.city}
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

interface User {
  id: number
  name: string
  city: string
}

function SearchableList(): JSX.Element {
  const [users] = useState<User[]>([
    { id: 1, name: '山田太郎', city: '東京' },
    { id: 2, name: '佐藤花子', city: '大阪' },
    { id: 3, name: '山田次郎', city: '東京' },
    { id: 4, name: '田中三郎', city: '京都' }
  ])
  
  const [searchTerm, setSearchTerm] = useState<string>('')
  
  const filteredUsers = users.filter(user =>
    user.name.includes(searchTerm) ||
    user.city.includes(searchTerm)
  )
  
  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="名前または都市で検索"
      />
      
      <p>検索結果: {filteredUsers.length}</p>
      
      {filteredUsers.length === 0 ? (
        <p>該当する結果がありません</p>
      ) : (
        <ul>
          {filteredUsers.map(user => (
            <li key={user.id}>
              {user.name} - {user.city}
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}

複数条件のフィルタリング

カテゴリ・価格・在庫状況など、複数の条件を同時にフィルタリングする方法を学びます。

条件を組み合わせるロジックを整理することで、実用的なUIが作れるようになります。

JavaScript版:

filter()内で各条件(17-21行)を個別の変数に分けてから&&でまとめている構造がポイントです。

category === '全て'のときに条件をスキップする書き方と、range inputの値をparseInt()数値変換している点も確認してください。

JSX
import { useState } from 'react'

function AdvancedFilter() {
  const [products] = useState([
    { id: 1, name: 'ノートPC', price: 120000, category: '電子機器', inStock: true },
    { id: 2, name: 'マウス', price: 3000, category: '電子機器', inStock: false },
    { id: 3, name: 'ノート', price: 500, category: '文房具', inStock: true },
    { id: 4, name: 'ペン', price: 200, category: '文房具', inStock: true },
    { id: 5, name: 'キーボード', price: 8000, category: '電子機器', inStock: true }
  ])
  
  const [category, setCategory] = useState('全て')
  const [maxPrice, setMaxPrice] = useState(150000)
  const [onlyInStock, setOnlyInStock] = useState(false)
  
  const filteredProducts = products.filter(product => {
    const categoryMatch = category === '全て' || product.category === category
    const priceMatch = product.price <= maxPrice
    const stockMatch = !onlyInStock || product.inStock
    
    return categoryMatch && priceMatch && stockMatch
  })
  
  return (
    <div>
      <div className="filters">
        <div>
          <label>カテゴリ:</label>
          <select value={category} onChange={(e) => setCategory(e.target.value)}>
            <option>全て</option>
            <option>電子機器</option>
            <option>文房具</option>
          </select>
        </div>
        
        <div>
          <label>最大価格: ¥{maxPrice.toLocaleString()}</label>
          <input
            type="range"
            min="0"
            max="150000"
            value={maxPrice}
            onChange={(e) => setMaxPrice(parseInt(e.target.value))}
          />
        </div>
        
        <div>
          <label>
            <input
              type="checkbox"
              checked={onlyInStock}
              onChange={(e) => setOnlyInStock(e.target.checked)}
            />
            在庫ありのみ
          </label>
        </div>
      </div>
      
      <p>検索結果: {filteredProducts.length}</p>
      
      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>
            <div>
              <h3>{product.name}</h3>
              <p>¥{product.price.toLocaleString()}</p>
              <p>{product.category}</p>
              <p>{product.inStock ? '在庫あり' : '在庫切れ'}</p>
            </div>
          </li>
        ))}
      </ul>
    </div>
  )
}

ネストされたリスト表示

カテゴリとその中の商品一覧のように、階層構造を持つデータを表示するには、map()をネストして使います。

構造を崩さずにレンダリングするポイントを確認しましょう。

JavaScript版:

外側のmap()でカテゴリを、内側のmap()でアイテムを展開しています。

keyは外側と内側それぞれの要素に対して独立して設定が必要な点を確認してください。

JSX
import { useState } from 'react'

function NestedList() {
  const [categories] = useState([
    {
      id: 1,
      name: '電子機器',
      items: [
        { id: 1, name: 'ノートPC' },
        { id: 2, name: 'マウス' },
        { id: 3, name: 'キーボード' }
      ]
    },
    {
      id: 2,
      name: '文房具',
      items: [
        { id: 4, name: 'ノート' },
        { id: 5, name: 'ペン' }
      ]
    }
  ])
  
  return (
    <div>
      {categories.map(category => (
        <div key={category.id}>
          <h2>{category.name}</h2>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

interface Item {
  id: number
  name: string
}

interface Category {
  id: number
  name: string
  items: Item[]
}

function NestedList(): JSX.Element {
  const [categories] = useState<Category[]>([
    {
      id: 1,
      name: '電子機器',
      items: [
        { id: 1, name: 'ノートPC' },
        { id: 2, name: 'マウス' },
        { id: 3, name: 'キーボード' }
      ]
    },
    {
      id: 2,
      name: '文房具',
      items: [
        { id: 4, name: 'ノート' },
        { id: 5, name: 'ペン' }
      ]
    }
  ])
  
  return (
    <div>
      {categories.map(category => (
        <div key={category.id}>
          <h2>{category.name}</h2>
          <ul>
            {category.items.map(item => (
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  )
}

空のリストの処理

データが0件のときに何も表示しないのはUX上よくありません。

空の状態を検知して専用のメッセージを出す「エンプティステート」の実装方法を紹介します。

JavaScript版:

tasks.length === 0を三項演算子でチェックし、空のときは専用メッセージを表示しています。

&&を2つ並べる代替パターンとの書き比べも確認してください。

JSX
import { useState } from 'react'

function TaskList() {
  const [tasks, setTasks] = useState([])
  
  return (
    <div>
      {tasks.length === 0 ? (
        <div className="empty-state">
          <p>タスクがありません</p>
          <p>新しいタスクを追加してください</p>
        </div>
      ) : (
        <ul>
          {tasks.map(task => (
            <li key={task.id}>{task.name}</li>
          ))}
        </ul>
      )}
    </div>
  )
}

または論理演算子を使う:

TSX
function TaskList() {
  const [tasks, setTasks] = useState([])
  
  return (
    <div>
      {tasks.length > 0 ? (
        <ul>
          {tasks.map(task => (
            <li key={task.id}>{task.name}</li>
          ))}
        </ul>
      ) : (
        <p>タスクがありません</p>
      )}
    </div>
  )
}

// または && を使う
function TaskList() {
  const [tasks, setTasks] = useState([])
  
  return (
    <div>
      {tasks.length > 0 && (
        <ul>
          {tasks.map(task => (
            <li key={task.id}>{task.name}</li>
          ))}
        </ul>
      )}
      
      {tasks.length === 0 && <p>タスクがありません</p>}
    </div>
  )
}

リスト内での条件分岐

リストの各アイテムに対して、ステータスに応じたバッジやスタイルを出し分けたいケースはよくあります。

リストレンダリングの中で条件分岐を活用する実践的なパターンを学びます。

JavaScript版:

ステータスに応じたバッジ生成をgetStatusBadge()に切り出し、map()の中をシンプルに保っているのがポイントです。

関数がJSX要素をそのまま返せる点も確認してください。

JSX
import { useState } from 'react'

function OrderList() {
  const [orders] = useState([
    { id: 1, product: 'ノートPC', status: 'completed', price: 120000 },
    { id: 2, product: 'マウス', status: 'pending', price: 3000 },
    { id: 3, product: 'キーボード', status: 'cancelled', price: 8000 },
    { id: 4, product: 'モニタ', status: 'completed', price: 45000 }
  ])
  
  const getStatusBadge = (status) => {
    switch (status) {
      case 'completed':
        return <span className="badge-success">✓ 完了</span>
      case 'pending':
        return <span className="badge-warning">⏳ 処理中</span>
      case 'cancelled':
        return <span className="badge-danger">✗ キャンセル</span>
      default:
        return <span className="badge-default">不明</span>
    }
  }
  
  return (
    <ul>
      {orders.map(order => (
        <li key={order.id} className="order-item">
          <div>
            <h3>{order.product}</h3>
            <p>¥{order.price.toLocaleString()}</p>
            {getStatusBadge(order.status)}
          </div>
        </li>
      ))}
    </ul>
  )
}

TypeScript版:

TSX
import { useState, type JSX } from 'react'

type OrderStatus = 'completed' | 'pending' | 'cancelled'

interface Order {
  id: number
  product: string
  status: OrderStatus
  price: number
}

function OrderList(): JSX.Element {
  const [orders] = useState<Order[]>([
    { id: 1, product: 'ノートPC', status: 'completed', price: 120000 },
    { id: 2, product: 'マウス', status: 'pending', price: 3000 },
    { id: 3, product: 'キーボード', status: 'cancelled', price: 8000 },
    { id: 4, product: 'モニタ', status: 'completed', price: 45000 }
  ])
  
  const getStatusBadge = (status: OrderStatus): JSX.Element => {
    switch (status) {
      case 'completed':
        return <span className="badge-success">✓ 完了</span>
      case 'pending':
        return <span className="badge-warning">⏳ 処理中</span>
      case 'cancelled':
        return <span className="badge-danger">✗ キャンセル</span>
    }
  }
  
  return (
    <ul>
      {orders.map(order => (
        <li key={order.id} className="order-item">
          <div>
            <h3>{order.product}</h3>
            <p>¥{order.price.toLocaleString()}</p>
            {getStatusBadge(order.status)}
          </div>
        </li>
      ))}
    </ul>
  )
}

find()、findIndex()、some()、every()の活用

map()filter()以外にも、配列操作に役立つメソッドがあります。

「条件に合う最初の1つを取得」「1つでも条件を満たすか確認」など、用途に合ったメソッドを選べるようになりましょう。

find() – 最初の1つを取得

users.find(user => user.role === 'admin')は条件に合う最初の1件だけを返します。

filter()との違いは戻り値が配列ではなくオブジェクト(またはundefined)な点です。

結果を三項演算子で存在チェックしているパターンに注目してください。

JavaScript版:

JSX
function UserProfile() {
  const [users] = useState([
    { id: 1, name: '山田太郎', role: 'admin' },
    { id: 2, name: '佐藤花子', role: 'user' },
    { id: 3, name: '鈴木次郎', role: 'user' }
  ])
  
  const adminUser = users.find(user => user.role === 'admin')
  
  return (
    <div>
      {adminUser ? (
        <p>管理者: {adminUser.name}</p>
      ) : (
        <p>管理者がいません</p>
      )}
    </div>
  )
}

some() – 1つでも条件を満たすかチェック

orders.some(...)は配列の中に1つでも条件を満たすものがあればtrueを返します。

filter().length > 0より意図が明確でパフォーマンスも良いです。

結果をhasPendingという変数に格納して条件分岐に使っている流れを確認してください。

JSX
function HasPendingOrders() {
  const [orders] = useState([
    { id: 1, status: 'completed' },
    { id: 2, status: 'pending' },
    { id: 3, status: 'completed' }
  ])
  
  const hasPending = orders.some(order => order.status === 'pending')
  
  return (
    <div>
      {hasPending ? (
        <p>⚠️ 処理中の注文があります</p>
      ) : (
        <p>✓ すべての注文が処理されました</p>
      )}
    </div>
  )
}

every() – すべてが条件を満たすかチェック

tasks.every(task => task.completed)全件が条件を満たすときだけtrueになります。

falseのときにfilter(t => !t.completed).lengthで残り件数を出しているチェーンのつなぎ方も参考にしてください。

JSX
function AllCompleted() {
  const [tasks] = useState([
    { id: 1, completed: true },
    { id: 2, completed: true },
    { id: 3, completed: false }
  ])
  
  const allCompleted = tasks.every(task => task.completed)
  
  return (
    <div>
      {allCompleted ? (
        <p>✓ すべてのタスクが完了しました</p>
      ) : (
        <p>⏳ 残りのタスク: {tasks.filter(t => !t.completed).length}</p>
      )}
    </div>
  )
}

よくあるミスと対処法

Reactのリスト表示では、特定のパターンに起因するバグが起きやすいです。

keyの誤用、パフォーマンスの問題、0が画面に表示されてしまう問題など、頻出のミスとその解決策をまとめています。

ミス1:indexをkeyに使う

❌と✅を並べることで違いがひと目でわかります。

key={index}からkey={item.id}への変更は1箇所だけですが、影響は大きいです。

❌ 悪い例:

JSX
{items.map((item, index) => (
  <li key={index}>{item}</li>
))}

✅ 良い例:

JSX
{items.map(item => (
  <li key={item.id}>{item}</li>
))}

ミス2:map()の中でfilter()やsort()を毎回実行

❌の例ではmap()の中でさらにfilter()を呼んでいます。

毎レンダリングごとに全件スキャンが走るため非効率です。

✅のようにfilter().map()とメソッドチェーンで事前に絞り込んでからmap()を1回だけ呼ぶ形を確認してください。

❌ 悪い例(非効率):

JSX
{items.map(item => (
  items.filter(i => i.active).map(filteredItem => (
    <li key={filteredItem.id}>{filteredItem}</li>
  ))
))}

✅ 良い例:

JSX
{items
  .filter(item => item.active)
  .map(item => (
    <li key={item.id}>{item}</li>
  ))
}

ミス3:0やfalseが表示される

{count && ...}と書いたとき、count0だと画面に0が表示されます。

&&の左辺が数値の場合は必ずcount > 0!!countで真偽値に変換するのがポイントです。

❌ 悪い例:

JSX
{count && <p>Count: {count}</p>}  // countが0の場合「0」が表示される

✅ 良い例:

JSX
{count > 0 && <p>Count: {count}</p>}

実践:複雑な商品カタログ

これまで学んだ条件分岐・リスト表示・フィルタリング・ソートをすべて組み合わせた、実践的な商品カタログを作ります。

複数の機能を統合したコンポーネントの構成を体験しましょう。

JavaScript版:

JSX
import { useState } from 'react'

function ProductCatalog() {
  const [products] = useState([
    { id: 1, name: 'ノートPC', category: 'PC', price: 120000, rating: 4.5, inStock: true },
    { id: 2, name: 'マウス', category: 'アクセサリー', price: 3000, rating: 4.0, inStock: false },
    { id: 3, name: 'キーボード', category: 'アクセサリー', price: 8000, rating: 4.2, inStock: true },
    { id: 4, name: 'モニタ', category: '周辺機器', price: 45000, rating: 4.8, inStock: true },
    { id: 5, name: 'USB-C ケーブル', category: 'アクセサリー', price: 1500, rating: 3.9, inStock: true }
  ])
  
  const [searchTerm, setSearchTerm] = useState('')
  const [category, setCategory] = useState('全て')
  const [minRating, setMinRating] = useState(0)
  const [sortBy, setSortBy] = useState('name')
  const [onlyInStock, setOnlyInStock] = useState(false)
  
  const filteredProducts = products
    .filter(product => {
      const searchMatch = product.name.includes(searchTerm)
      const categoryMatch = category === '全て' || product.category === category
      const ratingMatch = product.rating >= minRating
      const stockMatch = !onlyInStock || product.inStock
      
      return searchMatch && categoryMatch && ratingMatch && stockMatch
    })
    .sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name)
      } else if (sortBy === 'price-asc') {
        return a.price - b.price
      } else if (sortBy === 'price-desc') {
        return b.price - a.price
      } else {
        return b.rating - a.rating
      }
    })
  
  const categories = ['全て', 'PC', 'アクセサリー', '周辺機器']
  
  return (
    <div className="product-catalog">
      <h1>商品カタログ</h1>
      
      <div className="filters">
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="商品名で検索"
        />
        
        <select value={category} onChange={(e) => setCategory(e.target.value)}>
          {categories.map(cat => (
            <option key={cat} value={cat}>{cat}</option>
          ))}
        </select>
        
        <div>
          <label>
            最小評価: {minRating.toFixed(1)}
            <input
              type="range"
              min="0"
              max="5"
              step="0.1"
              value={minRating}
              onChange={(e) => setMinRating(parseFloat(e.target.value))}
            />
          </label>
        </div>
        
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="name">名前順</option>
          <option value="price-asc">価格が安い順</option>
          <option value="price-desc">価格が高い順</option>
          <option value="rating">評価が高い順</option>
        </select>
        
        <label>
          <input
            type="checkbox"
            checked={onlyInStock}
            onChange={(e) => setOnlyInStock(e.target.checked)}
          />
          在庫ありのみ
        </label>
      </div>
      
      <p>検索結果: {filteredProducts.length}</p>
      
      {filteredProducts.length === 0 ? (
        <div className="no-results">
          <p>該当する商品がありません</p>
        </div>
      ) : (
        <div className="product-grid">
          {filteredProducts.map(product => (
            <div key={product.id} className="product-card">
              <h3>{product.name}</h3>
              <p className="category">{product.category}</p>
              <p className="price">¥{product.price.toLocaleString()}</p>
              <p className="rating">{product.rating}</p>
              <p className="stock">
                {product.inStock ? (
                  <span className="in-stock">✓ 在庫あり</span>
                ) : (
                  <span className="out-of-stock">✗ 在庫切れ</span>
                )}
              </p>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

export default ProductCatalog

TypeScript版:

TSX
import { useState, type JSX } from 'react'

interface Product {
  id: number
  name: string
  category: string
  price: number
  rating: number
  inStock: boolean
}

type SortBy = 'name' | 'price-asc' | 'price-desc' | 'rating'

function ProductCatalog(): JSX.Element {
  const [products] = useState<Product[]>([
    { id: 1, name: 'ノートPC', category: 'PC', price: 120000, rating: 4.5, inStock: true },
    { id: 2, name: 'マウス', category: 'アクセサリー', price: 3000, rating: 4.0, inStock: false },
    { id: 3, name: 'キーボード', category: 'アクセサリー', price: 8000, rating: 4.2, inStock: true },
    { id: 4, name: 'モニタ', category: '周辺機器', price: 45000, rating: 4.8, inStock: true },
    { id: 5, name: 'USB-C ケーブル', category: 'アクセサリー', price: 1500, rating: 3.9, inStock: true }
  ])
  
  const [searchTerm, setSearchTerm] = useState<string>('')
  const [category, setCategory] = useState<string>('全て')
  const [minRating, setMinRating] = useState<number>(0)
  const [sortBy, setSortBy] = useState<SortBy>('name')
  const [onlyInStock, setOnlyInStock] = useState<boolean>(false)
  
  const filteredProducts = products
    .filter((product: Product) => {
      const searchMatch = product.name.includes(searchTerm)
      const categoryMatch = category === '全て' || product.category === category
      const ratingMatch = product.rating >= minRating
      const stockMatch = !onlyInStock || product.inStock
      
      return searchMatch && categoryMatch && ratingMatch && stockMatch
    })
    .sort((a: Product, b: Product) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name)
      } else if (sortBy === 'price-asc') {
        return a.price - b.price
      } else if (sortBy === 'price-desc') {
        return b.price - a.price
      } else {
        return b.rating - a.rating
      }
    })
  
  const categories: string[] = ['全て', 'PC', 'アクセサリー', '周辺機器']
  
  return (
    <div className="product-catalog">
      <h1>商品カタログ</h1>
      
      <div className="filters">
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="商品名で検索"
        />
        
        <select value={category} onChange={(e) => setCategory(e.target.value)}>
          {categories.map(cat => (
            <option key={cat} value={cat}>{cat}</option>
          ))}
        </select>
        
        <div>
          <label>
            最小評価: {minRating.toFixed(1)}
            <input
              type="range"
              min="0"
              max="5"
              step="0.1"
              value={minRating}
              onChange={(e) => setMinRating(parseFloat(e.target.value))}
            />
          </label>
        </div>
        
        <select 
          value={sortBy} 
          onChange={(e) => setSortBy(e.target.value as SortBy)}
        >
          <option value="name">名前順</option>
          <option value="price-asc">価格が安い順</option>
          <option value="price-desc">価格が高い順</option>
          <option value="rating">評価が高い順</option>
        </select>
        
        <label>
          <input
            type="checkbox"
            checked={onlyInStock}
            onChange={(e) => setOnlyInStock(e.target.checked)}
          />
          在庫ありのみ
        </label>
      </div>
      
      <p>検索結果: {filteredProducts.length}</p>
      
      {filteredProducts.length === 0 ? (
        <div className="no-results">
          <p>該当する商品がありません</p>
        </div>
      ) : (
        <div className="product-grid">
          {filteredProducts.map(product => (
            <div key={product.id} className="product-card">
              <h3>{product.name}</h3>
              <p className="category">{product.category}</p>
              <p className="price">¥{product.price.toLocaleString()}</p>
              <p className="rating">{product.rating}</p>
              <p className="stock">
                {product.inStock ? (
                  <span className="in-stock">✓ 在庫あり</span>
                ) : (
                  <span className="out-of-stock">✗ 在庫切れ</span>
                )}
              </p>
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

export default ProductCatalog

まとめ

この記事では、Reactの条件分岐とリスト表示のテクニックを詳しく学びました。

重要なポイント:

  • 三項演算子と論理AND演算子が最もよく使われる
  • map()でリスト表示する場合、一意のkeyが必須
  • indexをkeyにしてはいけない
  • filter()でフィルタリング、sort()でソート
  • 複数の配列メソッドは連鎖させて使える
  • 空のリストは事前にチェック
  • TypeScriptで型を明示することでバグを防ぐ

ベストプラクティス:

  • 条件分岐が複雑な場合は関数に切り出す
  • オブジェクトマッピングでswitchを避ける
  • 配列メソッド(filter、find、some、every)を活用
  • リストレンダリングのパフォーマンスに注意
  • 常に一意のkeyを使う

次のステップ: 次回は、useEffectを使ったライフサイクル管理とAPIからのデータ取得について学びます。

条件分岐とリスト表示を組み合わせることで、実用的なデータ駆動型アプリケーションが作れるようになります!