React・外のデータとつなぐ

useEffectで外のデータを読む

図鑑のデータをJSONファイルに出して、useEffectとfetchで読みこみます。依存配列の意味、書き忘れると起きる無限ループ、開発中に2回実行される理由、そして「useEffectは最後の手段」という原則まで扱います。

最後の章です。いまデータはsrc/data/birds.jsに直接書いてあります。これを外のファイルに出して、読みこむ形に変えます。

見た目は何も変わりません。変わるのは「データがどこから来るか」です。

なぜ外に出すのか

data/birds.jsはアプリのコードの一部なので、中身を書きかえるたびにビルドし直しになります。実務では、データはたいてい外にあります。

  • サーバーのAPIから受け取る
  • CMS(WordPressのREST APIなど)から受け取る
  • JSONファイルとして置いておく

このコースではJSONファイルでやります。外部のサービスに頼らないので、手元で確実に動きます。

public/に置いたファイルはそのまま公開される

Viteのプロジェクトでは、public/フォルダの中身は加工されずにそのままの名前で配信されます。

shima-zukan/
├── public/
│   └── birds.json     ← /birds.jsonでアクセスできる
└── src/

src/に置いたファイルはビルドで名前が変わりますが、public/は変わりません。外から取りにいくファイルはこちらに置きます。

useEffectの形

読みこみはuseEffectの中で行います。

import { useEffect, useState } from 'react'

const [birds, setBirds] = useState([])

useEffect(() => {
  fetch('/birds.json')
    .then((response) => response.json())
    .then((data) => setBirds(data))
}, [])

useEffectは、「画面を描いたあとに、これもやっておいて」という指示です。引数は2つあります。

引数意味
1つめ(関数)実行したい処理
2つめ(配列)依存配列。ここに書いた値が変わったときだけ、もう一度実行する

[](空の配列)は「変わるものは何もない=最初の1回だけ」という意味になります。データを最初に1回読むだけなら、この形です。

読みこみ中の表示

データが届くまでの一瞬、birdsは空の配列です。何も出ないと壊れて見えるので、読みこみ中の状態を用意します。

const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
  fetch('/birds.json')
    .then((response) => response.json())
    .then((data) => {
      setBirds(data)
      setIsLoading(false)
    })
}, [])

if (isLoading) {
  return <p>よみこみ中…</p>
}

前々回の早期returnがここで効きます。読みこみ中は一覧をまるごと描かず、短い文だけ返します。

開発中は2回実行される

コンソールを見ると、リクエストが2回飛んでいることがあります。壊れているのではなく、main.jsxにある<StrictMode>のしわざです。

Reactは開発中だけ、わざと「実行→片づけ→もう一度実行」をやります。片づけを書き忘れている効果(タイマーの止め忘れ、購読の解除し忘れ)をあぶり出すためです。

useEffect(() => {
  const timer = setInterval(…)
  return () => clearInterval(timer)   // ← この片づけを書き忘れていないか
}, [])

useEffectから関数を返すと、それが片づけとして呼ばれます。今回のfetchでは片づけを書いていませんが、本番のビルドでは1回しか実行されないので、このまま進めて問題ありません。

正直に:useEffectは最後の手段

ここまで書いておいてなんですが、公式は「Effectは必要ないことが多い」と繰り返し案内しています

  • 計算できるものはEffectで作らない——絞りこんだ結果や合計は、描画のたびに計算すればいい(前回やりました)
  • イベントで起きることはイベントに書く——ボタンを押したときの処理をEffectで待ち構えない
  • データ取得も、実務ではライブラリやフレームワークに任せることが多い——読みこみ中・失敗・再取得・キャッシュの扱いが、自前だとすぐ複雑になるため

useEffectの出番は、Reactの外側とやりとりするときです。今回のような小さな読みこみでは定番の書き方ですが、「困ったらuseEffect」と覚えないでください。

公式ドキュメントも見てみる

ja.react.dev外部サイトエフェクトを使って同期を行う – React公式の「エフェクトを使って同期を行う」。依存配列の書き方と、開発中に2回実行される理由がていねいに説明されていますja.react.dev外部サイトそのエフェクトは不要かも – Reactそして「そのエフェクトは不要かも」。Effectを書きたくなったときに読むと、たいてい別の書き方が見つかります

このレッスンのまとめ

  1. public/に置いたファイルはそのままの名前で配信される/birds.json
  2. useEffect(処理, 依存配列)——描画のあとに実行する処理を書く
  3. []は「最初の1回だけ」第2引数を書き忘れると無限ループになる
  4. 読みこみ中はisLoading早期returnで別の画面を返す
  5. 開発中に2回実行されるのはStrictModeの見張り。本番では1回
  6. Effectは最後の手段——計算で出せるものは計算、イベントの処理はイベントに書く

次は逆に、画面からデータを増やします。見つけた鳥をフォームから足せるようにする回です。

やってみよう:データをJSONから読みこむ

src/data/birds.jsをJSONに移して、fetchで読みこみます。

  1. public/birds.jsonを作り、8羽ぶんの配列をJSONの書き方で書く(下のコード)
  2. src/data/birds.jsは削除する
  3. App.jsximport { birds } …を消し、birdsをstateにする
  4. useEffectでfetchしてsetBirdsする(依存配列は[]
  5. isLoadingを足して、読みこみ中は「よみこみ中…」を返す
[
  { "id": 1, "name": "シマエナガ", "emoji": "🐦", "area": "北海道", "rare": 3, "memo": "雪のようにまっ白な小鳥。ふわふわの丸い体。" },
  { "id": 2, "name": "スズメ", "emoji": "🐤", "area": "まちなか", "rare": 1, "memo": "いちばん身近な小鳥。ちゅんちゅん鳴いている。" },
  { "id": 3, "name": "カルガモ", "emoji": "🦆", "area": "かわ", "rare": 1, "memo": "親子で列になって歩く姿が有名。" },
  { "id": 4, "name": "フクロウ", "emoji": "🦉", "area": "もり", "rare": 2, "memo": "夜に活動する。首がぐるりとよく回る。" },
  { "id": 5, "name": "ハト", "emoji": "🕊️", "area": "こうえん", "rare": 1, "memo": "地面を歩きながらえさをさがす。" },
  { "id": 6, "name": "トビ", "emoji": "🦅", "area": "うみべ", "rare": 2, "memo": "空高く輪をえがいて飛ぶ。ピーヒョロロと鳴く。" },
  { "id": 7, "name": "ハシブトガラス", "emoji": "🐦‍⬛", "area": "まちなか", "rare": 1, "memo": "" },
  { "id": 8, "name": "ハクチョウ", "emoji": "🦢", "area": "みずうみ", "rare": 3, "memo": "冬にやってくる大きな白い鳥。" }
]

JSONではキーも文字列も必ずダブルクォートで、末尾のカンマは書けません。JavaScriptの書き方のまま貼ると読みこみに失敗します。

import { useEffect, useState } from 'react'

const [birds, setBirds] = useState([])
const [isLoading, setIsLoading] = useState(true)

useEffect(() => {
  fetch('/birds.json')
    .then((response) => response.json())
    .then((data) => {
      setBirds(data)
      setIsLoading(false)
    })
}, [])

if (isLoading) {
  return (
    <div className="min-h-screen bg-sky-50 p-8">
      <p className="text-slate-500">よみこみ中…</p>
    </div>
  )
}
水色の背景の左上に「よみこみ中…」とだけ表示されている
一瞬だけ出る読みこみ中の画面。手元のファイルなので一瞬ですが、開発者ツールのネットワークで通信を遅くすると、じっくり見られます

読みこみが終わると、前回とまったく同じ画面になります。見た目が変わらないのが正解です。

よくある質問

useEffectとは何ですか?
描画そのものとは関係のない処理を、描画のあとに実行するための仕組みです。外部からのデータ取得、購読の開始と解除、ブラウザのAPI操作など、Reactの外側とやりとりする処理に使います。
依存配列とは何ですか?
useEffectの2つめの引数に渡す配列です。中に書いた値が前回と変わったときだけ、処理をもう一度実行します。空の配列を渡すと最初の1回だけになります。
useEffectが2回実行されるのはなぜですか?
開発中のStrictModeが、後片づけ漏れを見つけるためにわざと実行と解除を1往復多くしているためです。本番のビルドでは1回だけです。2回動いて困る処理は、書き方に問題があるサインでもあります。
データの取得は必ずuseEffectで書くのですか?
小さなアプリでは定番の書き方ですが、公式は「必要ないことが多い」と案内しています。実務ではデータ取得用のライブラリやフレームワークの仕組みを使うほうが、読みこみ中・失敗・再取得の扱いが楽になります。