[React講座] API通信とデータ取得

当ページのリンクには広告が含まれています。
目次

API通信とデータ取得

最小構成:useEffect + fetch のパターン

まずは「マウント時に1回だけAPIを叩いて一覧を表示する」最小パターンから。

import { useState, useEffect } from 'react'

export default function PostList() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    async function fetchPosts() {
      const res = await fetch('https://jsonplaceholder.typicode.com/posts')
      const data = await res.json()
      setPosts(data.slice(0, 5)) // 5件だけに絞る
    }

    fetchPosts()
  }, []) // 初回マウント時だけ

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

ここで押さえたい流れ:

  1. 初回レンダリング後、useEffect(..., []) が1回だけ走る
  2. fetchres.json() でデータ取得
  3. setPosts で state を更新
  4. state が変わったので再レンダリング ⇒ <li> 一覧が表示される

最初は 「マウント時1回だけ」「一覧表示だけ」 に絞って感覚を掴むのがおすすめです。

loadingerror を持つ

実務レベルに近づけるには、ローディング状態とエラー状態も持ったほうがよいです。

import { useState, useEffect } from 'react'

export default function PostList() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let isCancelled = false

    async function fetchPosts() {
      try {
        setLoading(true)
        setError(null)

        const res = await fetch('https://jsonplaceholder.typicode.com/posts')
        if (!res.ok) {
          throw new Error('ネットワークエラー')
        }
        const data = await res.json()

        if (!isCancelled) {
          setPosts(data.slice(0, 5))
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message || '何らかのエラーが発生しました')
        }
      } finally {
        if (!isCancelled) {
          setLoading(false)
        }
      }
    }

    fetchPosts()

    return () => {
      isCancelled = true
    }
  }, [])

  if (loading) return <p>読み込み中...</p>
  if (error) return <p style={{ color: 'red' }}>エラー: {error}</p>

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

ポイント整理:

  • 状態を3種類持つ
    • posts(データ)
    • loading(読み込み中か?)
    • error(エラーメッセージ)
  • 通信開始前に setLoading(true)setError(null)
  • 通信成功 ⇒ setPostssetLoading(false)
  • 通信失敗 ⇒ setError(...)setLoading(false)
  • UI側では
    • loading ならローディング表示
    • error があればエラー表示
    • どちらでもないときに一覧表示

この「状態3点セット + 条件分岐でUI切り替え」は、API連携画面の定番パターンです。

条件付きで再取得:検索キーワードやページ番号

次に、ユーザー入力に応じてAPIを叩き直すパターンを見てみます。

例:キーワード検索で毎回APIリクエスト

import { useState, useEffect } from 'react'

export default function SearchUsers() {
  const [keyword, setKeyword] = useState('')   // 入力中のキーワード
  const [query, setQuery] = useState('')      // 実際に検索に使うキーワード
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  useEffect(() => {
    if (!query) return // 空文字なら検索しない

    let isCancelled = false

    async function fetchUsers() {
      try {
        setLoading(true)
        setError(null)

        const res = await fetch(
          `https://api.example.com/users?keyword=${encodeURIComponent(query)}`
        )
        if (!res.ok) throw new Error('検索に失敗しました')
        const data = await res.json()

        if (!isCancelled) {
          setUsers(data)
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message)
        }
      } finally {
        if (!isCancelled) {
          setLoading(false)
        }
      }
    }

    fetchUsers()

    return () => {
      isCancelled = true
    }
  }, [query]) // ← query が変わるたびに検索

  function handleSubmit(e) {
    e.preventDefault()
    setQuery(keyword.trim())
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input
          value={keyword}
          onChange={(e) => setKeyword(e.target.value)}
          placeholder="ユーザー名で検索"
        />
        <button type="submit">検索</button>
      </form>

      {loading && <p>検索中...</p>}
      {error && <p style={{ color: 'red' }}>エラー: {error}</p>}

      <ul>
        {users.map((u) => (
          <li key={u.id}>{u.name}</li>
        ))}
      </ul>
    </div>
  )
}

設計のポイント:

  • 入力中のテキスト keyword と、実際に検索に使う query を分けることで、「入力されるたびにAPIを叩く」のを防ぎ、「フォーム送信時だけAPIを呼ぶ」ように制御している
  • useEffect の依存配列には query を入れ、「検索条件が変わったときだけ」リクエストを投げる

同じ考え方で、page(ページ番号)をstateに持ち、[page] を依存にしてページング付き一覧を作ることもできます。

データ表示時の基本テクニック:mapkey

APIから配列が返ってきたら、ほぼ必ず map でJSXに変換します。

<ul>
  {items.map((item) => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>
  • key は React が要素を識別するために使うIDのようなもの
  • 理想は APIのレコードIDなど、一意な値 を使う
  • arrayのindexをkeyに使うのは「並べ替えや削除が発生しないリスト」に限定

key の目的は 再レンダリング時に「どの要素がどれか」を見分けるため なので、id などの安定したユニークキーがあるなら、基本それ一択と覚えてOKです。

fetchaxios ざっくり比較

学習段階では fetch だけで十分ですが、実務では axios もよく使われます。
ざっくり違いだけ押さえておきましょう。

fetch

  • ブラウザ標準API(追加ライブラリ不要)
  • デフォルトではエラー時も res.ok を自分でチェックする必要がある
  • JSON変換も await res.json() を毎回書く

axios

  • 別途インストールが必要:npm install axios
  • 自動でJSONパースして res.data に入れてくれる
  • HTTPステータスが4xx/5xxなら例外を投げてくれるので、エラー処理が書きやすい

axios版の例:

import axios from 'axios'
import { useState, useEffect } from 'react'

export default function PostList() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    async function fetchPosts() {
      const res = await axios.get('https://jsonplaceholder.typicode.com/posts')
      setPosts(res.data.slice(0, 5))
    }
    fetchPosts()
  }, [])

  // 以降は同じ
}

どちらを使うかはプロジェクトポリシー次第ですが、仕組みの理解と基礎練習は fetch で十分です。

簡単な「再読込」ボタンを付けてみる

API連携画面では、手動で再取得するボタンもよくあります。

import { useState, useEffect } from 'react'

export default function RandomUser() {
  const [user, setUser] = useState(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  const [reloadKey, setReloadKey] = useState(0)

  useEffect(() => {
    let isCancelled = false

    async function fetchUser() {
      try {
        setLoading(true)
        setError(null)

        const res = await fetch('https://randomuser.me/api/')
        if (!res.ok) throw new Error('取得に失敗しました')
        const data = await res.json()
        const u = data.results[0]

        if (!isCancelled) {
          setUser({
            name: `${u.name.first} ${u.name.last}`,
            email: u.email,
          })
        }
      } catch (err) {
        if (!isCancelled) {
          setError(err.message)
        }
      } finally {
        if (!isCancelled) {
          setLoading(false)
        }
      }
    }

    fetchUser()

    return () => {
      isCancelled = true
    }
  }, [reloadKey]) // ← reloadKey が変わるたびに再取得

  function handleReload() {
    setReloadKey((k) => k + 1)
  }

  return (
    <div>
      <button onClick={handleReload} disabled={loading}>
        再読込
      </button>

      {loading && <p>読み込み中...</p>}
      {error && <p style={{ color: 'red' }}>エラー: {error}</p>}
      {user && (
        <div>
          <p>名前: {user.name}</p>
          <p>メール: {user.email}</p>
        </div>
      )}
    </div>
  )
}

処理の流れ:

reloadKey というstateを用意し、その値を useEffect の依存配列に入れることで、ボタンを押したときに reloadKey をインクリメント ⇒ useEffectが再実行 ⇒ 再読込

まとめ

  • useEffect + fetch が 「ReactでAPIを叩く基本の型」
  • 実務では data だけでなく、loading / error の3つのstateをセットで持つことが多い
  • 条件付き再取得は useEffect の依存配列に query / page / reloadKey などを入れて制御
  • map + key で一覧表示、key には安定したIDを使う
  • axiosを使うとエラー処理やJSONパースが少し楽になるが、基礎は fetch で十分

<<前へ(副作用とライフサイクル(useEffect))

>>次へ(ルーティング(React Router))

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次