Reactアプリケーションの高度な機能実装:UIコンポーネント、HTTP通信、および状態遷移アニメーション

概要

本記事では、Reactプロジェクトにおける実用的な高度機能を3つの軸で解説します。Ant Designを活用したプロダクション対応UI構築、axiosを用いた柔軟かつ信頼性の高いAPI連携、そしてreact-transition-groupによる自然なユーザーインタラクションの実現方法について、実装例を交えて詳細に紹介します。

Ant Designを活用したモダンUI開発

動的クラス名管理:classnamesライブラリの導入

Reactでは条件付きクラス名の適用が頻繁に必要になりますが、文字列連結による実装は可読性・保守性に課題があります。以下のようにclassnamesを活用することで、安全かつ宣言的にスタイルを制御できます。

import React, { useState } from 'react';
import cn from 'classnames';

export default function DynamicClassDemo() {
  const [isActive, setToggle] = useState(true);
  const [isHighlighted, setIsHighlighted] = useState(false);

  return (
    <div>
      {/* 従来の手動結合(推奨しない) */}
      <h2 className={`header ${isActive ? 'active' : ''} ${isHighlighted ? 'highlight' : ''}`}>
        手動結合例
      </h2>

      {/* classnamesによる安全な結合 */}
      <h2 className={cn('header', { active: isActive, highlight: isHighlighted })}>
        classnames活用例
      </h2>

      {/* 配列ベースの組み合わせ */}
      <h2 className={cn(['header', 'base-style'], { 'is-active': isActive })}>
        配列+オブジェクト混合
      </h2>
    </div>
  );
}

Ant Designの統合と国際化設定

Ant Designは、日本語対応やテーマカスタマイズが容易な企業向けUIライブラリです。アプリケーション全体で一貫したローカライズとテーマを適用するには、ConfigProviderをルートコンポーネントでラップします。

// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
  <ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#25B864' } }}>
    <App />
  </ConfigProvider>
);

CRACOによるビルド設定の拡張

Create React App(CRA)の制約を超えるために、CRACO(Create React App Configuration Override)を導入します。これにより、Webpackエイリアスやテーマ変数の注入など、カスタムビルドフローをシンプルに実現可能です。

// craco.config.js
const path = require('path');

module.exports = {
  webpack: {
    alias: {
      '@components': path.resolve(__dirname, 'src/components'),
      '@assets': path.resolve(__dirname, 'src/assets'),
      '@utils': path.resolve(__dirname, 'src/utils')
    }
  }
};

コメント投稿機能の実装例

Ant DesignのCommentInputAvatarを組み合わせたリアルタイムコメント機能を構築します。状態管理は関数コンポーネント+Hooksで簡潔に記述します。

// src/components/CommentList.jsx
import { Comment, Avatar, Button, Input } from 'antd';
import { DeleteOutlined } from '@ant-design/icons';
import { useState, useCallback } from 'react';

export default function CommentList() {
  const [comments, setComments] = useState([]);
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = useCallback(() => {
    if (!inputValue.trim()) return;
    
    const newComment = {
      id: Date.now(),
      author: '開発者',
      avatar: 'https://via.placeholder.com/40',
      content: inputValue,
      timeAgo: '今から1分前'
    };

    setComments(prev => [newComment, ...prev]);
    setInputValue('');
  }, [inputValue]);

  const handleDelete = useCallback((id) => {
    setComments(prev => prev.filter(c => c.id !== id));
  }, []);

  return (
    <div>
      <Input.TextArea 
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        rows={3}
        placeholder="コメントを入力してください..."
      />
      <Button type="primary" onClick={handleSubmit} style={{ marginTop: '8px' }}>
        投稿
      </Button>

      <div style={{ marginTop: '24px' }}>
        {comments.map(comment => (
          <Comment
            key={comment.id}
            author={<a href="#">{comment.author}</a>}
            avatar={<Avatar src={comment.avatar} alt={comment.author} />}
            content={<p>{comment.content}</p>}
            datetime={<span>{comment.timeAgo}</span>}
            actions={[
              <span key="delete" onClick={() => handleDelete(comment.id)}>
                <DeleteOutlined /> 削除
              </span>
            ]}
          />
        ))}
      </div>
    </div>
  );
}

axiosによる堅牢なAPI通信設計

基本的なHTTPリクエスト操作

axiosはPromiseベースのHTTPクライアントであり、RESTfulなAPIとのやり取りを直感的に行えます。以下の例では、並列リクエストとエラーハンドリングを含む実践的な使い方を示します。

import axios from 'axios';

// カスタムインスタンスの作成(環境ごとの設定)
const apiClient = axios.create({
  baseURL: import.meta.env.DEV ? 'https://httpbin.org' : '/api',
  timeout: 10000,
  headers: { 'X-Client': 'react-app' }
});

// リクエスト送信と結果処理
async function fetchUserData() {
  try {
    const response = await apiClient.get('/get', {
      params: { userId: 123 }
    });
    console.log('ユーザー情報:', response.data);
  } catch (error) {
    if (error.response?.status === 404) {
      console.warn('ユーザーが見つかりません');
    } else if (error.code === 'ECONNABORTED') {
      console.error('タイムアウトしました');
    }
  }
}

リクエスト/レスポンスインターセプターの活用

共通の前処理・後処理を一元管理するために、インターセプターを定義します。認証トークンの自動付与、ローディング状態の制御、エラー共通処理などが可能です。

// src/lib/api.js
import axios from 'axios';

const client = axios.create();

// リクエストインターセプター
client.interceptors.request.use(
  config => {
    const token = localStorage.getItem('auth_token');
    if (token) config.headers.Authorization = `Bearer ${token}`;
    // 共通ローディング表示ロジック(例:Reduxアクション発火)
    return config;
  },
  error => Promise.reject(error)
);

// レスポンスインターセプター
client.interceptors.response.use(
  response => response.data, // dataフィールドのみ返却
  error => {
    const status = error.response?.status;
    switch (status) {
      case 401:
        localStorage.removeItem('auth_token');
        window.location.href = '/login';
        break;
      case 500:
        alert('サーバーエラーが発生しました');
        break;
      default:
        console.error('APIエラー:', error);
    }
    return Promise.reject(error);
  }
);

export default client;

状態遷移アニメーションの実装

CSSTransitionによる単一要素のトランジション

要素の表示・非表示時にCSSアニメーションを適用する場合、CSSTransitionが最適です。appear、enter、exitの各フェーズに対応したクラス名を指定することで、細かい制御が可能です。

import { CSSTransition } from 'react-transition-group';

function AnimatedCard({ isVisible }) {
  return (
    <CSSTransition
      in={isVisible}
      timeout={300}
      classNames="fade"
      unmountOnExit
      appear
    >
      <div className="card">アニメーション付きカード</div>
    </CSSTransition>
  );
}

// CSS(fade-enter → fade-enter-active → fade-enter-done)
/* .fade-enter { opacity: 0; }
.fade-enter-active { opacity: 1; transition: opacity 300ms ease-in; }
.fade-enter-done { opacity: 1; }
.fade-exit { opacity: 1; }
.fade-exit-active { opacity: 0; transition: opacity 300ms ease-out; }
.fade-exit-done { opacity: 0; } */

SwitchTransitionによるコンポーネント切り替えアニメーション

ボタンのON/OFFやタブ切り替えなど、「ある要素が消えて、別の要素が現れる」ようなシナリオにはSwitchTransitionが有効です。mode属性で「新→旧」または「旧→新」の順序を指定できます。

import { SwitchTransition, CSSTransition } from 'react-transition-group';

function ToggleButton({ isOn, toggle }) {
  return (
    <SwitchTransition mode="out-in">
      <CSSTransition
        key={isOn ? 'on' : 'off'}
        timeout={250}
        classNames="slide"
      >
        <button onClick={toggle} className="toggle-btn">
          {isOn ? 'ON' : 'OFF'}
        </button>
      </CSSTransition>
    </SwitchTransition>
  );
}

// CSS(slide-enter → slide-enter-active → slide-enter-done)
/* .slide-enter { transform: translateX(100%); opacity: 0; }
.slide-enter-active { transform: translateX(0); opacity: 1; transition: all 250ms ease; }
.slide-exit { transform: translateX(0); opacity: 1; }
.slide-exit-active { transform: translateX(-100%); opacity: 0; transition: all 250ms ease; } */

TransitionGroupによるリストアイテムのアニメーション

配列データの追加・削除に対して個別にアニメーションを適用するには、TransitionGroupCSSTransitionを組み合わせます。各要素にユニークなkeyを指定することが必須です。

import { TransitionGroup, CSSTransition } from 'react-transition-group';

function AnimatedList({ items }) {
  return (
    <TransitionGroup component="ul" className="item-list">
      {items.map(item => (
        <CSSTransition
          key={item.id}
          timeout={300}
          classNames="list-item"
        >
          <li className="list-item-content">
            {item.name}
            <button onClick={() => removeItem(item.id)}>×</button>
          </li>
        </CSSTransition>
      ))}
    </TransitionGroup>
  );
}

タグ: ant-design Axios react-transition-group craco classnames

6月28日 22:39 投稿