useEffect・LocalStorage・React Router・プラン作成画面
このパートで学ぶこと
useEffectで「描画後の処理」を書く方法- LocalStorage の読み書きを React で扱う方法
- React Router でページ遷移を実装する方法
- プラン作成画面を React コンポーネントで作る方法
ステップ13|useEffect を理解しよう
useEffect が必要な理由
React では、コンポーネントが描画されるたびに関数が実行されます。
「描画とは関係なく1回だけ実行したい処理」 は useEffect で書きます。
代表的な例:
- ページを開いたとき LocalStorage からデータを読み込む
- 状態が変わったとき LocalStorage に保存する
useEffect の書き方
import { useEffect } from 'react';
useEffect(() => {
// ここに実行したい処理を書く
}, [依存配列]);
依存配列の意味
| 書き方 | 実行タイミング |
|---|---|
[] |
コンポーネントが初めて表示されたとき 1回だけ |
[count] |
count が変わるたびに実行 |
| なし(省略) | 毎回の描画ごとに実行(ほぼ使わない) |
参考:ページ表示時に1回だけ実行する書き方
useEffect(() => {
console.log('ページが表示された!');
}, []); // ← [] を渡すと最初の1回だけ
参考:状態が変わるたびに実行する書き方
useEffect(() => {
console.log('count が変わった!', count);
}, [count]); // ← count が変わるたびに実行
やってみよう
-
useEffect(() => { console.log('表示された'); }, [])をApp.jsxに書いてみましょう - ブラウザをリロードしたとき1回だけコンソールに出ることを確認しましょう
- 次に
[]の中にselectedAreaを入れて、セレクトボックスを変えるたびにログが出ることを確認しましょう
ステップ14|LocalStorage の読み込みを useEffect で書こう
ページ表示時にプランデータを読み込もう
今の script.js ではファイルの先頭でこのように書いていました。
// Vanilla JS
const createdPlans = JSON.parse(localStorage.getItem('createdPlans')) || [];
React ではこれを useEffect と useState に分けて書きます。
参考:初期読み込みのパターン
// まず状態の宣言(初期値は空配列)
const [createdPlans, setCreatedPlans] = useState([]);
// ページ表示時に LocalStorage から読み込む
useEffect(() => {
const saved = localStorage.getItem('createdPlans');
if (saved) {
setCreatedPlans(JSON.parse(saved));
}
}, []); // ← 最初の1回だけ
📌
useStateの初期値にJSON.parse(localStorage.getItem(...))を書くこともできますが、
useEffectで書く方が「副作用を分離する」という React のスタイルに合っています。
toast メッセージの表示も useEffect で書こう
今の script.js にはプラン作成後のトーストメッセージ表示があります。
これも useEffect の中に書きます。
参考:ページ表示時に LocalStorage のメッセージを確認して表示する
const [toastMessage, setToastMessage] = useState('');
useEffect(() => {
const message = localStorage.getItem('toastMessage');
if (message) {
setToastMessage(message);
localStorage.removeItem('toastMessage');
// 3秒後にメッセージを消す
setTimeout(() => {
setToastMessage('');
}, 3000);
}
}, []);
参考:トーストを条件付きで表示する
{toastMessage && (
<div className="toast show">{toastMessage}</div>
)}
やってみよう
-
createdPlansをuseState([])で宣言し、useEffectで LocalStorage から読み込みましょう -
toastMessageの状態を作り、ページ表示時にトーストを出す処理を書きましょう - ブラウザの開発者ツールで LocalStorage に
createdPlansを手動で追加して、ページをリロードしたとき読み込まれることを確認しましょう
ステップ15|プラン一覧を画面に表示しよう
PlanSection.jsx を作ろう
ホーム画面のプラン一覧エリアを PlanSection コンポーネントに分けます。
props で受け取るもの
-
plans:表示するプランの配列(createdPlans)
参考:プラン一覧の表示(二重ループ)
// PlanSection.jsx
function PlanSection({ plans }) {
return (
<div className="plan-content">
{plans.map((plan, planIndex) => (
<div key={planIndex} className="plan-box">
<h3>プラン {planIndex + 1}</h3>
{plan.map((spot, spotIndex) => (
<p key={spot.name}>
{spotIndex + 1}. {spot.name}
</p>
))}
</div>
))}
</div>
);
}
📌 プランの中にスポットが入っているので、ループの中でさらにループしています。
それぞれのkeyを忘れないようにしましょう。
やってみよう
-
PlanSection.jsxを作り、plansを props で受け取って表示しましょう -
App.jsxからcreatedPlansを渡して表示されることを確認しましょう - 開発者ツールで LocalStorage に複数のプランデータを手動で追加して、一覧に反映されることを確認しましょう
ステップ16|React Router を導入しよう
React Router とは?
今のアプリでは index.html と plan.html の2ページがあります。
React では React Router を使ってページ(URL)を切り替えます。
npm install react-router-dom
React Router の仕組みを理解しよう
React Router では、URL に応じて表示するコンポーネントを切り替えます。
URL が / → HomePage コンポーネントを表示
URL が /plan → PlanPage コンポーネントを表示
参考:React Router の基本セットアップ
// main.jsx(BrowserRouter を追加する)
import { BrowserRouter } from 'react-router-dom';
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
// App.jsx(Routes と Route でページを定義する)
import { Routes, Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import PlanPage from './pages/PlanPage';
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/plan" element={<PlanPage />} />
</Routes>
);
}
src/pages/ フォルダを作ろう
コンポーネント(部品)と ページ(画面)を分けて管理します。
src/
├── components/ ← 再利用できる部品(Header、MapArea など)
├── pages/ ← 画面単位(HomePage、PlanPage)
└── App.jsx ← ルーティングのみ担当
やってみよう
-
npm install react-router-domを実行しましょう -
main.jsxにBrowserRouterを追加しましょう -
src/pages/HomePage.jsxとsrc/pages/PlanPage.jsxを作りましょう -
App.jsxにルート設定を書いて、/で HomePage、/planで PlanPage が表示されることを確認しましょう
ステップ17|ページ遷移を実装しよう
useNavigate でページを移動する
Vanilla JS では location.href = 'plan.html' と書いていましたが、
React Router では useNavigate を使います。
参考:useNavigate の使い方
import { useNavigate } from 'react-router-dom';
function HomePage() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/plan')}>
プラン作成
</button>
);
}
Link コンポーネントを使う方法もある
ボタンではなくリンクとして遷移させるときは Link を使います。
import { Link } from 'react-router-dom';
<Link to="/plan">プラン作成</Link>
📌
<a href="/plan">と書くとページが完全リロードされてしまいます。
React Router のアプリ内での遷移は必ずLinkまたはuseNavigateを使いましょう。
やってみよう
- 「プラン作成」ボタンに
useNavigateを設定して/planに遷移できることを確認しましょう -
PlanPageの「地図に戻る」ボタンもnavigate('/')で戻れるようにしましょう
ステップ18|プラン作成画面(PlanPage)を作ろう
plan.html → PlanPage.jsx に移す
今の plan.html と plan.js の処理を PlanPage.jsx に集めます。
PlanPage で管理する状態
// plan.js の先頭にあったデータの読み込み
const [memoSpots, setMemoSpots] = useState([]); // 「プランメモ」のスポット一覧
const [selectedCards, setSelectedCards] = useState([]); // 選択中のカード
参考:ページ表示時にメモスポットを読み込む
useEffect(() => {
const saved = localStorage.getItem('Plan');
if (saved) {
setMemoSpots(JSON.parse(saved));
}
}, []);
カード選択のロジックを書こう
参考:カードをクリックしたときの選択トグル
function handleCardClick(spot) {
if (selectedCards.find(s => s.name === spot.name)) {
// すでに選択中 → 外す
setSelectedCards(selectedCards.filter(s => s.name !== spot.name));
} else {
// 未選択 → 追加
setSelectedCards([...selectedCards, spot]);
}
}
参考:選択中かどうかで className を切り替える
<div
className={
selectedCards.find(s => s.name === spot.name)
? 'memo-card active'
: 'memo-card'
}
onClick={() => handleCardClick(spot)}
>
<h3>{spot.name}</h3>
<p>{spot.description}</p>
</div>
参考:バッジ(番号)を表示する
// selectedCards の中での順番を調べる
const selectedIndex = selectedCards.findIndex(s => s.name === spot.name);
{selectedIndex !== -1 && (
<span className="plan-num">{selectedIndex + 1}</span>
)}
やってみよう
-
PlanPage.jsxにmemoSpotsとselectedCardsの状態を作りましょう - カードを表示して、クリックで選択・解除ができることを確認しましょう
- 選択したカードにバッジ(番号)が表示されることを確認しましょう
ステップ19|プラン作成ボタンを実装しよう
バリデーション(件数チェック)
2件未満、または6件以上の場合はアラートを出します。
参考:選択件数のバリデーション
function handleCreatePlan() {
const count = selectedCards.length;
if (count <= 1) {
alert('スポットを2箇所以上選択してください。');
return;
}
if (count >= 6) {
alert('スポットを5箇所以内で選択してください。');
return;
}
// ここに保存処理を書く
}
LocalStorage への保存とページ遷移
参考:プランを LocalStorage に追加して保存する
// 既存のプランを読み込んで、新しいプランを追加する
const existing = JSON.parse(localStorage.getItem('createdPlans')) || [];
existing.push(selectedCards);
localStorage.setItem('createdPlans', JSON.stringify(existing));
// トーストメッセージをセットしてホームに戻る
localStorage.setItem('toastMessage', 'プランを作成しました。');
navigate('/');
📌 ホーム画面の
useEffectでtoastMessageを読み込む処理を書いてあるので、
ここでは LocalStorage にメッセージをセットするだけで OK です。
やってみよう
- バリデーション付きの
handleCreatePlan関数を作りましょう - 「プラン作成」ボタンに
onClick={handleCreatePlan}を設定しましょう - プランを作成してホーム画面に戻ったとき、プランが表示されることを確認しましょう
- トーストメッセージが3秒後に消えることも確認しましょう
ステップ20|InfoWindow に「プランメモ追加」ボタンを付けよう
メモへの追加処理を MapArea に渡す
「プランメモに追加」ボタンを押したとき、memo(LocalStorage の Plan キー)にスポットを追加します。
この処理は HomePage に書き、MapArea に onAddToMemo として渡します。
参考:メモ追加の処理(HomePage 側)
function handleAddToMemo(spot) {
const saved = localStorage.getItem('Plan');
const current = saved ? JSON.parse(saved) : [];
// 重複チェック
const isDuplicate = current.find(item => item.name === spot.name);
if (isDuplicate) {
console.log('すでに追加済みです');
return;
}
current.push(spot);
localStorage.setItem('Plan', JSON.stringify(current));
}
参考:MapArea に渡す
// HomePage.jsx
<MapArea
spots={filteredSpots}
onAddToMemo={handleAddToMemo}
/>
参考:InfoWindow の中でボタンを使う
// MapArea.jsx
<InfoWindow ...>
<div>
<p>{activeSpot.name}</p>
<p>{activeSpot.description}</p>
<button onClick={() => onAddToMemo(activeSpot)}>
プランメモに追加
</button>
</div>
</InfoWindow>
📌 React の InfoWindow は JSX で中身を書けるので、
Vanilla JS のようにdomreadyイベントを使う必要がありません!
やってみよう
-
handleAddToMemoをHomePage.jsxに書きましょう -
MapAreaにonAddToMemoとして渡しましょう - InfoWindow の中にボタンを追加して、押すと LocalStorage の
Planにスポットが追加されることを確認しましょう
Part 3 まとめ
完成後のファイル構成です。
src/
├── components/
│ ├── Header.jsx
│ ├── MapArea.jsx ← spots・onAddToMemo を props で受け取る
│ ├── FilterPanel.jsx ← 絞り込み条件を props で受け取る
│ └── PlanSection.jsx ← plans を props で受け取って表示
├── pages/
│ ├── HomePage.jsx ← 状態管理の中心・各コンポーネントに props を渡す
│ └── PlanPage.jsx ← memoSpots・selectedCards を useState で管理
├── data/
│ └── spots.js
├── utils/
│ └── filterSpots.js
├── App.jsx ← Routes の設定のみ
├── main.jsx ← BrowserRouter を追加
└── index.css
学んだこと
-
useEffectで「副作用」を扱う方法(LocalStorage の読み書き・初期化処理) - 依存配列
[]の意味と使い分け - React Router で複数ページを管理する方法(
Routes・Route・useNavigate) - プラン作成画面のロジックを React のコンポーネントで書く方法
全体振り返り:Vanilla JS → React で何が変わったか
| やること | Vanilla JS | React |
|---|---|---|
| 画面を更新する |
innerHTML、appendChild
|
setState → 自動で再描画 |
| イベントを付ける | addEventListener |
onClick、onChange など props |
| ページ遷移 | location.href = '...' |
navigate('/...') |
| 別ファイルを読む | <script src="..."> |
import |
| 処理をまとめる | 関数 | コンポーネント |
| 初期化処理 | スクリプトの先頭 | useEffect(() => {...}, []) |
Vanilla JS と比べると書くことが増えますが、
状態と画面が常に連動している ので、複雑な機能を追加するほど管理しやすくなります!