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

はじめに
この節では、React の useEffect を使って、条件が変わったときに特別な処理を行う方法 を学びます。
前の節では、検索欄やカテゴリを使って、一覧を絞り込む画面を作りました。
今回は、その処理を useEffect を使った形で作ってみます。
先に大事なことを言うと、useEffect は 何でも入れる場所ではありません。
「値が変わったあとに、追加で何かしたい」ときに使います。
たとえば、こんなときです。
検索条件が変わった
↓
表示するスポットを作り直す
↓
件数も更新する
この節のゴール
この節が終わると、次のことができるようになります。
- 検索条件の変化を検知できる
- 条件が変わったときに表示内容を更新できる
useEffectのよくある使い方がわかるuseStateとuseEffectの違いを説明できる- 使いすぎると複雑になることも理解できる 今日の合言葉はこれです。
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 | 画面に表示するスポットを保存する |
そして、keyword や selectedCategory が変わったときに、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
useState と useEffect の違いをかんたんに説明してください。
回答
問題1の回答
値が変わったあとに、追加で何か処理したいときに使います。
例として、検索条件が変わったときに表示内容を更新する処理があります。
問題2の回答
依存配列です。
問題3の回答
useEffect(() => {
// 処理
}, [keyword, selectedCategory]);
問題4の回答
useState は値を保存するために使います。
useEffect は、その値が変わったあとに処理を行うために使います。
次に進む前のチェック
次へ進む前に、次の4つを確認してください。
useEffectの基本形がわかる- 依存配列に入れた値が変わると動くとわかる
- 検索条件が変わると一覧が更新される
useStateとuseEffectの違いを言える ここまでできればOKです。
次は、入力ミスを防ぐためのバリデーションを作っていきます。
