React RouterとHooksの実践的応用

React Router DOMの活用方法

1. ハッシュモードによるルーティング

ブラウザのURLハッシュを利用した基本的なルーティング実装例です。ハッシュ値の変化を監視してコンテンツを切り替えます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>ハッシュルーター</title>
</head>
<body>
    <nav>
        <a href="#/top">トップ</a>
        <a href="#/info">情報</a>
    </nav>
    <main id="content"></main>
    
    <script>
        const contentArea = document.getElementById('content');
        
        window.addEventListener('hashchange', () => {
            const path = location.hash.substring(1);
            renderContent(path);
        });
        
        function renderContent(path) {
            switch(path) {
                case '/top':
                    contentArea.innerHTML = '<h1>トップページ</h1>';
                    break;
                case '/info':
                    contentArea.innerHTML = '<h1>情報ページ</h1>';
                    break;
                default:
                    contentArea.innerHTML = '<h1>デフォルトページ</h1>';
            }
        }
        
        // 初期表示
        renderContent(location.hash.substring(1) || '/top');
    </script>
</body>
</html>

2. History APIを利用したルーティング

HTML5 History APIを使ったよりクリーンなURLの実装例です。pushStateメソッドでURLを制御します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Historyルーター</title>
</head>
<body>
    <nav>
        <a href="/top" onclick="handleRoute(event)">トップ</a>
        <a href="/info" onclick="handleRoute(event)">情報</a>
    </nav>
    <main id="app"></main>
    
    <script>
        const app = document.getElementById('app');
        
        function handleRoute(event) {
            event.preventDefault();
            const path = event.target.getAttribute('href');
            history.pushState({}, '', path);
            updateView(path);
        }
        
        function updateView(path) {
            switch(path) {
                case '/top':
                    app.innerHTML = '<h2>トップ</h2>';
                    break;
                case '/info':
                    app.innerHTML = '<h2>情報</h2>';
                    break;
                default:
                    app.innerHTML = '<h2>未設定</h2>';
            }
        }
        
        window.addEventListener('popstate', () => {
            updateView(location.pathname);
        });
    </script>
</body>
</html>

3. 基本的なルーター設定

React Router DOM v6での基本的な実装方法です。Routesコンポーネントでルートを定義します。

// src/components/MainApp.jsx
import React from 'react';
import { BrowserRouter, Link, Routes, Route, Navigate } from 'react-router-dom';
import { routeConfig } from '@/config/routes';

export function MainApp() {
  return (
    <BrowserRouter>
      <header>
        <Link to="/">ホーム</Link>
        <Link to="/info">案内</Link>
        <Link to="/user/25">ユーザー</Link>
      </header>
      
      <Routes>
        {routeConfig.map(route => (
          <Route
            key={route.id}
            path={route.path}
            element={route.element}
          />
        ))}
      </Routes>
    </BrowserRouter>
  );
}

// src/config/routes.js
import { TopPage } from '@/pages/TopPage';
import { InfoPage } from '@/pages/InfoPage';
import { UserPage } from '@/pages/UserPage';
import { NotFound } from '@/pages/NotFound';

export const routeConfig = [
  { id: 1, path: '/', element: <Navigate to="/top" replace /> },
  { id: 2, path: '/top', element: <TopPage /> },
  { id: 3, path: '/info', element: <InfoPage /> },
  { id: 4, path: '/user/:userId', element: <UserPage /> },
  { id: 5, path: '*', element: <NotFound /> }
];

4. NavLinkによるスタイリング

NavLinkを使用してアクティブなリンクにスタイルを適用する方法です。

import React from 'react';
import { NavLink, Routes, Route } from 'react-router-dom';
import { routeConfig } from '@/config/routes';

export function Navigation() {
  const linkStyle = ({ isActive }) => ({
    color: isActive ? 'crimson' : 'inherit',
    fontWeight: isActive ? 'bold' : 'normal'
  });
  
  const linkClass = ({ isActive }) => 
    isActive ? 'nav-link active' : 'nav-link';
  
  return (
    <nav>
      <NavLink to="/top" style={linkStyle} className={linkClass}>
        トップ
      </NavLink>
      <NavLink to="/info" style={linkStyle} className={linkClass}>
        案内
      </NavLink>
      
      <Routes>
        {routeConfig.map(route => (
          <Route key={route.id} path={route.path} element={route.element} />
        ))}
      </Routes>
    </nav>
  );
}

5. ネストしたルーティングとリダイレクト

親子関係を持つルーティング構造の実装例です。

// src/pages/TopPage.jsx
import React from 'react';
import { Link, Routes, Route, Navigate } from 'react-router-dom';
import { topSubRoutes } from '@/config/routes';

export function TopPage() {
  return (
    <div>
      <aside>
        <Link to="/top/products">商品</Link>
        <Link to="/top/pricing">料金</Link>
      </aside>
      
      <Routes>
        {topSubRoutes.map(sub => (
          <Route
            key={sub.id}
            path={sub.path}
            element={sub.element}
          />
        ))}
      </Routes>
    </div>
  );
}

// src/config/routes.js
export const topSubRoutes = [
  { id: 'top-1', path: '*', element: <Navigate to="products" replace /> },
  { id: 'top-2', path: 'products', element: <ProductList /> },
  { id: 'top-3', path: 'pricing', element: <PricingTable /> }
];

6. プログラムによるナビゲーション

useNavigateフックを使ったプログラムによる画面遷移の実装です。

// src/utils/withNavigation.jsx
import { useNavigate } from 'react-router-dom';

export function withNavigation(Component) {
  return function WrappedComponent(props) {
    const navigate = useNavigate();
    return <Component {...props} navigate={navigate} />;
  };
}

// src/pages/TopPage.jsx
import React from 'react';
import { withNavigation } from '@/utils/withNavigation';

class TopPageClass extends React.Component {
  handleJump = () => {
    this.props.navigate('/info', { state: { from: 'top' } });
  };
  
  render() {
    return (
      <div>
        <h1>トップページ</h1>
        <button onClick={this.handleJump}>案内へ移動</button>
      </div>
    );
  }
}

export const TopPage = withNavigation(TopPageClass);

7. 動的ルーティング

URLパラメータを利用した動的なルーティングの実装です。

// src/config/routes.js
export const routeConfig = [
  { id: 4, path: '/user/:userId', element: <UserDetail /> }
];

// src/pages/UserDetail.jsx
import React from 'react';
import { useParams } from 'react-router-dom';

export function UserDetail() {
  const { userId } = useParams();
  
  return (
    <div>
      <h2>ユーザー詳細</h2>
      <p>ユーザーID: {userId}</p>
    </div>
  );
}

8. パラメータの受け渡し

複数の方法によるパラメータの受け渡し例です。

// クエリパラメータとstate付き遷移
import { Link, useLocation } from 'react-router-dom';

function NavigationButtons() {
  return (
    <div>
      <Link 
        to={{ pathname: '/info', search: '?source=header' }}
        state={{ userType: 'premium' }}
      >
        詳細情報
      </Link>
    </div>
  );
}

// パラメータの取得
function InfoPage() {
  const location = useLocation();
  const queryParams = new URLSearchParams(location.search);
  
  React.useEffect(() => {
    console.log('Query:', queryParams.get('source'));
    console.log('State:', location.state);
  }, []);
  
  return <div>情報ページ</div>;
}

React Hooksの応用テクニック

1. 基本的なカウンター実装

useStateフックを使ったシンプルなカウンター例です。

import React, { useState } from 'react';

export function SimpleCounter() {
  const [number, setNumber] = useState(0);
  
  return (
    <div>
      <h3>現在の値: {number}</h3>
      <button onClick={() => setNumber(prev => prev + 1)}>増加</button>
      <button onClick={() => setNumber(prev => prev - 1)}>減少</button>
    </div>
  );
}

2. 複雑な状態管理

配列やオブジェクトを扱う状態管理の実装例です。

import React, { useState } from 'react';

export function ItemManager() {
  const [items, setItems] = useState([]);
  const [formData, setFormData] = useState({ title: '', quantity: 1 });
  
  const addItem = () => {
    const newItem = {
      id: Date.now(),
      ...formData
    };
    setItems(prev => [...prev, newItem]);
  };
  
  const updateQuantity = (id, delta) => {
    setItems(prev => prev.map(item => 
      item.id === id ? { ...item, quantity: item.quantity + delta } : item
    ));
  };
  
  return (
    <div>
      <input 
        value={formData.title}
        onChange={e => setFormData({...formData, title: e.target.value})}
      />
      <button onClick={addItem}>追加</button>
      
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}: {item.quantity}
            <button onClick={() => updateQuantity(item.id, 1)}>+</button>
            <button onClick={() => updateQuantity(item.id, -1)}>-</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

3. useStateの高度な使い方

初期値の遅延評価と更新関数の活用例です。

import React, { useState } from 'react';

export function AdvancedState() {
  // 初期値の遅延評価
  const [value, setValue] = useState(() => {
    const saved = localStorage.getItem('myKey');
    return saved ? JSON.parse(saved) : 0;
  });
  
  const handleMultipleUpdates = () => {
    // 関数型更新で連続して安全に状態を更新
    for (let i = 0; i < 5; i++) {
      setValue(prev => prev + i);
    }
  };
  
  return (
    <div>
      <div>値: {value}</div>
      <button onClick={handleMultipleUpdates}>5回更新</button>
    </div>
  );
}

4. useEffectの基本的な使い方

副作用の処理とクリーンアップの実装例です。

import React, { useState, useEffect } from 'react';

export function DataFetcher() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    let isMounted = true;
    
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        const result = await response.json();
        if (isMounted) {
          setData(result);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };
    
    fetchData();
    
    // クリーンアップ関数
    return () => {
      isMounted = false;
    };
  }, []); // 空配列でマウント時のみ実行
  
  if (loading) return <div>読み込み中...</div>;
  return <div>{JSON.stringify(data)}</div>;
}

5. 複数のuseEffectの利用

複数の副作用を分離して管理する例です。

import React, { useState, useEffect } from 'react';

export function MultiEffects() {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  const [logs, setLogs] = useState([]);
  
  // 効果A: countAの変化をログに記録
  useEffect(() => {
    setLogs(prev => [...prev, `効果A: ${countA}`]);
  }, [countA]);
  
  // 効果B: countBの変化をログに記録
  useEffect(() => {
    setLogs(prev => [...prev, `効果B: ${countB}`]);
  }, [countB]);
  
  // 効果C: マウント時に一度だけ実行
  useEffect(() => {
    setLogs(prev => [...prev, '初期マウント']);
  }, []);
  
  return (
    <div>
      <button onClick={() => setCountA(c => c + 1)}>増加A</button>
      <button onClick={() => setCountB(c => c + 1)}>増加B</button>
      <ul>{logs.map((log, i) => <li key={i}>{log}</li>)}</ul>
    </div>
  );
}

6. useContextによる状態共有

Contextを使ったグローバル状態の共有例です。

import React, { createContext, useContext } from 'react';

const AuthContext = createContext(null);
const ThemeContext = createContext({ theme: 'light' });

export function AppProviders({ children }) {
  const auth = { user: 'takashi', isLoggedIn: true };
  
  return (
    <AuthContext.Provider value={auth}>
      <ThemeContext.Provider value={{ theme: 'dark' }}>
        {children}
      </ThemeContext.Provider>
    </AuthContext.Provider>
  );
}

export function Dashboard() {
  const auth = useContext(AuthContext);
  const theme = useContext(ThemeContext);
  
  return (
    <div style={{ background: theme.theme === 'dark' ? '#333' : '#fff' }}>
      ユーザー: {auth.user}
    </div>
  );
}

7. useReducerによる状態管理

複雑な状態ロジックをuseReducerで整理する例です。

import React, { useReducer } from 'react';

const initialState = { count: 0, step: 1 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'setStep':
      return { ...state, step: action.payload };
    case 'reset':
      return initialState;
    default:
      return state;
  }
}

export function CounterWithReducer() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <div>
      <h3>カウント: {state.count}</h3>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <input 
        type="number" 
        value={state.step}
        onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
      />
      <button onClick={() => dispatch({ type: 'reset' })}>リセット</button>
    </div>
  );
}

8. useCallbackによるパフォーマンス最適化

関数の再生成を防ぎ、不要な再レンダリングを抑制する例です。

import React, { useState, useCallback, memo } from 'react';

export function ParentComponent() {
  const [value, setValue] = useState(0);
  const [flag, setFlag] = useState(false);
  
  // valueが変わらない限り同じ関数インスタンスを維持
  const handleIncrement = useCallback(() => {
    setValue(v => v + 1);
  }, [value]);
  
  // 毎回新しい関数が生成される
  const handleToggle = () => {
    setFlag(f => !f);
  };
  
  return (
    <div>
      <MemoizedButton onClick={handleIncrement} label="増加" />
      <MemoizedButton onClick={handleToggle} label="切替" />
      <div>値: {value}, フラグ: {String(flag)}</div>
    </div>
  );
}

const MemoizedButton = memo(function MemoizedButton({ onClick, label }) {
  console.log(`レンダリング: ${label}`);
  return <button onClick={onClick}>{label}</button>;
});

9. useMemoによる値のメモ化

高コストな計算結果をメモ化する例です。

import React, { useState, useMemo } from 'react';

function expensiveCalculation(num) {
  console.log('計算実行...');
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += i * num;
  }
  return result;
}

export function CalculationComponent() {
  const [input, setInput] = useState(1);
  const [other, setOther] = useState('');
  
  // inputが変わらない限り計算結果を再利用
  const result = useMemo(() => {
    return expensiveCalculation(input);
  }, [input]);
  
  return (
    <div>
      <input 
        type="number" 
        value={input}
        onChange={e => setInput(Number(e.target.value))}
      />
      <input 
        value={other}
        onChange={e => setOther(e.target.value)}
      />
      <div>結果: {result}</div>
    </div>
  );
}

10. useRefの活用

DOM要素への参照と過去の値の保持を行う例です。

import React, { useRef, useEffect, useState } from 'react';

export function FocusManager() {
  const inputRef = useRef(null);
  const prevValueRef = useRef('');
  const [text, setText] = useState('');
  
  useEffect(() => {
    // テキスト変更時に前の値を保持
    prevValueRef.current = text;
  }, [text]);
  
  const handleFocus = () => {
    inputRef.current?.focus();
  };
  
  return (
    <div>
      <input
        ref={inputRef}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={handleFocus}>フォーカス</button>
      <div>現在: {text}</div>
      <div>以前: {prevValueRef.current}</div>
    </div>
  );
}

11. forwardRefの使用

子コンポーネントにrefを渡す実装例です。

import React, { forwardRef, useRef } from 'react';

const CustomInput = forwardRef((props, ref) => {
  return (
    <div>
      <label>{props.label}</label>
      <input ref={ref} {...props} />
    </div>
  );
});

export function FormContainer() {
  const nameRef = useRef(null);
  
  const handleSubmit = () => {
    nameRef.current?.focus();
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <CustomInput ref={nameRef} label="名前" />
      <button type="submit">送信</button>
    </form>
  );
}

12. useImperativeHandleによる制御

親コンポーネントに公開するメソッドを制限する例です。

import React, { forwardRef, useRef, useImperativeHandle } from 'react';

const LimitedInput = forwardRef((props, ref) => {
  const internalRef = useRef(null);
  
  useImperativeHandle(ref, () => ({
    // 特定のメソッドのみを公開
    focusInput: () => {
      internalRef.current?.focus();
    },
    getValue: () => internalRef.current?.value,
    clear: () => {
      if (internalRef.current) {
        internalRef.current.value = '';
      }
    }
  }), []);
  
  return <input ref={internalRef} {...props} />;
});

export function ControlledForm() {
  const inputRef = useRef(null);
  
  const handleAction = () => {
    inputRef.current?.focusInput();
    console.log(inputRef.current?.getValue());
  };
  
  return (
    <div>
      <LimitedInput ref={inputRef} />
      <button onClick={handleAction}>アクション</button>
    </div>
  );
}

13. useLayoutEffectの特性

ブラウザ描画前に同期処理を行う例です。

import React, { useState, useLayoutEffect, useEffect } from 'react';

export function LayoutDemo() {
  const [width, setWidth] = useState(0);
  
  useLayoutEffect(() => {
    // DOMへの同期処理(描画前)
    const element = document.getElementById('measure');
    if (element) {
      const rect = element.getBoundingClientRect();
      setWidth(rect.width);
    }
  }, []);
  
  useEffect(() => {
    // 非同期処理(描画後)
    console.log('描画完了後:', width);
  }, [width]);
  
  return (
    <div>
      <div id="measure">測定要素</div>
      <div>幅: {width}px</div>
    </div>
  );
}

14. カスタムフックの作成

再利用可能なロジックをカスタムフックとして抽出する例です。

// カスタムフック: トグル機能
function useToggle(initial = false) {
  const [state, setState] = useState(initial);
  const toggle = useCallback(() => setState(s => !s), []);
  return [state, toggle];
}

// カスタムフック: ローカルストレージ
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const stored = window.localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });
  
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (e) {
      console.error('保存失敗:', e);
    }
  }, [key, value]);
  
  return [value, setValue];
}

// 使用例
export function CustomHookDemo() {
  const [visible, toggleVisible] = useToggle(true);
  const [username, setUsername] = useLocalStorage('username', 'ゲスト');
  
  return (
    <div>
      {visible && <div>表示要素</div>}
      <button onClick={toggleVisible}>表示切替</button>
      
      <input
        value={username}
        onChange={e => setUsername(e.target.value)}
      />
    </div>
  );
}

15. Fiberアーキテクチャの基礎

Reactの再描画制御メカニズムについての理解です。

requestIdleCallbackを使用した非優先処理の例:

function performLowPriorityTask() {
  // ブラウザがアイドル状態になったら実行
  requestIdleCallback((deadline) => {
    while (deadline.timeRemaining() > 0) {
      // 小さなタスクを分割実行
      processChunk();
    }
  }, { timeout: 2000 });
}

16. Hooksの内部構造

Hooksが連結リストとして管理される仕組みの理解です。

Hooksは呼び出し順序に依存しており、条件分岐内での使用は禁止されています。これはReactが各フックの状態を配列のインデックスで追跡するためです。

// 正しい例
function CorrectComponent() {
  const [a, setA] = useState(0); // インデックス0
  const [b, setB] = useState(1); // インデックス1
  
  if (a > 0) {
    // ロジックのみ、フックなし
  }
  
  return <div />;
}

// 誤った例(動作しない)
function IncorrectComponent() {
  const [a, setA] = useState(0);
  
  if (a > 0) {
    // エラー: 条件内でフック使用
    const [b, setB] = useState(1);
  }
  
  return <div />;
}

タグ: react-router-dom React Hooks useState useEffect useContext

6月11日 22:35 投稿