Reactコンポーネントとは?関数型とクラス型の違いを解説

Reactコンポーネントの基本概念から関数型・クラス型の違いまで詳しく解説。使い分けのポイントとHooksの活用方法を実践的なサンプルコードとともに紹介します。

Learning Next 運営
52 分で読めます

みなさん、Reactを学び始めて混乱したことはありませんか?

「コンポーネントって何?」 「関数型とクラス型の違いは?」 「どちらを使えばいいの?」

そんな疑問を持ったことはありませんか?

実は、Reactコンポーネントの理解はReact開発の基本中の基本です。 この記事では、コンポーネントの基本概念から関数型・クラス型の違いまで詳しく解説します。

使い分けのポイントとHooksの活用方法を、実践的なサンプルコードとともに学んでいきましょう。

Reactコンポーネントって何?

まず、コンポーネントの基本的な考え方から理解しましょう。

コンポーネントの基本概念

Reactコンポーネントは、UIの部品のようなものです。 HTMLの要素のように使える、自作のUIパーツを作ることができます。

簡単に言うと、レゴブロックのようなものです。 小さな部品を組み合わせて、大きなアプリケーションを作ります。

基本的なコンポーネントの例

実際にコンポーネントを見てみましょう。

const Welcome = (props) => {
  return <h1>こんにちは、{props.name}さん!</h1>;
};

このWelcomeがコンポーネントです。

propsという引数を受け取って、JSXを返しています。 関数のような働きをしているのがわかりますね。

実際に使うときは、こんな感じになります。

const App = () => {
  return (
    <div>
      <Welcome name="太郎" />
      <Welcome name="花子" />
      <Welcome name="次郎" />
    </div>
  );
};

同じコンポーネントに違うnameを渡して、3つの挨拶を表示しています。

これが再利用性というコンポーネントの大きな特徴です。

コンポーネントの4つの特徴

コンポーネントには、以下のような特徴があります。

  • 再利用可能: 何度でも使える
  • 独立性: 他の部分に影響しない
  • 組み合わせ可能: 複数組み合わせて使える
  • カプセル化: 内部の処理を隠せる

これらの特徴により、効率的な開発が可能になります。

より実用的な例

もう少し実用的なボタンコンポーネントを見てみましょう。

const Button = ({ children, onClick, variant = 'primary' }) => {
  const buttonStyles = {
    primary: { backgroundColor: '#007bff', color: 'white' },
    secondary: { backgroundColor: '#6c757d', color: 'white' },
    danger: { backgroundColor: '#dc3545', color: 'white' }
  };

  return (
    <button 
      onClick={onClick}
      style={{
        padding: '10px 20px',
        border: 'none',
        borderRadius: '4px',
        cursor: 'pointer',
        ...buttonStyles[variant]
      }}
    >
      {children}
    </button>
  );
};

このコンポーネントは、3つのパラメータを受け取ります。

  • children: ボタンの中身
  • onClick: クリック時の処理
  • variant: ボタンの種類

使い方は以下のようになります。

const ButtonExample = () => {
  return (
    <div>
      <Button onClick={() => alert('保存')}>保存</Button>
      <Button variant="secondary" onClick={() => alert('キャンセル')}>
        キャンセル
      </Button>
      <Button variant="danger" onClick={() => alert('削除')}>
        削除
      </Button>
    </div>
  );
};

同じコンポーネントでも、パラメータを変えることで見た目と動作が変わります。

「こんなに簡単に再利用できるの?」と思うかもしれませんが、これがコンポーネントの力なんです。

関数型コンポーネントの基本

関数型コンポーネントは、JavaScriptの関数として定義します。

基本的な書き方

最もシンプルな関数型コンポーネントを見てみましょう。

const Greeting = ({ name, age }) => {
  return (
    <div>
      <h2>プロフィール</h2>
      <p>名前: {name}</p>
      <p>年齢: {age}歳</p>
    </div>
  );
};

アロー関数を使って定義しています。

{ name, age }の部分は、分割代入という書き方です。 props.nameprops.ageと書かなくても、直接使えます。

通常の関数での書き方

アロー関数を使わない書き方もあります。

function UserCard({ user }) {
  return (
    <div style={{ 
      border: '1px solid #ddd', 
      padding: '15px', 
      margin: '10px',
      borderRadius: '8px'
    }}>
      <h3>{user.name}</h3>
      <p>メール: {user.email}</p>
      <p>部署: {user.department}</p>
    </div>
  );
}

functionキーワードを使った書き方です。

どちらを使っても結果は同じですが、アロー関数の方が短く書けるので人気です。

実際の使用例

作ったコンポーネントを使ってみましょう。

const UserList = () => {
  const users = [
    { id: 1, name: '田中太郎', email: 'tanaka@example.com', department: '開発部' },
    { id: 2, name: '佐藤花子', email: 'sato@example.com', department: '営業部' }
  ];

  return (
    <div>
      <h1>ユーザー一覧</h1>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </div>
  );
};

mapメソッドを使って、ユーザーの配列をUserCardコンポーネントの配列に変換しています。

key={user.id}は、Reactが効率的に描画するために必要です。

関数型コンポーネントは、シンプルで読みやすいのが特徴です。

useStateを使った状態管理

関数型コンポーネントでは、useStateを使って状態を管理します。

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  const increment = () => setCount(count + step);
  const decrement = () => setCount(count - step);
  const reset = () => setCount(0);

  return (
    <div style={{ padding: '20px', textAlign: 'center' }}>
      <h2>カウンター</h2>
      <p style={{ fontSize: '24px', fontWeight: 'bold' }}>
        現在の値: {count}
      </p>
      
      <div style={{ marginBottom: '15px' }}>
        <label>
          ステップ: 
          <input 
            type="number" 
            value={step} 
            onChange={(e) => setStep(Number(e.target.value))}
            style={{ marginLeft: '10px', width: '60px' }}
          />
        </label>
      </div>
      
      <div>
        <button onClick={decrement} style={{ margin: '0 5px' }}>
          -{step}
        </button>
        <button onClick={reset} style={{ margin: '0 5px' }}>
          リセット
        </button>
        <button onClick={increment} style={{ margin: '0 5px' }}>
          +{step}
        </button>
      </div>
    </div>
  );
};

このコードでは、2つの状態を管理しています。

const [count, setCount] = useState(0)で、countという状態を定義しています。 0は初期値です。

setCountは状態を更新する関数です。 setCount(count + step)のように使います。

同じように、stepという状態も管理しています。

「useState」という名前の通り、状態を使うためのHookです。

useEffectを使った副作用処理

データの取得や、DOMの操作などは副作用と呼ばれます。 関数型コンポーネントでは、useEffectを使って副作用を処理します。

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

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

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error('ユーザーデータの取得に失敗しました');
        }
        
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    if (userId) {
      fetchUser();
    }
  }, [userId]);

  useEffect(() => {
    if (user) {
      document.title = `${user.name}のプロフィール`;
    }
    
    return () => {
      document.title = 'React App';
    };
  }, [user]);

  if (loading) {
    return <div>読み込み中...</div>;
  }

  if (error) {
    return <div style={{ color: 'red' }}>エラー: {error}</div>;
  }

  if (!user) {
    return <div>ユーザーが見つかりません</div>;
  }

  return (
    <div style={{ padding: '20px' }}>
      <h2>{user.name}のプロフィール</h2>
      <img 
        src={user.avatar} 
        alt={user.name}
        style={{ width: '100px', height: '100px', borderRadius: '50%' }}
      />
      <p>メール: {user.email}</p>
      <p>職業: {user.occupation}</p>
      <p>自己紹介: {user.bio}</p>
    </div>
  );
};

このコードには、2つのuseEffectがあります。

1つ目のuseEffectは、ユーザーデータの取得を行います。 [userId]という依存配列により、userIdが変わったときに再実行されます。

2つ目のuseEffectは、ページのタイトルを更新します。 return () => {}の部分は、クリーンアップ処理です。

コンポーネントがアンマウントされるときに実行されます。

「useEffect」は、副作用を扱うためのHookです。

カスタムHooksの活用

関数型コンポーネントでは、カスタムHooksを作って機能を再利用できます。

const useApi = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    if (url) {
      fetchData();
    }
  }, [url]);

  return { data, loading, error };
};

const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  const setStoredValue = (newValue) => {
    try {
      setValue(newValue);
      window.localStorage.setItem(key, JSON.stringify(newValue));
    } catch (error) {
      console.error('localStorage error:', error);
    }
  };

  return [value, setStoredValue];
};

2つのカスタムHooksを定義しました。

useApiは、API呼び出しを簡単にするHookです。 useLocalStorageは、ローカルストレージを簡単に使うHookです。

実際に使ってみましょう。

const TodoApp = () => {
  const [todos, setTodos] = useLocalStorage('todos', []);
  const [inputValue, setInputValue] = useState('');
  const { data: suggestions, loading } = useApi('/api/todo-suggestions');

  const addTodo = () => {
    if (inputValue.trim()) {
      setTodos([
        ...todos, 
        { 
          id: Date.now(), 
          text: inputValue, 
          completed: false,
          createdAt: new Date().toISOString()
        }
      ]);
      setInputValue('');
    }
  };

  const toggleTodo = (id) => {
    setTodos(todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  return (
    <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
      <h1>Todo アプリ</h1>
      
      <div style={{ marginBottom: '20px' }}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="新しいタスクを入力"
          style={{ padding: '10px', width: '300px' }}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
        />
        <button onClick={addTodo} style={{ padding: '10px', marginLeft: '10px' }}>
          追加
        </button>
      </div>

      {loading && <p>おすすめタスクを読み込み中...</p>}
      
      {suggestions && (
        <div style={{ marginBottom: '20px' }}>
          <h3>おすすめタスク</h3>
          {suggestions.map(suggestion => (
            <button
              key={suggestion.id}
              onClick={() => setInputValue(suggestion.text)}
              style={{ 
                margin: '5px', 
                padding: '5px 10px',
                backgroundColor: '#f0f0f0',
                border: '1px solid #ccc',
                borderRadius: '4px',
                cursor: 'pointer'
              }}
            >
              {suggestion.text}
            </button>
          ))}
        </div>
      )}

      <ul style={{ listStyle: 'none', padding: 0 }}>
        {todos.map(todo => (
          <li 
            key={todo.id}
            style={{
              display: 'flex',
              alignItems: 'center',
              padding: '10px',
              backgroundColor: todo.completed ? '#f0f0f0' : 'white',
              border: '1px solid #ddd',
              borderRadius: '4px',
              marginBottom: '5px'
            }}
          >
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
              style={{ marginRight: '10px' }}
            />
            <span style={{
              textDecoration: todo.completed ? 'line-through' : 'none',
              flex: 1
            }}>
              {todo.text}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
};

このTodoアプリでは、2つのカスタムHooksを使っています。

useLocalStorageでTodoデータを永続化し、useApiでおすすめタスクを取得しています。

カスタムHooksにより、ロジックの再利用関心の分離が実現できます。

「こんなに簡単に機能を再利用できるの?」と思うかもしれませんが、これがHooksの力なんです。

クラス型コンポーネントの基本

クラス型コンポーネントは、ES6のクラス構文を使って定義します。

基本的な書き方

まず、シンプルなクラス型コンポーネントを見てみましょう。

import React, { Component } from 'react';

class BasicClassComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      message: 'Hello'
    };
    
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState({
      count: this.state.count + 1
    });
  }

  render() {
    return (
      <div>
        <h2>{this.state.message}</h2>
        <p>カウント: {this.state.count}</p>
        <button onClick={this.handleClick}>
          カウントアップ
        </button>
      </div>
    );
  }
}

このコードには、クラス型コンポーネントの基本要素が含まれています。

constructorで状態を初期化し、renderメソッドでJSXを返します。

this.handleClick = this.handleClick.bind(this)は、thisのバインドです。 これがないと、handleClick内でthisが正しく動作しません。

現代的な書き方

プロパティの初期化を使った、より現代的な書き方もあります。

class ModernClassComponent extends Component {
  state = {
    user: null,
    loading: false
  };

  handleSubmit = (e) => {
    e.preventDefault();
    this.setState({ loading: true });
    // API呼び出し処理
  };

  render() {
    const { user, loading } = this.state;
    
    return (
      <div>
        {loading ? (
          <p>読み込み中...</p>
        ) : (
          <form onSubmit={this.handleSubmit}>
            {/* フォーム要素 */}
          </form>
        )}
      </div>
    );
  }
}

この書き方では、constructorを書かなくても状態を定義できます。

アロー関数を使うことで、thisのバインドも自動的に解決されます。

ライフサイクルメソッド

クラス型コンポーネントでは、ライフサイクルメソッドを使って処理のタイミングを制御できます。

class LifecycleExample extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
      error: null
    };
    console.log('1. constructor: コンポーネント作成');
  }

  componentDidMount() {
    console.log('2. componentDidMount: DOMに追加された');
    this.fetchData();
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('3. componentDidUpdate: 更新された');
    
    if (prevProps.userId !== this.props.userId) {
      this.fetchData();
    }
  }

  componentWillUnmount() {
    console.log('4. componentWillUnmount: 削除される');
    if (this.timeoutId) {
      clearTimeout(this.timeoutId);
    }
  }

  componentDidCatch(error, errorInfo) {
    console.log('5. componentDidCatch: エラーをキャッチ');
    this.setState({
      error: error.message
    });
  }

  fetchData = async () => {
    try {
      const response = await fetch(`/api/user/${this.props.userId}`);
      const data = await response.json();
      this.setState({ data, error: null });
    } catch (error) {
      this.setState({ error: error.message });
    }
  };

  render() {
    console.log('render: レンダリング実行');
    
    const { data, error } = this.state;

    if (error) {
      return <div>エラー: {error}</div>;
    }

    if (!data) {
      return <div>読み込み中...</div>;
    }

    return (
      <div>
        <h2>{data.name}のプロフィール</h2>
        <p>メール: {data.email}</p>
      </div>
    );
  }
}

主要なライフサイクルメソッドは以下のとおりです。

  • componentDidMount: 初回レンダリング後に実行
  • componentDidUpdate: 更新後に実行
  • componentWillUnmount: 削除前に実行
  • componentDidCatch: エラーが発生した時に実行

これらのメソッドにより、適切なタイミングで処理を実行できます。

複雑な状態管理

クラス型コンポーネントでは、this.statethis.setStateを使って状態管理を行います。

class ComplexStateExample extends Component {
  state = {
    user: {
      name: '',
      email: '',
      preferences: {
        theme: 'light',
        notifications: true
      }
    },
    errors: {},
    isSubmitting: false
  };

  handleInputChange = (field, value) => {
    this.setState(prevState => ({
      user: {
        ...prevState.user,
        [field]: value
      }
    }));
  };

  handlePreferenceChange = (preference, value) => {
    this.setState(prevState => ({
      user: {
        ...prevState.user,
        preferences: {
          ...prevState.user.preferences,
          [preference]: value
        }
      }
    }));
  };

  validateForm = () => {
    const { user } = this.state;
    const errors = {};

    if (!user.name.trim()) {
      errors.name = '名前は必須です';
    }

    if (!user.email.trim()) {
      errors.email = 'メールアドレスは必須です';
    } else if (!/\S+@\S+\.\S+/.test(user.email)) {
      errors.email = '有効なメールアドレスを入力してください';
    }

    this.setState({ errors });
    return Object.keys(errors).length === 0;
  };

  handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!this.validateForm()) {
      return;
    }

    this.setState({ isSubmitting: true });

    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(this.state.user)
      });

      if (response.ok) {
        alert('保存しました');
        this.setState({
          user: { name: '', email: '', preferences: { theme: 'light', notifications: true } },
          errors: {}
        });
      } else {
        throw new Error('保存に失敗しました');
      }
    } catch (error) {
      this.setState({
        errors: { submit: error.message }
      });
    } finally {
      this.setState({ isSubmitting: false });
    }
  };

  render() {
    const { user, errors, isSubmitting } = this.state;

    return (
      <form onSubmit={this.handleSubmit} style={{ padding: '20px', maxWidth: '400px' }}>
        <h2>ユーザー設定</h2>
        
        <div style={{ marginBottom: '15px' }}>
          <label>
            名前:
            <input
              type="text"
              value={user.name}
              onChange={(e) => this.handleInputChange('name', e.target.value)}
              style={{ marginLeft: '10px', padding: '5px' }}
            />
          </label>
          {errors.name && <div style={{ color: 'red' }}>{errors.name}</div>}
        </div>

        <div style={{ marginBottom: '15px' }}>
          <label>
            メール:
            <input
              type="email"
              value={user.email}
              onChange={(e) => this.handleInputChange('email', e.target.value)}
              style={{ marginLeft: '10px', padding: '5px' }}
            />
          </label>
          {errors.email && <div style={{ color: 'red' }}>{errors.email}</div>}
        </div>

        <div style={{ marginBottom: '15px' }}>
          <label>
            テーマ:
            <select
              value={user.preferences.theme}
              onChange={(e) => this.handlePreferenceChange('theme', e.target.value)}
              style={{ marginLeft: '10px', padding: '5px' }}
            >
              <option value="light">ライト</option>
              <option value="dark">ダーク</option>
            </select>
          </label>
        </div>

        <div style={{ marginBottom: '15px' }}>
          <label>
            <input
              type="checkbox"
              checked={user.preferences.notifications}
              onChange={(e) => this.handlePreferenceChange('notifications', e.target.checked)}
            />
            通知を受け取る
          </label>
        </div>

        {errors.submit && (
          <div style={{ color: 'red', marginBottom: '15px' }}>
            {errors.submit}
          </div>
        )}

        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? '保存中...' : '保存'}
        </button>
      </form>
    );
  }
}

この例では、ネストした状態を管理しています。

this.setStateは、オブジェクトをマージしてくれるので、一部の状態だけを更新できます。

ただし、ネストした状態を更新するときは、スプレッド演算子を使って手動でマージする必要があります。

クラス型コンポーネントでは、状態管理が少し複雑になりがちです。

関数型 vs クラス型の比較

同じ機能を実装して、違いを比較してみましょう。

文法とコードの簡潔性

タイマーコンポーネントを、両方の方法で実装してみます。

関数型コンポーネント版

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

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

  useEffect(() => {
    let interval = null;
    
    if (isRunning) {
      interval = setInterval(() => {
        setSeconds(seconds => seconds + 1);
      }, 1000);
    } else if (!isRunning && seconds !== 0) {
      clearInterval(interval);
    }
    
    return () => clearInterval(interval);
  }, [isRunning, seconds]);

  const start = () => setIsRunning(true);
  const stop = () => setIsRunning(false);
  const reset = () => {
    setSeconds(0);
    setIsRunning(false);
  };

  return (
    <div>
      <h2>タイマー: {seconds}秒</h2>
      <button onClick={start} disabled={isRunning}>スタート</button>
      <button onClick={stop} disabled={!isRunning}>ストップ</button>
      <button onClick={reset}>リセット</button>
    </div>
  );
};

関数型では、useStateuseEffectを使って簡潔に書けます。

クラス型コンポーネント版

import React, { Component } from 'react';

class ClassTimer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      seconds: 0,
      isRunning: false
    };
    this.interval = null;
  }

  componentDidUpdate(prevProps, prevState) {
    if (this.state.isRunning !== prevState.isRunning) {
      if (this.state.isRunning) {
        this.interval = setInterval(() => {
          this.setState(prevState => ({
            seconds: prevState.seconds + 1
          }));
        }, 1000);
      } else {
        clearInterval(this.interval);
      }
    }
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  start = () => {
    this.setState({ isRunning: true });
  };

  stop = () => {
    this.setState({ isRunning: false });
  };

  reset = () => {
    this.setState({
      seconds: 0,
      isRunning: false
    });
  };

  render() {
    const { seconds, isRunning } = this.state;
    
    return (
      <div>
        <h2>タイマー: {seconds}秒</h2>
        <button onClick={this.start} disabled={isRunning}>スタート</button>
        <button onClick={this.stop} disabled={!isRunning}>ストップ</button>
        <button onClick={this.reset}>リセット</button>
      </div>
    );
  }
}

クラス型では、ライフサイクルメソッドを使って状態の変化に対応しています。

比較すると、関数型コンポーネントの方が簡潔で読みやすいことがわかります。

パフォーマンスの違い

パフォーマンス最適化の方法も見てみましょう。

関数型の最適化

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

const OptimizedFunctionComponent = memo(({ items, onItemClick }) => {
  const expensiveValue = useMemo(() => {
    return items.reduce((sum, item) => sum + item.value, 0);
  }, [items]);

  const handleClick = useCallback((id) => {
    onItemClick(id);
  }, [onItemClick]);

  return (
    <div>
      <h3>合計: {expensiveValue}</h3>
      {items.map(item => (
        <ItemComponent
          key={item.id}
          item={item}
          onClick={handleClick}
        />
      ))}
    </div>
  );
});

関数型では、memouseMemouseCallbackを使って最適化します。

クラス型の最適化

import React, { Component } from 'react';

class OptimizedClassComponent extends Component {
  getExpensiveValue() {
    const { items } = this.props;
    if (this._cachedItems === items) {
      return this._cachedValue;
    }
    
    this._cachedItems = items;
    this._cachedValue = items.reduce((sum, item) => sum + item.value, 0);
    return this._cachedValue;
  }

  shouldComponentUpdate(nextProps) {
    return nextProps.items !== this.props.items ||
           nextProps.onItemClick !== this.props.onItemClick;
  }

  handleClick = (id) => {
    this.props.onItemClick(id);
  };

  render() {
    const { items } = this.props;
    const expensiveValue = this.getExpensiveValue();

    return (
      <div>
        <h3>合計: {expensiveValue}</h3>
        {items.map(item => (
          <ItemComponent
            key={item.id}
            item={item}
            onClick={this.handleClick}
          />
        ))}
      </div>
    );
  }
}

クラス型では、shouldComponentUpdateや手動でのキャッシュが必要です。

関数型の方が、直感的で簡単に最適化できます。

学習コストの比較

学習のしやすさも重要な要素です。

関数型コンポーネント

メリット

  • JavaScriptの関数がわかれば理解しやすい
  • Hooksは直感的で覚えやすい
  • 最新のReactの推奨方法

デメリット

  • useEffectの依存配列の理解が必要
  • Hooksのルールを覚える必要がある

クラス型コンポーネント

メリット

  • オブジェクト指向の知識があれば理解しやすい
  • ライフサイクルメソッドが明確

デメリット

  • thisの理解が必要
  • bindの概念が複雑
  • より多くのコード記述が必要

初心者にとっては、関数型コンポーネントの方が学習しやすいと言えるでしょう。

どちらを選ぶべきか?

現在のReact開発では、関数型コンポーネントが推奨されています。

関数型コンポーネントが推奨される理由

現在のReactでは、以下の理由で関数型コンポーネントが推奨されています。

  • シンプルで読みやすい: コードが簡潔
  • Hooksの強力な機能: 状態管理やライフサイクルを統一的に扱える
  • パフォーマンス最適化: 最適化が簡単
  • Reactの将来性: 今後の機能はHooksベースで開発される
  • テストのしやすさ: 関数なのでテストしやすい
  • 再利用性: カスタムHooksで機能を再利用できる

使い分けのガイドライン

具体的な使い分けの指針を見てみましょう。

関数型コンポーネントを使う場合

以下の場合は、関数型コンポーネントを使いましょう。

  • 新しいコンポーネントを作成する
  • シンプルな表示コンポーネント
  • 状態管理が必要なコンポーネント
  • API連携が必要なコンポーネント
  • カスタムHooksを活用したい

クラス型コンポーネントを使う場合

以下の場合は、クラス型コンポーネントを使います。

  • エラー境界を実装する
  • 既存のクラス型コンポーネントの保守
  • レガシーコードとの互換性が必要
  • ライフサイクルメソッドの詳細な制御が必要

実際の例

現在のベストプラクティスを見てみましょう。

関数型コンポーネントの例

const UserDashboard = () => {
  const [user, setUser] = useState(null);
  const [notifications, setNotifications] = useState([]);
  
  useEffect(() => {
    fetchUserData();
    fetchNotifications();
  }, []);
  
  const fetchUserData = async () => {
    try {
      const response = await fetch('/api/user');
      const userData = await response.json();
      setUser(userData);
    } catch (error) {
      console.error('ユーザーデータの取得に失敗:', error);
    }
  };
  
  const fetchNotifications = async () => {
    try {
      const response = await fetch('/api/notifications');
      const notificationData = await response.json();
      setNotifications(notificationData);
    } catch (error) {
      console.error('通知の取得に失敗:', error);
    }
  };
  
  return (
    <div>
      <UserProfile user={user} />
      <NotificationList notifications={notifications} />
    </div>
  );
};

新しいコンポーネントは、このように関数型で作成します。

クラス型コンポーネントの例(エラー境界)

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    
    return this.props.children;
  }
}

エラー境界は、まだHooksで実装できないため、クラス型コンポーネントを使います。

移行の指針

既存のクラス型コンポーネントから関数型への移行も検討しましょう。

移行の優先順位

  1. 新しいコンポーネント: 関数型で作成
  2. よく変更されるコンポーネント: 段階的に移行
  3. 安定したコンポーネント: 無理に移行しない
  4. エラー境界: クラス型のまま維持

移行の例

// 移行前: クラス型
class UserList extends Component {
  constructor(props) {
    super(props);
    this.state = {
      users: [],
      loading: true
    };
  }

  async componentDidMount() {
    try {
      const response = await fetch('/api/users');
      const users = await response.json();
      this.setState({ users, loading: false });
    } catch (error) {
      console.error('Error:', error);
      this.setState({ loading: false });
    }
  }

  render() {
    const { users, loading } = this.state;
    
    if (loading) {
      return <div>Loading...</div>;
    }

    return (
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    );
  }
}

// 移行後: 関数型
const UserList = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        const response = await fetch('/api/users');
        const users = await response.json();
        setUsers(users);
      } catch (error) {
        console.error('Error:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

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

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
};

移行により、コードがより簡潔で読みやすくなります。

移行時の注意点

移行する際は、以下の点に注意しましょう。

  • useEffectの依存配列を正しく設定する
  • 無限ループを作らないようにする
  • パフォーマンス最適化を考慮する
  • 十分なテストを行う

段階的な移行により、リスクを最小限に抑えることができます。

まとめ

Reactコンポーネントの理解により、より良いアプリケーション開発が可能になります。

重要なポイント

この記事で学んだポイントをまとめてみましょう。

コンポーネントの基本

  • コンポーネントは再利用可能なUI部品
  • propsを受け取ってJSXを返す
  • 関数型とクラス型の2つの方法がある

関数型コンポーネント

  • シンプルで読みやすいコード
  • Hooksによる強力な機能
  • カスタムHooksによる再利用性
  • 現在の推奨方法

クラス型コンポーネント

  • ライフサイクルメソッドによる詳細な制御
  • エラー境界の実装が可能
  • thisとbindの理解が必要
  • レガシーコードで使用

選択の指針

どちらを選ぶかの指針も整理しましょう。

  • 新しいコンポーネント: 関数型を選択
  • エラー境界: クラス型を使用
  • 既存コード: 段階的に移行
  • 学習: 関数型から始める

今後の学習

React開発をさらに深めるために、以下の学習をおすすめします。

  • Hooksの深い理解: useEffect、useCallback、useMemoなど
  • カスタムHooksの作成: 機能の再利用を実現
  • パフォーマンス最適化: React.memoや仮想化の活用
  • テストの書き方: Jest、React Testing Libraryの使用
  • 型安全性: TypeScriptとの組み合わせ

最後に

現在のReact開発では、関数型コンポーネントが主流です。 Hooksの強力な機能により、より簡潔で保守しやすいコードが書けます。

これからReactを学ぶ方は、関数型コンポーネントを中心に学習することをおすすめします。

既存のクラス型コンポーネントを扱う場合も、この記事の内容を参考に適切に対応してください。

ぜひこの記事を参考にして、効果的なReactコンポーネント開発を進めてみてください。

あなたのReact開発が成功することを、心から応援しています!

関連記事