0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ツーリングスポットアプリ React化【Part 3】

0
Posted at

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 ではこれを useEffectuseState に分けて書きます。

参考:初期読み込みのパターン

// まず状態の宣言(初期値は空配列)
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>
)}

やってみよう

  • createdPlansuseState([]) で宣言し、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.htmlplan.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.jsxBrowserRouter を追加しましょう
  • src/pages/HomePage.jsxsrc/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.htmlplan.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.jsxmemoSpotsselectedCards の状態を作りましょう
  • カードを表示して、クリックで選択・解除ができることを確認しましょう
  • 選択したカードにバッジ(番号)が表示されることを確認しましょう

ステップ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('/');

📌 ホーム画面の useEffecttoastMessage を読み込む処理を書いてあるので、
ここでは LocalStorage にメッセージをセットするだけで OK です。

やってみよう

  • バリデーション付きの handleCreatePlan 関数を作りましょう
  • 「プラン作成」ボタンに onClick={handleCreatePlan} を設定しましょう
  • プランを作成してホーム画面に戻ったとき、プランが表示されることを確認しましょう
  • トーストメッセージが3秒後に消えることも確認しましょう

ステップ20|InfoWindow に「プランメモ追加」ボタンを付けよう

メモへの追加処理を MapArea に渡す

「プランメモに追加」ボタンを押したとき、memo(LocalStorage の Plan キー)にスポットを追加します。
この処理は HomePage に書き、MapAreaonAddToMemo として渡します。

参考:メモ追加の処理(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 イベントを使う必要がありません!

やってみよう

  • handleAddToMemoHomePage.jsx に書きましょう
  • MapAreaonAddToMemo として渡しましょう
  • 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 で複数ページを管理する方法(RoutesRouteuseNavigate
  • プラン作成画面のロジックを React のコンポーネントで書く方法

全体振り返り:Vanilla JS → React で何が変わったか

やること Vanilla JS React
画面を更新する innerHTMLappendChild setState → 自動で再描画
イベントを付ける addEventListener onClickonChange など props
ページ遷移 location.href = '...' navigate('/...')
別ファイルを読む <script src="..."> import
処理をまとめる 関数 コンポーネント
初期化処理 スクリプトの先頭 useEffect(() => {...}, [])

Vanilla JS と比べると書くことが増えますが、
状態と画面が常に連動している ので、複雑な機能を追加するほど管理しやすくなります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?