memory-1761599_1280
プログラミング

Reactコンポーネントとは?UI部品の作り方から設計パターンまで徹底解説

目次

Reactを学ぶことは、コンポーネントを学ぶことと同義です。コンポーネントは、Reactアプリケーションを構築するための最も基本的かつ重要な「ビルディングブロック」であり、その設計思想を理解することが、効率的でメンテナンス性の高いUI開発の鍵となります。本記事では、「コンポーネントって一体何?」という基本から、具体的な書き方、責務を分離するための設計パターン、そしてパフォーマンスや品質を高めるための実践的なテクニックまで、Reactコンポーネントのすべてを網羅的に解説します。

Reactコンポーネントの本質:UIを構築する思想

コンポーネントとは何か:部品化による開発革命

Reactコンポーネントとは、UI(ユーザーインターフェース)を構成する、独立して再利用可能な部品のことです。この概念を理解するために、従来のWeb開発と比較してみましょう。

従来のHTMLでは、ページ全体を一つの大きなファイルとして記述していました:

html

<!-- 従来のHTML -->
<div class="header">
  <h1>My Website</h1>
  <nav>...</nav>
</div>
<div class="main">
  <div class="card">
    <h2>Card Title</h2>
    <p>Card content...</p>
  </div>
  <!-- 同じ構造を何度も繰り返す -->
  <div class="card">
    <h2>Another Card</h2>
    <p>More content...</p>
  </div>
</div>

Reactでは、これを独立した部品(コンポーネント)として分割します:

jsx

// Headerコンポーネント
function Header() {
  return (
    <header>
      <h1>My Website</h1>
      <nav>...</nav>
    </header>
  );
}

// Cardコンポーネント(再利用可能)
function Card({ title, content }) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <p>{content}</p>
    </div>
  );
}

// それらを組み合わせて画面を構成
function App() {
  return (
    <div>
      <Header />
      <main>
        <Card title="Card Title" content="Card content..." />
        <Card title="Another Card" content="More content..." />
      </main>
    </div>
  );
}

この部品化アプローチがもたらす利点は計り知れません。同じCardコンポーネントを異なる場所で再利用でき、一箇所の修正がすべての使用箇所に反映され、各部品を独立してテストできます。

関数コンポーネント:現代Reactの標準形

現在のReact開発では、関数コンポーネントが完全に主流となっています。その理由を、実際のコードで見てみましょう。

シンプルなコンポーネントの例:

jsx

// 最もシンプルな関数コンポーネント
function Welcome({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// アロー関数でも定義可能
const Welcome = ({ name }) => {
  return <h1>Hello, {name}!</h1>;
};

// returnが1行の場合は省略記法も使える
const Welcome = ({ name }) => <h1>Hello, {name}!</h1>;

より実践的なコンポーネント:

jsx

function UserProfile({ user, onEdit, onDelete }) {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div className="user-profile">
      <div className="user-header">
        <img src={user.avatar} alt={user.name} />
        <h3>{user.name}</h3>
        <button onClick={() => setIsExpanded(!isExpanded)}>
          {isExpanded ? 'Show Less' : 'Show More'}
        </button>
      </div>
      
      {isExpanded && (
        <div className="user-details">
          <p>Email: {user.email}</p>
          <p>Joined: {new Date(user.joinedAt).toLocaleDateString()}</p>
          <div className="actions">
            <button onClick={() => onEdit(user.id)}>Edit</button>
            <button onClick={() => onDelete(user.id)}>Delete</button>
          </div>
        </div>
      )}
    </div>
  );
}

関数コンポーネントの美しさは、その直感的な構造にあります。引数としてpropsを受け取り、JSXを返す。それだけで立派なReactコンポーネントになるのです。

JSXと仮想DOM:宣言的UIの実現

JSXは、JavaScriptの中にHTMLのような構文を書ける拡張記法です。しかし、これは単なる構文糖ではありません。

jsx

// JSXで書いたコード
const element = (
  <div className="greeting">
    <h1>Hello, {name}</h1>
    <p>Welcome to React!</p>
  </div>
);

// 実際にはこのようなJavaScriptに変換される
const element = React.createElement(
  'div',
  { className: 'greeting' },
  React.createElement('h1', null, 'Hello, ', name),
  React.createElement('p', null, 'Welcome to React!')
);

この変換により、Reactは仮想DOMという軽量なJavaScriptオブジェクトのツリーを作成します。状態が変更されると、新しい仮想DOMツリーが作成され、前回との差分が計算されます:

jsx

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

  // countが変更されるたびに、新しい仮想DOMが作成される
  // Reactは差分を検出し、変更された部分だけを実DOMに反映
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

この仕組みにより、開発者は「UIがどのように変化するか」ではなく、「UIがどうあるべきか」を宣言的に記述できるのです。

単一責任の原則:優れたコンポーネント設計の基礎

優れたコンポーネントは、単一の責任だけを持ちます。これを実践的な例で見てみましょう。

悪い例:責任が混在したコンポーネント

jsx

// ❌ データ取得、状態管理、表示のすべてを担当
function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      });
  }, []);

  const filteredUsers = users.filter(user => 
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  );

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

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search users..."
      />
      <ul>
        {filteredUsers.map(user => (
          <li key={user.id}>
            <img src={user.avatar} alt={user.name} />
            <span>{user.name}</span>
            <span>{user.email}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

良い例:責任を分離したコンポーネント

jsx

// ✅ 検索フィールドコンポーネント(検索UIのみ)
function SearchField({ value, onChange, placeholder }) {
  return (
    <input
      type="text"
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder={placeholder}
    />
  );
}

// ✅ ユーザーアイテムコンポーネント(1人分の表示のみ)
function UserItem({ user }) {
  return (
    <li className="user-item">
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
      <span>{user.email}</span>
    </li>
  );
}

// ✅ ユーザーリストコンポーネント(リスト表示のみ)
function UserList({ users }) {
  return (
    <ul className="user-list">
      {users.map(user => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}

// ✅ コンテナコンポーネント(データ取得とフィルタリングロジック)
function UserListContainer() {
  const [searchTerm, setSearchTerm] = useState('');
  const { data: users, loading } = useFetch('/api/users');

  const filteredUsers = users?.filter(user => 
    user.name.toLowerCase().includes(searchTerm.toLowerCase())
  ) || [];

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

  return (
    <div className="user-list-container">
      <SearchField
        value={searchTerm}
        onChange={setSearchTerm}
        placeholder="Search users..."
      />
      <UserList users={filteredUsers} />
    </div>
  );
}

この分離により、各コンポーネントは独立してテスト可能になり、再利用性も大幅に向上します。

コンポーネントの基本文法:動的UIの実現

propsとchildren:データの受け渡し

propsは、親コンポーネントから子コンポーネントへデータを渡すための仕組みです。実践的な使い方を見ていきましょう。

基本的なpropsの使い方:

jsx

// Buttonコンポーネント
function Button({ variant, size, onClick, disabled, children }) {
  const className = `btn btn-${variant} btn-${size}`;
  
  return (
    <button 
      className={className}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
}

// 使用例
function App() {
  return (
    <div>
      <Button 
        variant="primary" 
        size="large" 
        onClick={() => alert('Clicked!')}
      >
        Click Me
      </Button>
      
      <Button 
        variant="secondary" 
        size="small" 
        disabled
      >
        Disabled Button
      </Button>
    </div>
  );
}

childrenを活用した柔軟なコンポーネント:

jsx

// Cardコンポーネント
function Card({ title, footer, children }) {
  return (
    <div className="card">
      {title && (
        <div className="card-header">
          <h3>{title}</h3>
        </div>
      )}
      
      <div className="card-body">
        {children}
      </div>
      
      {footer && (
        <div className="card-footer">
          {footer}
        </div>
      )}
    </div>
  );
}

// 使用例
function ProductCard({ product }) {
  return (
    <Card 
      title={product.name}
      footer={
        <Button onClick={() => addToCart(product.id)}>
          Add to Cart
        </Button>
      }
    >
      <img src={product.image} alt={product.name} />
      <p className="price">${product.price}</p>
      <p className="description">{product.description}</p>
    </Card>
  );
}

stateによるインタラクティブなUI

stateは、コンポーネントが内部で管理する動的なデータです。ユーザーの操作に応じて変化するUIを実現します。

フォームの状態管理:

jsx

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
    // エラーをクリア
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };

  const validate = () => {
    const newErrors = {};
    if (!formData.name) newErrors.name = 'Name is required';
    if (!formData.email) newErrors.email = 'Email is required';
    else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Email is invalid';
    }
    if (!formData.message) newErrors.message = 'Message is required';
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validate()) return;
    
    setIsSubmitting(true);
    try {
      await submitForm(formData);
      alert('Form submitted successfully!');
      // フォームをリセット
      setFormData({ name: '', email: '', message: '' });
    } catch (error) {
      alert('Submission failed. Please try again.');
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="contact-form">
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          className={errors.name ? 'error' : ''}
        />
        {errors.name && <span className="error-message">{errors.name}</span>}
      </div>

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && <span className="error-message">{errors.email}</span>}
      </div>

      <div className="form-group">
        <label htmlFor="message">Message</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          rows="5"
          className={errors.message ? 'error' : ''}
        />
        {errors.message && <span className="error-message">{errors.message}</span>}
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

条件分岐とリスト描画

Reactでは、JavaScriptの式を使って条件付きレンダリングを行います。

条件分岐のパターン:

jsx

function NotificationBadge({ notifications }) {
  // 早期リターンパターン
  if (!notifications || notifications.length === 0) {
    return null;
  }

  return (
    <div className="notification-badge">
      {/* 三項演算子 */}
      <span className={notifications.length > 9 ? 'many' : 'few'}>
        {notifications.length > 99 ? '99+' : notifications.length}
      </span>
      
      {/* &&演算子 */}
      {notifications.some(n => n.urgent) && (
        <span className="urgent-indicator">!</span>
      )}
    </div>
  );
}

リスト描画とkeyの重要性:

jsx

function TodoList({ todos, onToggle, onDelete }) {
  return (
    <ul className="todo-list">
      {todos.map(todo => (
        // keyは必須:ReactがDOM要素を効率的に更新するために必要
        <li key={todo.id} className={todo.completed ? 'completed' : ''}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => onToggle(todo.id)}
          />
          <span>{todo.text}</span>
          <button onClick={() => onDelete(todo.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

// ❌ 悪い例:インデックスをkeyとして使用
{todos.map((todo, index) => (
  <li key={index}>...</li>  // 順序が変わると問題が発生
))}

// ✅ 良い例:安定したユニークなIDを使用
{todos.map(todo => (
  <li key={todo.id}>...</li>
))}

設計パターン:スケーラブルなアーキテクチャ

状態のリフトアップ:適切な状態管理の配置

複数のコンポーネントで状態を共有する必要がある場合、その状態は共通の親コンポーネントに配置します。

jsx

// Temperature入力コンポーネント
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
  const scaleNames = {
    c: 'Celsius',
    f: 'Fahrenheit'
  };

  return (
    <fieldset>
      <legend>Enter temperature in {scaleNames[scale]}:</legend>
      <input
        value={temperature}
        onChange={(e) => onTemperatureChange(e.target.value)}
      />
    </fieldset>
  );
}

// 温度変換の計算関数
function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}

// 親コンポーネント(状態を管理)
function TemperatureCalculator() {
  const [temperature, setTemperature] = useState('');
  const [scale, setScale] = useState('c');

  const handleCelsiusChange = (temperature) => {
    setScale('c');
    setTemperature(temperature);
  };

  const handleFahrenheitChange = (temperature) => {
    setScale('f');
    setTemperature(temperature);
  };

  const celsius = scale === 'f' ? toCelsius(temperature) : temperature;
  const fahrenheit = scale === 'c' ? toFahrenheit(temperature) : temperature;

  return (
    <div>
      <TemperatureInput
        scale="c"
        temperature={celsius}
        onTemperatureChange={handleCelsiusChange}
      />
      <TemperatureInput
        scale="f"
        temperature={fahrenheit}
        onTemperatureChange={handleFahrenheitChange}
      />
      {temperature && (
        <BoilingVerdict celsius={parseFloat(celsius)} />
      )}
    </div>
  );
}

Context APIによるグローバル状態管理

深いコンポーネントツリーでデータを共有する場合、Context APIを使用します。

jsx

// テーマコンテキストの作成
const ThemeContext = createContext();

// テーマプロバイダー
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState(() => {
    // ローカルストレージから初期値を取得
    return localStorage.getItem('theme') || 'light';
  });

  const toggleTheme = () => {
    setTheme(prevTheme => {
      const newTheme = prevTheme === 'light' ? 'dark' : 'light';
      localStorage.setItem('theme', newTheme);
      return newTheme;
    });
  };

  const value = {
    theme,
    toggleTheme,
    colors: theme === 'light' ? lightColors : darkColors
  };

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

// カスタムフックでContextを使いやすくする
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 深くネストしたコンポーネントでの使用
function DeepChildComponent() {
  const { theme, toggleTheme, colors } = useTheme();

  return (
    <div style={{ backgroundColor: colors.background, color: colors.text }}>
      <p>Current theme: {theme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

コンパウンドコンポーネントパターン

複数のコンポーネントが協調して動作する高度なパターンです。

jsx

// Tabsコンポーネントの実装
const TabsContext = createContext();

function Tabs({ children, defaultActiveTab = 0 }) {
  const [activeTab, setActiveTab] = useState(defaultActiveTab);

  return (
    <TabsContext.Provider value={{ activeTab, setActiveTab }}>
      <div className="tabs">{children}</div>
    </TabsContext.Provider>
  );
}

function TabList({ children }) {
  return <div className="tab-list" role="tablist">{children}</div>;
}

function Tab({ index, children }) {
  const { activeTab, setActiveTab } = useContext(TabsContext);
  
  return (
    <button
      role="tab"
      aria-selected={activeTab === index}
      className={`tab ${activeTab === index ? 'active' : ''}`}
      onClick={() => setActiveTab(index)}
    >
      {children}
    </button>
  );
}

function TabPanels({ children }) {
  const { activeTab } = useContext(TabsContext);
  return <div className="tab-panels">{children[activeTab]}</div>;
}

function TabPanel({ children }) {
  return <div className="tab-panel" role="tabpanel">{children}</div>;
}

// 使用例
function App() {
  return (
    <Tabs defaultActiveTab={0}>
      <TabList>
        <Tab index={0}>Profile</Tab>
        <Tab index={1}>Settings</Tab>
        <Tab index={2}>Security</Tab>
      </TabList>
      
      <TabPanels>
        <TabPanel>
          <h2>Profile Information</h2>
          {/* Profile content */}
        </TabPanel>
        <TabPanel>
          <h2>Application Settings</h2>
          {/* Settings content */}
        </TabPanel>
        <TabPanel>
          <h2>Security Options</h2>
          {/* Security content */}
        </TabPanel>
      </TabPanels>
    </Tabs>
  );
}

実務レベルの実装:品質とパフォーマンス

フォルダ構成とファイル管理

大規模プロジェクトでも管理しやすい構成例:

src/
├── components/          # 共通UIコンポーネント
│   ├── Button/
│   │   ├── Button.jsx
│   │   ├── Button.module.css
│   │   ├── Button.test.jsx
│   │   └── index.js
│   └── Card/
│       └── ...
├── features/           # 機能別のコンポーネント
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── utils/
│   └── products/
│       └── ...
├── hooks/             # 共通カスタムフック
├── utils/             # ユーティリティ関数
└── styles/            # グローバルスタイル

スタイリングの選択肢

CSS Modules:

jsx

// Button.module.css
.button {
  padding: 8px 16px;
  border-radius: 4px;
  font-weight: 500;
  transition: all 0.2s;
}

.primary {
  background-color: #007bff;
  color: white;
}

.button:hover {
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

// Button.jsx
import styles from './Button.module.css';

function Button({ variant = 'primary', children, ...props }) {
  return (
    <button 
      className={`${styles.button} ${styles[variant]}`}
      {...props}
    >
      {children}
    </button>
  );
}

Tailwind CSS:

jsx

function Card({ title, children, highlighted }) {
  return (
    <div className={`
      bg-white rounded-lg shadow-md p-6
      ${highlighted ? 'ring-2 ring-blue-500' : ''}
      hover:shadow-lg transition-shadow duration-200
    `}>
      {title && (
        <h3 className="text-xl font-semibold mb-4 text-gray-800">
          {title}
        </h3>
      )}
      <div className="text-gray-600">
        {children}
      </div>
    </div>
  );
}

アクセシビリティの実装

すべてのユーザーが使えるコンポーネントを作ります:

jsx

function Modal({ isOpen, onClose, title, children }) {
  const modalRef = useRef();
  const previousActiveElement = useRef();

  useEffect(() => {
    if (isOpen) {
      // 現在のフォーカス要素を記憶
      previousActiveElement.current = document.activeElement;
      // モーダルにフォーカスを移動
      modalRef.current?.focus();
    } else {
      // モーダルを閉じたら元の要素にフォーカスを戻す
      previousActiveElement.current?.focus();
    }
  }, [isOpen]);

  // Escapeキーで閉じる
  useEffect(() => {
    const handleEscape = (e) => {
      if (e.key === 'Escape' && isOpen) {
        onClose();
      }
    };

    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div
        ref={modalRef}
        className="modal-content"
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        onClick={(e) => e.stopPropagation()}
      >
        <h2 id="modal-title">{title}</h2>
        <button
          className="close-button"
          onClick={onClose}
          aria-label="Close modal"
        >
          ×
        </button>
        {children}
      </div>
    </div>
  );
}

パフォーマンス最適化

React.memoによる再レンダリング防止:

jsx

// 最適化前
function ExpensiveList({ items, onItemClick }) {
  console.log('ExpensiveList rendered');
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {/* 複雑な計算やレンダリング */}
          {calculateComplexValue(item)}
        </li>
      ))}
    </ul>
  );
}

// 最適化後
const ExpensiveList = React.memo(({ items, onItemClick }) => {
  console.log('ExpensiveList rendered');
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {calculateComplexValue(item)}
        </li>
      ))}
    </ul>
  );
}, (prevProps, nextProps) => {
  // カスタム比較関数(オプション)
  return (
    prevProps.items === nextProps.items &&
    prevProps.onItemClick === nextProps.onItemClick
  );
});

// 親コンポーネントでの使用
function Parent() {
  const [count, setCount] = useState(0);
  const [items] = useState(() => generateLargeList());
  
  // onItemClickをメモ化して参照を安定させる
  const handleItemClick = useCallback((id) => {
    console.log('Item clicked:', id);
  }, []);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>
        Count: {count}
      </button>
      <ExpensiveList items={items} onItemClick={handleItemClick} />
    </div>
  );
}

仮想スクロール(大量リストの最適化):

jsx

import { FixedSizeList } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      <img src={items[index].thumbnail} alt="" />
      <span>{items[index].name}</span>
      <span>{items[index].price}</span>
    </div>
  );

  return (
    <FixedSizeList
      height={600}      // ビューポートの高さ
      itemCount={items.length}
      itemSize={80}     // 各アイテムの高さ
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

現代のReact:Server ComponentsとNext.js

Server Componentsの基本

React Server Componentsは、サーバーでのみ実行される新しいタイプのコンポーネントです:

jsx

// app/products/page.jsx (Server Component - デフォルト)
async function ProductsPage() {
  // サーバーで直接データベースにアクセス
  const products = await db.query('SELECT * FROM products');
  
  return (
    <div>
      <h1>Our Products</h1>
      <ProductGrid products={products} />
    </div>
  );
}

// components/ProductGrid.jsx (Server Component)
function ProductGrid({ products }) {
  return (
    <div className="grid">
      {products.map(product => (
        // Client Componentを使用
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// components/ProductCard.jsx (Client Component)
'use client'; // このディレクティブでClient Componentを明示

import { useState } from 'react';

function ProductCard({ product }) {
  const [isLiked, setIsLiked] = useState(false);
  
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => setIsLiked(!isLiked)}>
        {isLiked ? '❤️' : '🤍'}
      </button>
    </div>
  );
}

データ取得パターン

Next.js App Routerでの実践的なデータ取得:

jsx

// app/blog/[slug]/page.jsx
export async function generateStaticParams() {
  const posts = await getPosts();
  
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{new Date(post.publishedAt).toLocaleDateString()}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={post.id} />
      </Suspense>
    </article>
  );
}

// Commentsは別途データを取得
async function Comments({ postId }) {
  const comments = await getComments(postId);
  
  return (
    <section>
      <h2>Comments</h2>
      {comments.map(comment => (
        <Comment key={comment.id} comment={comment} />
      ))}
    </section>
  );
}

まとめ:コンポーネント思考で築くモダンなUI

Reactコンポーネントは、単なるUIの部品ではありません。それは、アプリケーションの構造、データフロー、パフォーマンス、そして保守性を決定づける、最も重要な設計単位です。

本記事で解説した内容を振り返ると:

  1. 基本概念:コンポーネントは独立した再利用可能なUI部品であり、単一責任の原則に従って設計する
  2. 実装技術:関数コンポーネント、props、state、JSXを使い、宣言的にUIを構築する
  3. 設計パターン:状態のリフトアップ、Context API、コンパウンドコンポーネントなどで、スケーラブルな構造を実現する
  4. 品質向上:アクセシビリティ、パフォーマンス最適化、適切なテストで、プロダクションレベルの品質を確保する
  5. 最新動向:Server Componentsの登場により、サーバーとクライアントの境界でより効率的なアーキテクチャが可能に

コンポーネント思考を身につけることは、React開発者としての成長の第一歩です。小さく始めて、徐々に複雑なパターンに挑戦していくことで、あなたも効率的で保守性の高いReactアプリケーションを構築できるようになるでしょう。

実際に手を動かし、様々なコンポーネントを作ってみることが、理解を深める最良の方法です。この記事のコード例を参考に、ぜひ自分なりのコンポーネントライブラリを構築してみてください。

ビジネスの成長をサポートします

Harmonic Societyは、最新のテクノロジーとクリエイティブな発想で、
お客様のビジネス課題を解決します。

豊富な実績と経験
最新技術への対応
親身なサポート体制

師田 賢人

Harmonic Society株式会社 代表取締役。一橋大学(商学部)卒業後、Accenture Japanに入社。ITコンサルタントとして働いた後、Webエンジニアを経て2016年に独立。ブロックチェーン技術を専門に200名以上の専門家に取材をし記事を執筆する。2023年にHarmonic Society株式会社を設立後、AI駆動開発によるWebサイト・アプリ制作を行っている。

コメントを残す