mcpField
R
react-development

react-development

github.com/manutej開発62465 回インストール40 週間前に更新
リポジトリ

react-development について

React の公式ドキュメントに基づく、開発者向けのガイドです。インタラクティブなアプリの構築に必要なコンポーネント、フック、状態管理、カスタムフック、性能改善を扱います。

主な機能

  1. 01コンポーネント構成と JSX
  2. 02フックと副作用
  3. 03Context と Reducer による状態管理
  4. 04再利用可能なカスタムフック
  5. 05メモ化、遅延読み込み、コード分割

活用シーン

  1. 01React の SPA 構築
  2. 02フォームとデータ取得の実装
  3. 03クラスコンポーネントの関数化
  4. 04大規模リストやアプリの性能改善

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/manutej/luxor-claude-marketplace --skill react-development

インストール後、Claude Code で/react-developmentを入力して読み込まれたか確認します。

同梱ファイルEXAMPLES.mdREADME.md

詳細

React 開発スキル

このスキルでは、react.dev の公式 React ドキュメントに基づき、フック、コンポーネント、状態管理、コンテキスト、エフェクト、パフォーマンス最適化の手法を使って、最新の React アプリケーションを構築するための包括的なガイダンスを提供します。

このスキルを使用する場面

次のような場合に、このスキルを使用してください。

  • React でシングルページアプリケーション(SPA)を構築する
  • 再利用可能な UI コンポーネントやコンポーネントライブラリを作成する
  • フックとコンテキストを使って複雑なアプリケーションの状態を管理する
  • フォーム、データ取得、副作用を実装する
  • React アプリケーションのパフォーマンスを最適化する
  • 動的データを使ったインタラクティブなユーザーインターフェースを構築する
  • クラスコンポーネントをフックを使った関数コンポーネントに移行する
  • 外部ライブラリを使わずにグローバルな状態管理を実装する
  • ロジックを再利用するためのカスタムフックを作成する
  • アクセシブルで高パフォーマンスな Web アプリケーションを構築する

コアコンセプト

コンポーネント

コンポーネントは React アプリケーションを構成する基本要素です。UI を独立した再利用可能なパーツに分割できます。

関数コンポーネント(モダンなアプローチ):

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

// Arrow function syntax
const Greeting = ({ name, age }) => {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
    </div>
  );
};

コンポーネントの合成:

function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}
JSX

JSX は HTML に似た JavaScript の構文拡張です。React 要素を生成します。

JSX の基本:

// Embedding expressions
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

// JSX attributes
const image = <img src={user.avatarUrl} alt={user.name} />;

// JSX children
const container = (
  <div>
    <h1>Welcome</h1>
    <p>Get started with React</p>
  </div>
);

// Conditional rendering
const greeting = (
  <div>
    {isLoggedIn ? <UserGreeting /> : <GuestGreeting />}
  </div>
);

// Lists and keys
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>{number}</li>
);
Props

Props は React コンポーネントに渡される引数です。HTML 属性を介してコンポーネントに渡されます。

Props の受け渡しと使用:

function Product({ name, price, inStock }) {
  return (
    <div className="product">
      <h3>{name}</h3>
      <p>${price}</p>
      {inStock ? <span>In Stock</span> : <span>Out of Stock</span>}
    </div>
  );
}

// Usage
<Product name="Laptop" price={999} inStock={true} />

子要素を含む Props:

function Card({ title, children }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">
        {children}
      </div>
    </div>
  );
}

// Usage
<Card title="Welcome">
  <p>This is the card content</p>
  <button>Click me</button>
</Card>

デフォルト Props:

function Button({ text = 'Click me', variant = 'primary' }) {
  return <button className={variant}>{text}</button>;
}
状態

状態はコンポーネントのメモリです。コンポーネントが情報を記憶し、ユーザーの操作に応答できるようにします。

コンポーネントのローカル状態:

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

React フック

フックを使うと、関数コンポーネントで状態やその他の React 機能を利用できます。

useState

useState フックを使うと、関数コンポーネントに状態を追加できます。

基本的な使用方法:

import { useState } from 'react';

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({ name, email });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Name"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
        type="email"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

オブジェクトを使った状態:

function UserProfile() {
  const [user, setUser] = useState({
    name: '',
    age: 0,
    email: ''
  });

  const updateField = (field, value) => {
    setUser(prev => ({
      ...prev,
      [field]: value
    }));
  };

  return (
    <div>
      <input
        value={user.name}
        onChange={(e) => updateField('name', e.target.value)}
      />
      <input
        type="number"
        value={user.age}
        onChange={(e) => updateField('age', parseInt(e.target.value))}
      />
      <input
        type="email"
        value={user.email}
        onChange={(e) => updateField('email', e.target.value)}
      />
    </div>
  );
}

配列を使った状態:

function TodoList() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');

  const addTodo = () => {
    setTodos(prev => [...prev, { id: Date.now(), text: input }]);
    setInput('');
  };

  const removeTodo = (id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  };

  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            {todo.text}
            <button onClick={() => removeTodo(todo.id)}>Delete</button>
          </li>
        ))}
      </ul>
    </div>
  );
}
useEffect

useEffect フックを使うと、関数コンポーネントで副作用を実行できます。

基本的な副作用:

import { useState, useEffect } from 'react';

function DocumentTitle() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

データ取得:

function UserData({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchUser() {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();

        if (!cancelled) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchUser();

    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return <div>{user?.name}</div>;
}

イベントリスナーとクリーンアップ:

function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }

    window.addEventListener('resize', handleResize);

    // Cleanup function
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []); // Empty dependency array = run once on mount

  return <div>{size.width} x {size.height}</div>;
}

タイマーとインターバル:

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  useEffect(() => {
    if (!isRunning) return;

    const interval = setInterval(() => {
      setSeconds(s => s + 1);
    }, 1000);

    return () => clearInterval(interval);
  }, [isRunning]);

  return (
    <div>
      <p>Seconds: {seconds}</p>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? 'Pause' : 'Start'}
      </button>
      <button onClick={() => setSeconds(0)}>Reset</button>
    </div>
  );
}
useContext

useContext フックを使うと、コンポーネントからコンテキストを読み取り、変更を購読できます。

コンテキストの作成と使用:

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

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar />
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);

  return (
    <button className={theme}>
      I am styled by {theme} theme
    </button>
  );
}

複数のコンテキスト:

const ThemeContext = createContext('light');
const UserContext = createContext(null);

function App() {
  const [theme, setTheme] = useState('light');
  const [currentUser, setCurrentUser] = useState({ name: 'John', role: 'admin' });

  return (
    <ThemeContext.Provider value={theme}>
      <UserContext.Provider value={currentUser}>
        <Dashboard />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

function Dashboard() {
  const theme = useContext(ThemeContext);
  const user = useContext(UserContext);

  return (
    <div className={theme}>
      <h1>Welcome, {user.name}</h1>
      <p>Role: {user.role}</p>
    </div>
  );
}
useReducer

useReducer フックは、複雑な状態ロジックを管理する際に useState の代わりとして使用できます。

基本的なリデューサーパターン:

import { useReducer } from 'react';

function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

複雑な状態管理(Context7 のタスクリストパターン):

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

function TaskApp() {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask onAddTask={handleAddTask} />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];
useMemo

useMemo フックを使うと、コストの高い計算結果をキャッシュできます。

コストの高い計算のメモ化:

import { useMemo, useState } from 'react';

function ProductList({ products, category }) {
  const [sortOrder, setSortOrder] = useState('asc');

  const filteredAndSortedProducts = useMemo(() => {
    console.log('Filtering and sorting products...');

    const filtered = products.filter(p => p.category === category);

    return filtered.sort((a, b) => {
      if (sortOrder === 'asc') {
        return a.price - b.price;
      }
      return b.price - a.price;
    });
  }, [products, category, sortOrder]);

  return (
    <div>
      <button onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}>
        Sort: {sortOrder}
      </button>
      <ul>
        {filteredAndSortedProducts.map(product => (
          <li key={product.id}>{product.name} - ${product.price}</li>
        ))}
      </ul>
    </div>
  );
}

オブジェクトの再生成を防ぐ:

function SearchResults({ query }) {
  const searchOptions = useMemo(() => ({
    query,
    limit: 10,
    caseSensitive: false
  }), [query]);

  // searchOptions object only recreated when query changes
  const results = useSearch(searchOptions);

  return <ResultsList results={results} />;
}
useCallback

useCallback フックを使うと、再レンダーの間、関数定義をキャッシュできます。

イベントハンドラーのメモ化:

import { useCallback, useState } from 'react';

function ProductPage({ productId }) {
  const [items, setItems] = useState([]);

  const handleAddToCart = useCallback(() => {
    setItems(prevItems => [...prevItems, productId]);
  }, [productId]);

  return <AddToCartButton onAdd={handleAddToCart} />;
}

// Memoized child component
const AddToCartButton = memo(({ onAdd }) => {
  console.log('Button rendered');
  return <button onClick={onAdd}>Add to Cart</button>;
});

子コンポーネントの最適化:

function TodoList() {
  const [todos, setTodos] = useState(initialTodos);

  const handleToggle = useCallback((id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  }, []);

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </ul>
  );
}
useRef

useRef フックを使うと、レンダリングに必要のない値を参照できます。

DOM 要素へのアクセス:

import { useRef } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>Focus input</button>
    </>
  );
}

変更可能な値の保存:

function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef(null);

  function handleStart() {
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 10);
  }

  function handleStop() {
    clearInterval(intervalRef.current);
  }

  return (
    <div>
      <p>Time: {(time / 100).toFixed(2)}s</p>
      <button onClick={handleStart}>Start</button>
      <button onClick={handleStop}>Stop</button>
    </div>
  );
}

動画プレーヤーの制御(Context7 パターン):

import { useRef, useState } from 'react';

function VideoPlayer({ src, isPlaying }) {
  const ref = useRef(null);

  useEffect(() => {
    if (isPlaying) {
      ref.current.play();
    } else {
      ref.current.pause();
    }
  }, [isPlaying]);

  return <video ref={ref} src={src} loop playsInline />;
}

function App() {
  const [isPlaying, setIsPlaying] = useState(false);

  return (
    <>
      <button onClick={() => setIsPlaying(!isPlaying)}>
        {isPlaying ? 'Pause' : 'Play'}
      </button>
      <VideoPlayer
        isPlaying={isPlaying}
        src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
      />
    </>
  );
}

状態管理パターン

ローカル状態パターン

共有する必要のない、コンポーネント固有のデータにはローカル状態を使用します。

function LoginForm() {
  const [formData, setFormData] = useState({
    username: '',
    password: '',
    rememberMe: false
  });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (field) => (e) => {
    const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
    setFormData(prev => ({ ...prev, [field]: value }));
  };

  const validate = () => {
    const newErrors = {};
    if (!formData.username) newErrors.username = 'Required';
    if (!formData.password) newErrors.password = 'Required';
    return newErrors;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const newErrors = validate();

    if (Object.keys(newErrors).length > 0) {
      setErrors(newErrors);
      return;
    }

    setIsSubmitting(true);
    try {
      await login(formData);
    } catch (error) {
      setErrors({ submit: error.message });
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.username}
        onChange={handleChange('username')}
        placeholder="Username"
      />
      {errors.username && <span>{errors.username}</span>}

      <input
        type="password"
        value={formData.password}
        onChange={handleChange('password')}
        placeholder="Password"
      />
      {errors.password && <span>{errors.password}</span>}

      <label>
        <input
          type="checkbox"
          checked={formData.rememberMe}
          onChange={handleChange('rememberMe')}
        />
        Remember me
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Logging in...' : 'Log in'}
      </button>

      {errors.submit && <div>{errors.submit}</div>}
    </form>
  );
}
Context API パターン

テーマ、ユーザー認証、設定など、グローバルに共有する状態や広く共有される状態には Context を使用します。

プロバイダーを使ったテーマコンテキスト:

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

const ThemeContext = createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Usage
function App() {
  return (
    <ThemeProvider>
      <Page />
    </ThemeProvider>
  );
}

function Page() {
  const { theme, toggleTheme } = useTheme();

  return (
    <div className={`page-${theme}`}>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}
Reducer + Context パターン(Context7 のベストプラクティス)

useReducer と Context を組み合わせて、スケーラブルな状態管理を実現します。

Reducer + Context によるタスク管理:

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

// Context for tasks data
const TasksContext = createContext(null);

// Context for dispatch function
const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

export function useTasks() {
  return useContext(TasksContext);
}

export function useTasksDispatch() {
  return useContext(TasksDispatchContext);
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        }
        return t;
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  { id: 0, text: 'Philosopher's Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

// Component using the pattern
function AddTask() {
  const [text, setText] = useState('');
  const dispatch = useTasksDispatch();

  return (
    <>
      <input
        placeholder="Add task"
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button onClick={() => {
        setText('');
        dispatch({
          type: 'added',
          id: nextId++,
          text: text,
        });
      }}>Add</button>
    </>
  );
}

function TaskList() {
  const tasks = useTasks();
  return (
    <ul>
      {tasks.map(task => (
        <Task key={task.id} task={task} />
      ))}
    </ul>
  );
}

function Task({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  const dispatch = useTasksDispatch();

  let taskContent;
  if (isEditing) {
    taskContent = (
      <>
        <input
          value={task.text}
          onChange={e => {
            dispatch({
              type: 'changed',
              task: {
                ...task,
                text: e.target.value
              }
            });
          }} />
        <button onClick={() => setIsEditing(false)}>
          Save
        </button>
      </>
    );
  } else {
    taskContent = (
      <>
        {task.text}
        <button onClick={() => setIsEditing(true)}>
          Edit
        </button>
      </>
    );
  }

  return (
    <label>
      <input
        type="checkbox"
        checked={task.done}
        onChange={e => {
          dispatch({
            type: 'changed',
            task: {
              ...task,
              done: e.target.checked
            }
          });
        }}
      />
      {taskContent}
      <button onClick={() => {
        dispatch({
          type: 'deleted',
          id: task.id
        });
      }}>
        Delete
      </button>
    </label>
  );
}

let nextId = 3;

カスタムフック

カスタムフックを使うと、コンポーネントのロジックを再利用可能な関数として切り出せます。

基本的なカスタムフック
import { useState, useEffect } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });

  useEffect(() => {
    function handleResize() {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    }

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// Usage
function Component() {
  const { width, height } = useWindowSize();
  return <div>{width} x {height}</div>;
}
オンライン状態フック(Context7 パターン)
import { useState, useEffect } from 'react';

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }

    function handleOffline() {
      setIsOnline(false);
    }

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOnline;
}

// Usage
function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
フォームフック
function useForm(initialValues, onSubmit) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));
    // Clear error when user starts typing
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: null }));
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);

    try {
      await onSubmit(values);
    } catch (error) {
      setErrors({ submit: error.message });
    } finally {
      setIsSubmitting(false);
    }
  };

  const reset = () => {
    setValues(initialValues);
    setErrors({});
  };

  return {
    values,
    errors,
    isSubmitting,
    handleChange,
    handleSubmit,
    setErrors,
    reset
  };
}

// Usage
function ContactForm() {
  const { values, errors, isSubmitting, handleChange, handleSubmit } = useForm(
    { name: '', email: '', message: '' },
    async (data) => {
      await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(data)
      });
    }
  );

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={values.name}
        onChange={(e) => handleChange('name', e.target.value)}
      />
      {errors.name && <span>{errors.name}</span>}

      <input
        value={values.email}
        onChange={(e) => handleChange('email', e.target.value)}
      />
      {errors.email && <span>{errors.email}</span>}

      <textarea
        value={values.message}
        onChange={(e) => handleChange('message', e.target.value)}
      />
      {errors.message && <span>{errors.message}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}
Fetch フック
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      try {
        setLoading(true);
        const response = await fetch(url, options);

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();

        if (!cancelled) {
          setData(result);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err.message);
          setData(null);
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    }

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url, JSON.stringify(options)]);

  return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
  const { data, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return <div>{data.name}</div>;
}
ローカルストレージフック
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// Usage
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [fontSize, setFontSize] = useLocalStorage('fontSize', 16);

  return (
    <div>
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>

      <input
        type="number"
        value={fontSize}
        onChange={(e) => setFontSize(parseInt(e.target.value))}
      />
    </div>
  );
}

パフォーマンス最適化

React.memo

コンポーネントをメモ化して、不要な再レンダーを防ぎます。

import { memo } from 'react';

const ExpensiveComponent = memo(function ExpensiveComponent({ data, onAction }) {
  console.log('Rendering expensive component');

  return (
    <div>
      {data.map(item => (
        <div key={item.id}>{item.name}</div>
      ))}
      <button onClick={onAction}>Action</button>
    </div>
  );
});

// With custom comparison
const CustomMemoComponent = memo(
  function Component({ user }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return prevProps.user.id === nextProps.user.id;
  }
);
遅延読み込み

初期バンドルサイズを削減するため、必要に応じてコンポーネントを読み込みます。

import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function App() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

// Lazy loading with routes
function Dashboard() {
  const [showAdmin, setShowAdmin] = useState(false);

  return (
    <div>
      <button onClick={() => setShowAdmin(true)}>
        Show Admin Panel
      </button>

      {showAdmin && (
        <Suspense fallback={<Spinner />}>
          <AdminPanel />
        </Suspense>
      )}
    </div>
  );
}
コード分割

パフォーマンスを向上させるため、コードを小さなチャンクに分割します。

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading page...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}
仮想スクロール

大きなリストでは、表示されている項目のみを描画します。

function VirtualList({ items, height, itemHeight }) {
  const [scrollTop, setScrollTop] = useState(0);

  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(
    startIndex + Math.ceil(height / itemHeight),
    items.length
  );

  const visibleItems = items.slice(startIndex, endIndex);

  const offsetY = startIndex * itemHeight;
  const totalHeight = items.length * itemHeight;

  return (
    <div
      style={{ height, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div
              key={startIndex + index}
              style={{ height: itemHeight }}
            >
              {item}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Context7の調査に基づくベストプラクティス

1. 依存配列を適切に指定する

useEffect、useMemo、useCallbackでは、必ずすべての依存関係を含めてください。

// ❌ Bad - missing dependencies
useEffect(() => {
  fetchData(userId);
}, []);

// ✅ Good - all dependencies included
useEffect(() => {
  fetchData(userId);
}, [userId]);
2. クリーンアップ関数

メモリリークを防ぐため、副作用は必ずクリーンアップしてください。

useEffect(() => {
  const subscription = api.subscribe(id);

  return () => {
    subscription.unsubscribe();
  };
}, [id]);
3. 関心事を分離する

再レンダリングを最適化するため、データとディスパッチ用のコンテキストを分けてください。

// ✅ Good - separate contexts
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);

// Components that only dispatch won't re-render when tasks change
function AddTask() {
  const dispatch = useTasksDispatch(); // No re-render when tasks change
  // ...
}
4. インラインでのオブジェクト生成を避ける

不要な再レンダリングを防ぐため、useMemoまたはuseCallbackを使用してください。

// ❌ Bad - new object on every render
<Component style={{ margin: 10 }} />

// ✅ Good - memoized object
const style = useMemo(() => ({ margin: 10 }), []);
<Component style={style} />
5. 状態更新関数

新しい状態が以前の状態に依存する場合は、更新関数を使用してください。

// ❌ Bad - may use stale state
setCount(count + 1);

// ✅ Good - uses current state
setCount(prevCount => prevCount + 1);
6. 複雑なロジックを切り出す

複雑な状態ロジックは、reducerまたはカスタムフックに移してください。

// ✅ Good - complex logic in reducer
function cartReducer(state, action) {
  switch (action.type) {
    case 'add_item':
      // Complex logic here
      return newState;
    case 'remove_item':
      // Complex logic here
      return newState;
    default:
      return state;
  }
}
7. リスト項目にkey propsを指定する

リスト項目には、必ず一意のキーを指定してください。

// ❌ Bad - using index as key
{items.map((item, index) => <div key={index}>{item}</div>)}

// ✅ Good - using unique ID
{items.map(item => <div key={item.id}>{item.text}</div>)}
8. 制御コンポーネント

フォーム入力には、制御コンポーネントを優先して使用してください。

function Form() {
  const [value, setValue] = useState('');

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}
9. エラーバウンダリ

エラーを捕捉し、適切に処理するために、エラーバウンダリを実装してください。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>
10. PropTypesまたはTypeScript

型チェックには、PropTypesまたはTypeScriptを使用してください。

import PropTypes from 'prop-types';

function User({ name, age, email }) {
  return <div>{name} ({age})</div>;
}

User.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired,
  email: PropTypes.string
};

User.defaultProps = {
  email: '[email protected]'
};

追加の例

例1:複数ステップのフォーム
function MultiStepForm() {
  const [step, setStep] = useState(1);
  const [formData, setFormData] = useState({
    personalInfo: {},
    address: {},
    preferences: {}
  });

  const updateFormData = (section, data) => {
    setFormData(prev => ({
      ...prev,
      [section]: { ...prev[section], ...data }
    }));
  };

  const nextStep = () => setStep(s => s + 1);
  const prevStep = () => setStep(s => s - 1);

  return (
    <div>
      {step === 1 && (
        <PersonalInfoStep
          data={formData.personalInfo}
          onNext={(data) => {
            updateFormData('personalInfo', data);
            nextStep();
          }}
        />
      )}
      {step === 2 && (
        <AddressStep
          data={formData.address}
          onNext={(data) => {
            updateFormData('address', data);
            nextStep();
          }}
          onPrev={prevStep}
        />
      )}
      {step === 3 && (
        <PreferencesStep
          data={formData.preferences}
          onSubmit={(data) => {
            updateFormData('preferences', data);
            submitForm({ ...formData, preferences: data });
          }}
          onPrev={prevStep}
        />
      )}
    </div>
  );
}
例2:無限スクロール
function InfiniteScroll() {
  const [items, setItems] = useState([]);
  const [page, setPage] = useState(1);
  const [loading, setLoading] = useState(false);
  const [hasMore, setHasMore] = useState(true);

  const loadMore = useCallback(async () => {
    if (loading || !hasMore) return;

    setLoading(true);
    try {
      const newItems = await fetchItems(page);
      setItems(prev => [...prev, ...newItems]);
      setHasMore(newItems.length > 0);
      setPage(p => p + 1);
    } finally {
      setLoading(false);
    }
  }, [page, loading, hasMore]);

  useEffect(() => {
    const handleScroll = () => {
      if (
        window.innerHeight + window.scrollY >=
        document.documentElement.scrollHeight - 500
      ) {
        loadMore();
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [loadMore]);

  return (
    <div>
      {items.map(item => <Item key={item.id} data={item} />)}
      {loading && <div>Loading...</div>}
      {!hasMore && <div>No more items</div>}
    </div>
  );
}
例3:デバウンス検索
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (debouncedSearchTerm) {
      searchAPI(debouncedSearchTerm).then(setResults);
    } else {
      setResults([]);
    }
  }, [debouncedSearchTerm]);

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}
例4:Portalを使ったモーダル
import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="close-button" onClick={onClose}>×</button>
        {children}
      </div>
    </div>,
    document.body
  );
}

// Usage
function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
      <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
        <h2>Modal Content</h2>
        <p>This is a modal dialog</p>
      </Modal>
    </div>
  );
}
例5:ドラッグ&ドロップ
function DragDropList() {
  const [items, setItems] = useState([
    { id: 1, text: 'Item 1' },
    { id: 2, text: 'Item 2' },
    { id: 3, text: 'Item 3' }
  ]);

  const [draggedItem, setDraggedItem] = useState(null);

  const handleDragStart = (item) => {
    setDraggedItem(item);
  };

  const handleDragOver = (e) => {
    e.preventDefault();
  };

  const handleDrop = (targetItem) => {
    if (!draggedItem || draggedItem.id === targetItem.id) return;

    const draggedIndex = items.findIndex(i => i.id === draggedItem.id);
    const targetIndex = items.findIndex(i => i.id === targetItem.id);

    const newItems = [...items];
    newItems.splice(draggedIndex, 1);
    newItems.splice(targetIndex, 0, draggedItem);

    setItems(newItems);
    setDraggedItem(null);
  };

  return (
    <ul>
      {items.map(item => (
        <li
          key={item.id}
          draggable
          onDragStart={() => handleDragStart(item)}
          onDragOver={handleDragOver}
          onDrop={() => handleDrop(item)}
        >
          {item.text}
        </li>
      ))}
    </ul>
  );
}

まとめ

このReact開発スキルでは、次の内容を扱います。

  1. コア概念:コンポーネント、JSX、Props、State
  2. 必須のフック:useState、useEffect、useContext、useReducer、useMemo、useCallback、useRef
  3. 状態管理:ローカル状態、Context API、Reducer + Contextパターン
  4. カスタムフック:再利用可能なロジックを切り出すパターン
  5. パフォーマンス:メモ化、遅延読み込み、コード分割、仮想スクロール
  6. ベストプラクティス:適切な依存関係、クリーンアップ、関心事の分離など、Context7の調査に基づく内容
  7. 実践的な例:フォーム、無限スクロール、検索、モーダル、ドラッグ&ドロップ

パターンと例は、公式Reactドキュメント(信頼度スコア:10)に基づいており、関数コンポーネントとフックを中心とした最新のReact開発手法を示しています。

コメント