Next.jsによるWebアプリケーション開発概論

useEffectで学ぶ副作用処理:条件が変わったときに画面を更新する

8おすすめスポット共有アプリを作りながら学ぶ 画面遷移設計・状態管理・ユーザー入力処理
Next.jsTailwind CSSアプリ開発開発Web開発

はじめに

この節では、React の useEffect を使って、条件が変わったときに特別な処理を行う方法 を学びます。

前の節では、検索欄やカテゴリを使って、一覧を絞り込む画面を作りました。

今回は、その処理を useEffect を使った形で作ってみます。

先に大事なことを言うと、useEffect何でも入れる場所ではありません

「値が変わったあとに、追加で何かしたい」ときに使います。

たとえば、こんなときです。

検索条件が変わった
↓
表示するスポットを作り直す
↓
件数も更新する

この節のゴール

この節が終わると、次のことができるようになります。

  • 検索条件の変化を検知できる
  • 条件が変わったときに表示内容を更新できる
  • useEffect のよくある使い方がわかる
  • useStateuseEffect の違いを説明できる
  • 使いすぎると複雑になることも理解できる 今日の合言葉はこれです。

useState は値を持つ。useEffect は値が変わった後に何かする。


1. 副作用処理とは何か

まず、「副作用」という言葉が少し難しいです。

ここでは、かんたんにこう考えてください。

画面の表示以外で、追加で行う処理

たとえば、次のようなものです。

  • 検索条件が変わったら、表示リストを作り直す
  • データを読み込む
  • ブラウザの localStorage に保存する
  • ページタイトルを変える
  • タイマーを動かす

今回やるのは、この中でも一番わかりやすいものです。

検索条件が変わったら、表示リストを作り直す

2. useState と useEffect の違い

まず、2つの違いを整理します。

役割何をするか
useState値を保存する検索キーワードを覚える
useEffect値が変わった後に処理する検索キーワードが変わったら一覧を更新する

少しだけ言い換えると、こうです。

useState
→ 今の値を持っておく

useEffect
→ その値が変わったときに何かする

3. 今回作るもの

今回は、スポット一覧ページで次の処理を作ります。

キーワードを入力する
カテゴリを選ぶ
↓
useEffect が変化を検知する
↓
条件に合うスポットだけを作る
↓
画面に表示する

作るファイルはここです。

app/spots/page.tsx

4. useEffect の基本形

まず、useEffect の基本形を見ます。

useEffect(() => {
  // 条件が変わったときに実行したい処理を書く
}, [変化を見たい値]);

たとえば、keyword が変わったときに処理したいなら、こうです。

useEffect(() => {
  console.log('キーワードが変わりました');
}, [keyword]);

この [keyword] の部分を 依存配列 と呼びます。

初心者のうちは、こう覚えてください。

依存配列に入れた値が変わると、useEffect の中が動く。


5. まず小さく試す

いきなり一覧全体を作る前に、小さく確認します。

'use client';

import { useEffect, useState } from 'react';

/**
 * 役割: useEffect の動きを確認する
 * 入力: なし
 * 出力: キーワード入力画面
 */
export default function TestPage(): JSX.Element {
  const [keyword, setKeyword] = useState<string>('');

  useEffect(() => {
    console.log('検索キーワードが変わりました:', keyword);
  }, [keyword]);

  return (
    <main style={{ padding: '32px' }}>
      <h1>useEffect の練習</h1>

      <input
        type="text"
        value={keyword}
        onChange={(event) => {
          setKeyword(event.target.value);
        }}
        placeholder="キーワードを入力"
      />
    </main>
  );
}

このコードでは、入力欄に文字を入れるたびに、コンソールに文字が出ます。

つまり、

入力する
↓
keyword が変わる
↓
useEffect が動く
↓
console.log が出る

という流れです。


6. 表示内容を更新する考え方

次に、実際の一覧画面で使います。

必要な state は3つです。

const [keyword, setKeyword] = useState<string>('');
const [selectedCategory, setSelectedCategory] = useState<Category>('すべて');
const [filteredSpots, setFilteredSpots] = useState<Spot[]>(spots);

それぞれの役割はこうです。

state役割
keyword検索欄の文字を保存する
selectedCategory選ばれているカテゴリを保存する
filteredSpots画面に表示するスポットを保存する

そして、keywordselectedCategory が変わったときに、filteredSpots を作り直します。

useEffect(() => {
  const result = spots.filter((spot) => {
    const normalizedKeyword = keyword.trim().toLowerCase();

    const matchesKeyword =
      normalizedKeyword.length === 0 ||
      spot.name.toLowerCase().includes(normalizedKeyword) ||
      spot.description.toLowerCase().includes(normalizedKeyword) ||
      spot.location.toLowerCase().includes(normalizedKeyword);

    const matchesCategory =
      selectedCategory === 'すべて' || spot.category === selectedCategory;

    return matchesKeyword && matchesCategory;
  });

  setFilteredSpots(result);
}, [keyword, selectedCategory]);

これで、条件が変わるたびに表示内容も変わります。


7. 完成コード

app/spots/page.tsx を次の内容に置き換えてください。

app/spots/page.tsx

'use client';

import Link from 'next/link';
import { useEffect, useState } from 'react';

type Spot = {
  id: string;
  name: string;
  description: string;
  category: string;
  location: string;
  imageUrl: string;
};

const categories = [
  'すべて',
  '図書館',
  'カフェ',
  '学食',
  '作業スペース',
  '休憩スポット',
] as const;

type Category = (typeof categories)[number];

const spots: Spot[] = [
  {
    id: '1',
    name: '静かな図書館',
    description: 'とても静かで、勉強や読書に集中できます。',
    category: '図書館',
    location: '名古屋駅 徒歩5分',
    imageUrl:
      'https://images.unsplash.com/photo-1521587760476-6c12a4b040da?auto=format&fit=crop&w=1200&q=80',
  },
  {
    id: '2',
    name: 'おしゃれカフェ',
    description: '落ち着いた雰囲気で、友人との会話や作業に向いています。',
    category: 'カフェ',
    location: '栄駅 徒歩3分',
    imageUrl:
      'https://images.unsplash.com/photo-1501339847302-ac426a4a7cbb?auto=format&fit=crop&w=1200&q=80',
  },
  {
    id: '3',
    name: '学食の穴場席',
    description: '昼休み以外は比較的空いていて、短い休憩に使いやすいです。',
    category: '学食',
    location: '大学キャンパス内',
    imageUrl:
      'https://images.unsplash.com/photo-1528605248644-14dd04022da1?auto=format&fit=crop&w=1200&q=80',
  },
  {
    id: '4',
    name: '集中できる作業スペース',
    description: 'コンセントがあり、課題や制作作業に使いやすい場所です。',
    category: '作業スペース',
    location: '伏見駅 徒歩6分',
    imageUrl:
      'https://images.unsplash.com/photo-1497366754035-f200968a6e72?auto=format&fit=crop&w=1200&q=80',
  },
];

/**
 * 役割: スポット一覧画面を表示し、検索条件の変化に応じて表示内容を更新する
 * 入力: なし
 * 出力: 検索・絞り込み付きのスポット一覧画面
 */
export default function SpotsPage(): JSX.Element {
  const [keyword, setKeyword] = useState<string>('');
  const [selectedCategory, setSelectedCategory] =
    useState<Category>('すべて');
  const [filteredSpots, setFilteredSpots] = useState<Spot[]>(spots);

  useEffect(() => {
    const result = spots.filter((spot) => {
      const normalizedKeyword = keyword.trim().toLowerCase();

      const matchesKeyword =
        normalizedKeyword.length === 0 ||
        spot.name.toLowerCase().includes(normalizedKeyword) ||
        spot.description.toLowerCase().includes(normalizedKeyword) ||
        spot.location.toLowerCase().includes(normalizedKeyword);

      const matchesCategory =
        selectedCategory === 'すべて' || spot.category === selectedCategory;

      return matchesKeyword && matchesCategory;
    });

    setFilteredSpots(result);
  }, [keyword, selectedCategory]);

  return (
    <main
      style={{
        maxWidth: '960px',
        margin: '0 auto',
        padding: '32px',
      }}
    >
      <h1>スポット一覧</h1>
      <p>
        検索条件やカテゴリが変わると、useEffect が動いて表示内容を更新します。
      </p>

      <section
        style={{
          display: 'grid',
          gap: '16px',
          marginTop: '24px',
          padding: '16px',
          border: '1px solid #ddd',
          borderRadius: '12px',
          background: '#fff',
        }}
      >
        <label style={{ display: 'grid', gap: '8px' }}>
          <span>キーワード検索</span>
          <input
            type="text"
            value={keyword}
            onChange={(event) => {
              setKeyword(event.target.value);
            }}
            placeholder="例)図書館、カフェ、名古屋"
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>カテゴリで絞り込み</span>
          <select
            value={selectedCategory}
            onChange={(event) => {
              setSelectedCategory(event.target.value as Category);
            }}
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          >
            {categories.map((category) => {
              return (
                <option key={category} value={category}>
                  {category}
                </option>
              );
            })}
          </select>
        </label>

        <p
          style={{
            margin: 0,
            padding: '12px',
            borderRadius: '8px',
            background: '#e6f6f2',
            color: '#1e7b65',
            fontWeight: 700,
          }}
        >
          {filteredSpots.length}件のスポットが見つかりました
        </p>
      </section>

      <section
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
          gap: '16px',
          marginTop: '24px',
        }}
      >
        {filteredSpots.map((spot) => {
          return (
            <article
              key={spot.id}
              style={{
                border: '1px solid #ddd',
                borderRadius: '12px',
                overflow: 'hidden',
                background: '#fff',
              }}
            >
              <img
                src={spot.imageUrl}
                alt={spot.name}
                style={{
                  width: '100%',
                  height: '150px',
                  objectFit: 'cover',
                  display: 'block',
                }}
              />

              <div style={{ padding: '16px' }}>
                <p
                  style={{
                    display: 'inline-block',
                    margin: 0,
                    padding: '4px 10px',
                    borderRadius: '999px',
                    background: '#e6f6f2',
                    color: '#1e7b65',
                    fontSize: '12px',
                    fontWeight: 700,
                  }}
                >
                  {spot.category}
                </p>

                <h2>{spot.name}</h2>
                <p>{spot.description}</p>
                <p style={{ color: '#666' }}>場所: {spot.location}</p>

                <Link href={`/spots/${spot.id}`}>詳細を見る</Link>
              </div>
            </article>
          );
        })}
      </section>

      {filteredSpots.length === 0 ? (
        <p
          style={{
            marginTop: '24px',
            padding: '16px',
            border: '1px solid #ddd',
            borderRadius: '12px',
            background: '#f5f8fa',
          }}
        >
          条件に合うスポットがありません。キーワードやカテゴリを変えてみましょう。
        </p>
      ) : null}
    </main>
  );
}

8. 画面を確認する

保存したら、開発サーバーを起動します。

npm run dev

ブラウザで開きます。

http://localhost:3000/spots

次の動きを確認してください。

  • 検索欄に「図書館」と入力する
  • カテゴリを「カフェ」に変更する
  • 件数が変わる
  • 表示されるカードが変わる
  • 0件になったときにメッセージが出る ここまでできれば成功です。

9. 検索条件の変化を検知する

今回のコードで、条件の変化を見ているのはこの部分です。

useEffect(() => {
  // 条件が変わったときに実行する処理
}, [keyword, selectedCategory]);

keyword が変わったとき。selectedCategory が変わったとき。

このどちらかが変わると、useEffect の中が動きます。


10. 表示内容を更新する

表示内容を更新しているのは、この部分です。

setFilteredSpots(result);

result には、条件に合ったスポットだけが入っています。

つまり、

検索条件が変わる
↓
filter で条件に合うスポットを作る
↓
setFilteredSpots で保存する
↓
画面に表示されるカードが変わる

という流れです。


11. よくある使い方

useEffect は、次のような場面でよく使います。

データを読み込む

useEffect(() => {
  console.log('データを読み込みます');
}, []);

[] は、最初の1回だけ動くという意味です。


条件が変わったときに処理する

useEffect(() => {
  console.log('検索条件が変わりました');
}, [keyword]);

keyword が変わるたびに動きます。


localStorage に保存する

useEffect(() => {
  localStorage.setItem('keyword', keyword);
}, [keyword]);

検索キーワードが変わるたびに、ブラウザへ保存します。


12. 注意点

useEffect は便利ですが、使いすぎるとコードが複雑になります。

注意1 何でも useEffect に入れない

単純な計算だけなら、useEffect を使わない方がよい場合もあります。

たとえば、前の節のように useMemo で絞り込む方法もあります。

const filteredSpots = useMemo(() => {
  return spots.filter(...);
}, [keyword, selectedCategory]);

一覧を絞り込むだけなら、この書き方の方がすっきりすることもあります。


注意2 依存配列を忘れない

これは危険です。

useEffect(() => {
  console.log('毎回動く');
});

依存配列を書かないと、画面が更新されるたびに毎回動きます。

基本は、次のように書きます。

useEffect(() => {
  console.log('条件が変わったときだけ動く');
}, [keyword, selectedCategory]);

注意3 useEffect の中で state を更新するときは慎重にする

今回のように、条件が変わったときに filteredSpots を更新する使い方は学習としてわかりやすいです。

ただし、何でも useEffect の中で setState すればよいわけではありません。

特に、次のような書き方は無限ループの原因になります。

useEffect(() => {
  setKeyword(keyword + '!');
}, [keyword]);

keyword が変わる。useEffect が動く。 また keyword を変える。 また useEffect が動く。

このように終わらなくなります。


13. useState と useEffect の違いをもう一度整理する

最後に、もう一度整理します。

useState

const [keyword, setKeyword] = useState<string>('');

これは、値を保存する ために使います。

例です。

検索キーワード
選択中のカテゴリ
表示するスポット一覧

useEffect

useEffect(() => {
  // 値が変わったあとに実行する処理
}, [keyword]);

これは、値が変わったあとに処理する ために使います。

例です。

検索条件が変わったら一覧を更新する
お気に入りが変わったらlocalStorageへ保存する
ページを開いたらデータを読み込む

14. 今回覚えること

今回覚えるのは、これだけで十分です。

useState は値を持つ
useEffect は値の変化をきっかけに処理する
依存配列に入れた値が変わると useEffect が動く
表示内容を更新するときは setState を使う
使いすぎると複雑になるので注意する

15. ここまでのまとめ

この節では、useEffect を使って、検索条件が変わったときに一覧画面を更新しました。

流れはこうです。

検索する
↓
keyword が変わる
↓
useEffect が動く
↓
filter で条件に合うデータを作る
↓
filteredSpots が更新される
↓
画面が変わる

これで、条件に合わせて画面を更新する基本がわかりました。


練習問題

問題1

useEffect はどんなときに使いますか。

問題2

useEffect の第2引数に書く配列を何と呼びますか。

問題3

今回、検索キーワードとカテゴリの変化を見ているコードはどれですか。

問題4

useStateuseEffect の違いをかんたんに説明してください。


回答

問題1の回答

値が変わったあとに、追加で何か処理したいときに使います。

例として、検索条件が変わったときに表示内容を更新する処理があります。

問題2の回答

依存配列です。

問題3の回答

useEffect(() => {
  // 処理
}, [keyword, selectedCategory]);

問題4の回答

useState は値を保存するために使います。

useEffect は、その値が変わったあとに処理を行うために使います。


次に進む前のチェック

次へ進む前に、次の4つを確認してください。

  • useEffect の基本形がわかる
  • 依存配列に入れた値が変わると動くとわかる
  • 検索条件が変わると一覧が更新される
  • useStateuseEffect の違いを言える ここまでできればOKです。

次は、入力ミスを防ぐためのバリデーションを作っていきます。

教材トップへ戻る