T-CREATOR

初心者でも失敗しない!React アニメーションの基礎とおすすめライブラリ

初心者でも失敗しない!React アニメーションの基礎とおすすめライブラリ

React でアニメーションを実装したいけれど、「どこから始めればいいのか分からない」「ライブラリがたくさんあって選べない」と悩んでいませんか?

この記事では、React アニメーション初心者の方でも安心して取り組めるよう、基本的な概念から実用的な実装方法まで、段階的にご紹介いたします。よくある失敗例とその解決法も含めているので、効率的に学習を進められますよ。

背景

なぜ React でアニメーションが必要なのか

現代の Web アプリケーションにおいて、アニメーションは単なる装飾ではありません。ユーザー体験を大きく向上させる重要な要素となっています。

ユーザビリティの向上 アニメーションによって、ユーザーの操作に対する適切なフィードバックを提供できます。例えば、ボタンをクリックした際の視覚的な反応や、フォーム送信時のローディング表示などです。

直感的な操作感 画面遷移やメニューの開閉など、状態変化を滑らかにアニメーションすることで、ユーザーが迷わずアプリケーションを操作できるようになります。

プロフェッショナルな印象 適切に実装されたアニメーションは、アプリケーション全体の品質を高め、ユーザーに対してプロフェッショナルな印象を与えます。

アニメーションの効果具体例ユーザーへの影響
フィードバックボタンホバー、クリック反応操作が正しく認識されたことの確認
状態変化の表現モーダル開閉、タブ切り替え画面の変化を理解しやすくする
注意喚起通知表示、エラーメッセージ重要な情報への注目を促す
読み込み状況の表示ローディングスピナー、プログレスバー待機時間の体感を軽減する

初心者が陥りやすい落とし穴

React アニメーション実装において、初心者の方がよく遭遇する問題をご紹介します。

過度に複雑なライブラリの選択 最初から高機能なライブラリを選んでしまい、基本的な概念を理解せずに挫折してしまうケースがよくあります。

パフォーマンスを考慮しない実装 アニメーションが滑らかに動作せず、カクカクした動きになってしまう問題です。特にモバイルデバイスでは顕著に現れます。

アクセシビリティの軽視 視覚障害や前庭障害を持つユーザーへの配慮を怠り、すべてのユーザーが快適に利用できないアニメーションを作ってしまうことがあります。

課題

アニメーション実装でよくある失敗例

初心者の方が実際に遭遇する典型的な失敗例とその原因を見ていきましょう。

ちらつきが発生する問題

javascript// 問題のあるコード例
const BadAnimation = () => {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        切り替え
      </button>
      {isVisible && (
        <div
          style={{
            opacity: isVisible ? 1 : 0,
            transition: 'opacity 0.3s ease',
          }}
        >
          コンテンツ
        </div>
      )}
    </div>
  );
};

この例では、要素の表示・非表示を制御する際に、DOM からの追加・削除とアニメーションが適切に連動していないため、ちらつきが発生します。

メモリリークの発生

javascript// メモリリークを引き起こす危険なコード
const ProblematicComponent = () => {
  const [position, setPosition] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setPosition((prev) => prev + 1);
    }, 16); // 60FPS相当

    // クリーンアップを忘れている!
    // return () => clearInterval(interval);
  }, []);

  return (
    <div style={{ transform: `translateX(${position}px)` }}>
      動く要素
    </div>
  );
};
bash# 発生するエラー例
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.

レイアウトシフトによるパフォーマンス低下

javascript// パフォーマンスが悪い実装
const SlowAnimation = () => {
  const [isExpanded, setIsExpanded] = useState(false);

  return (
    <div
      style={{
        width: isExpanded ? '300px' : '100px', // width変更はリフローを引き起こす
        height: isExpanded ? '200px' : '50px', // height変更もリフローを引き起こす
        transition: 'all 0.3s ease',
      }}
      onClick={() => setIsExpanded(!isExpanded)}
    >
      クリックして展開
    </div>
  );
};

この実装では、widthheight の変更により、ブラウザがレイアウトの再計算(リフロー)を行うため、アニメーションがカクカクしてしまいます。

ライブラリ選択の迷いと混乱

React のアニメーションライブラリは数多く存在し、初心者の方にとって選択が困難な状況となっています。

情報の氾濫 各ライブラリの公式ドキュメントや記事を読んでも、どれが自分のレベルや目的に適しているか判断が難しいことがあります。

学習コストの見積もり困難 ライブラリごとに API や概念が大きく異なるため、習得にかかる時間や労力を予測しにくく、途中で方向転換を余儀なくされることがあります。

プロジェクト要件とのミスマッチ 高機能なライブラリを選んだものの、実際のプロジェクトでは必要ない機能ばかりで、バンドルサイズが大きくなってしまう問題もあります。

解決策

初心者向けライブラリ選択の基準

適切なライブラリを選択するための明確な基準をご紹介します。

学習コストの低さ 最初は API がシンプルで、直感的に理解できるライブラリを選ぶことが重要です。

ドキュメントの質 初心者向けのチュートリアルや豊富なサンプルコードがあるライブラリを選びましょう。

コミュニティの活発さ 問題が発生した際に、解決策を見つけやすい環境が整っているかも重要な判断基準です。

ライブラリ学習難易度機能の豊富さバンドルサイズ推奨度(初心者)
CSS Transitions★☆☆★☆☆0KB★★★
React Transition Group★★☆★★☆9KB★★★
Framer Motion★★☆★★★32KB★★☆
React Spring★★★★★★28KB★☆☆
GSAP★★★★★★47KB+★☆☆

段階的な学習アプローチ

効率的にスキルアップするための学習ステップをご提案します。

段階 1:基礎概念の理解 CSS のアニメーション機能を理解し、React での状態管理と組み合わせる方法を学びます。

段階 2:React 標準機能の活用 React の状態管理やライフサイクルを使った基本的なアニメーション実装を学びます。

段階 3:専用ライブラリの導入 実用的なライブラリを使って、より高度なアニメーションを実装します。

段階 4:カスタマイズと最適化 独自のアニメーション関数やカスタムフックを作成し、パフォーマンスを最適化します。

具体例

ステップ 1:CSS アニメーションから始める

最も基本的な CSS アニメーションと React の組み合わせから始めましょう。

基本的なホバーエフェクト

typescriptimport React, { useState } from 'react';

const BasicHoverButton = () => {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <button
      className={`basic-button ${
        isHovered ? 'hovered' : ''
      }`}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      ホバーしてください
    </button>
  );
};

export default BasicHoverButton;
css.basic-button {
  padding: 12px 24px;
  background-color: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  cursor: pointer;
  transition: all 0.3s ease;
  transform: translateY(0);
}

.basic-button.hovered {
  background-color: #2563eb;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}

条件付きアニメーション

typescriptimport React, { useState } from 'react';

const ConditionalAnimation = () => {
  const [showMessage, setShowMessage] = useState(false);

  return (
    <div className='container'>
      <button onClick={() => setShowMessage(!showMessage)}>
        {showMessage
          ? 'メッセージを隠す'
          : 'メッセージを表示'}
      </button>

      <div
        className={`message ${
          showMessage ? 'visible' : 'hidden'
        }`}
      >
        こんにちは!アニメーションが動作しています。
      </div>
    </div>
  );
};

export default ConditionalAnimation;
css.message {
  margin-top: 16px;
  padding: 16px;
  background-color: #ecfdf5;
  border: 1px solid #10b981;
  border-radius: 8px;
  transition: all 0.3s ease;
}

.message.hidden {
  opacity: 0;
  transform: translateY(-10px);
  max-height: 0;
  padding: 0 16px;
  margin-top: 0;
}

.message.visible {
  opacity: 1;
  transform: translateY(0);
  max-height: 100px;
}

よくあるエラーと解決法

bash# Error: Cannot read property 'classList' of null
# 原因: DOM要素が存在しない状態でクラス操作を行おうとした
typescript// 解決法: 条件付きレンダリングの適切な使用
const SafeAnimation = () => {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        切り替え
      </button>
      {/* 条件付きレンダリングを使用 */}
      {isVisible && (
        <div className='animated-content'>
          表示されるコンテンツ
        </div>
      )}
    </div>
  );
};

ステップ 2:React Transition Group の基本

React Transition Group は、React で最も基本的なアニメーションライブラリです。

インストールと基本設定

bashyarn add react-transition-group
yarn add --dev @types/react-transition-group  # TypeScript使用時

CSSTransition を使った基本的な実装

typescriptimport React, { useState } from 'react';
import { CSSTransition } from 'react-transition-group';

const BasicCSSTransition = () => {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(!showModal)}>
        モーダルを開く
      </button>

      <CSSTransition
        in={showModal}
        timeout={300}
        classNames='modal'
        unmountOnExit
      >
        <div
          className='modal-overlay'
          onClick={() => setShowModal(false)}
        >
          <div
            className='modal-content'
            onClick={(e) => e.stopPropagation()}
          >
            <h2>モーダルタイトル</h2>
            <p>ここにコンテンツが表示されます。</p>
            <button onClick={() => setShowModal(false)}>
              閉じる
            </button>
          </div>
        </div>
      </CSSTransition>
    </div>
  );
};

export default BasicCSSTransition;
css/* モーダルのアニメーションスタイル */
.modal-enter {
  opacity: 0;
  transform: scale(0.9);
}

.modal-enter-active {
  opacity: 1;
  transform: scale(1);
  transition: opacity 300ms ease, transform 300ms ease;
}

.modal-exit {
  opacity: 1;
  transform: scale(1);
}

.modal-exit-active {
  opacity: 0;
  transform: scale(0.9);
  transition: opacity 300ms ease, transform 300ms ease;
}

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal-content {
  background: white;
  padding: 24px;
  border-radius: 8px;
  max-width: 400px;
  width: 90%;
}

TransitionGroup を使ったリストアニメーション

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

const AnimatedList = () => {
  const [items, setItems] = useState([
    { id: 1, text: 'アイテム 1' },
    { id: 2, text: 'アイテム 2' },
    { id: 3, text: 'アイテム 3' },
  ]);

  const addItem = () => {
    const newId =
      Math.max(...items.map((item) => item.id)) + 1;
    setItems([
      ...items,
      { id: newId, text: `アイテム ${newId}` },
    ]);
  };

  const removeItem = (id: number) => {
    setItems(items.filter((item) => item.id !== id));
  };

  return (
    <div>
      <button onClick={addItem}>アイテムを追加</button>

      <TransitionGroup className='item-list'>
        {items.map((item) => (
          <CSSTransition
            key={item.id}
            timeout={300}
            classNames='item'
          >
            <div className='list-item'>
              {item.text}
              <button onClick={() => removeItem(item.id)}>
                削除
              </button>
            </div>
          </CSSTransition>
        ))}
      </TransitionGroup>
    </div>
  );
};

export default AnimatedList;
css.item-list {
  margin-top: 16px;
}

.list-item {
  padding: 12px;
  margin: 8px 0;
  background-color: #f3f4f6;
  border-radius: 8px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.item-enter {
  opacity: 0;
  transform: translateX(-100%);
}

.item-enter-active {
  opacity: 1;
  transform: translateX(0);
  transition: opacity 300ms ease, transform 300ms ease;
}

.item-exit {
  opacity: 1;
  transform: translateX(0);
}

.item-exit-active {
  opacity: 0;
  transform: translateX(100%);
  transition: opacity 300ms ease, transform 300ms ease;
}

よくあるエラーと解決法

bash# Warning: findDOMNode is deprecated in StrictMode
# 原因: React 18のStrictModeでの非推奨警告
typescript// 解決法: nodeRefを使用
import React, { useState, useRef } from 'react';
import { CSSTransition } from 'react-transition-group';

const FixedCSSTransition = () => {
  const [show, setShow] = useState(false);
  const nodeRef = useRef(null);

  return (
    <div>
      <button onClick={() => setShow(!show)}>
        切り替え
      </button>

      <CSSTransition
        nodeRef={nodeRef} // refを追加
        in={show}
        timeout={300}
        classNames='fade'
        unmountOnExit
      >
        <div ref={nodeRef} className='content'>
          コンテンツ
        </div>
      </CSSTransition>
    </div>
  );
};

ステップ 3:Framer Motion の入門

Framer Motion は現在最も人気の高い React アニメーションライブラリの一つです。

インストールと基本的な使用方法

bashyarn add framer-motion

基本的なアニメーション

typescriptimport React, { useState } from 'react';
import { motion } from 'framer-motion';

const BasicFramerMotion = () => {
  const [isVisible, setIsVisible] = useState(true);

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        切り替え
      </button>

      {isVisible && (
        <motion.div
          initial={{ opacity: 0, y: 50 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0, y: -50 }}
          transition={{ duration: 0.3 }}
          style={{
            width: 200,
            height: 100,
            backgroundColor: '#3b82f6',
            borderRadius: 8,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            color: 'white',
            margin: '16px 0',
          }}
        >
          アニメーション要素
        </motion.div>
      )}
    </div>
  );
};

export default BasicFramerMotion;

ホバーアニメーション

typescriptimport React from 'react';
import { motion } from 'framer-motion';

const HoverAnimations = () => {
  return (
    <div
      style={{
        display: 'flex',
        gap: '16px',
        padding: '20px',
      }}
    >
      <motion.button
        whileHover={{ scale: 1.05 }}
        whileTap={{ scale: 0.95 }}
        style={{
          padding: '12px 24px',
          backgroundColor: '#10b981',
          color: 'white',
          border: 'none',
          borderRadius: '8px',
          cursor: 'pointer',
        }}
      >
        スケールボタン
      </motion.button>

      <motion.button
        whileHover={{
          backgroundColor: '#ef4444',
          transition: { duration: 0.2 },
        }}
        style={{
          padding: '12px 24px',
          backgroundColor: '#f59e0b',
          color: 'white',
          border: 'none',
          borderRadius: '8px',
          cursor: 'pointer',
        }}
      >
        色変化ボタン
      </motion.button>

      <motion.button
        whileHover={{ rotateZ: 5 }}
        whileTap={{ rotateZ: -5 }}
        style={{
          padding: '12px 24px',
          backgroundColor: '#8b5cf6',
          color: 'white',
          border: 'none',
          borderRadius: '8px',
          cursor: 'pointer',
        }}
      >
        回転ボタン
      </motion.button>
    </div>
  );
};

export default HoverAnimations;

AnimatePresence を使った条件付きアニメーション

typescriptimport React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

const ConditionalFramerMotion = () => {
  const [selectedId, setSelectedId] = useState<
    string | null
  >(null);

  const items = [
    {
      id: '1',
      title: 'カード 1',
      content: 'カード1の詳細内容です。',
    },
    {
      id: '2',
      title: 'カード 2',
      content: 'カード2の詳細内容です。',
    },
    {
      id: '3',
      title: 'カード 3',
      content: 'カード3の詳細内容です。',
    },
  ];

  return (
    <div>
      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'repeat(3, 1fr)',
          gap: '16px',
        }}
      >
        {items.map((item) => (
          <motion.div
            key={item.id}
            layoutId={item.id}
            onClick={() => setSelectedId(item.id)}
            style={{
              padding: '20px',
              backgroundColor: '#f3f4f6',
              borderRadius: '8px',
              cursor: 'pointer',
            }}
            whileHover={{ scale: 1.05 }}
          >
            <h3>{item.title}</h3>
          </motion.div>
        ))}
      </div>

      <AnimatePresence>
        {selectedId && (
          <motion.div
            style={{
              position: 'fixed',
              top: 0,
              left: 0,
              right: 0,
              bottom: 0,
              backgroundColor: 'rgba(0, 0, 0, 0.5)',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
            }}
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={() => setSelectedId(null)}
          >
            <motion.div
              layoutId={selectedId}
              style={{
                width: 400,
                padding: '40px',
                backgroundColor: 'white',
                borderRadius: '12px',
              }}
              onClick={(e) => e.stopPropagation()}
            >
              {items.find(
                (item) => item.id === selectedId
              ) && (
                <>
                  <h2>
                    {
                      items.find(
                        (item) => item.id === selectedId
                      )?.title
                    }
                  </h2>
                  <p>
                    {
                      items.find(
                        (item) => item.id === selectedId
                      )?.content
                    }
                  </p>
                  <button
                    onClick={() => setSelectedId(null)}
                  >
                    閉じる
                  </button>
                </>
              )}
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
};

export default ConditionalFramerMotion;

ステップ 4:React Spring の基礎

React Spring は物理ベースのアニメーションに特化したライブラリです。

インストールと基本的な使用方法

bashyarn add react-spring

useSpring を使った基本アニメーション

typescriptimport React, { useState } from 'react';
import { useSpring, animated } from 'react-spring';

const BasicSpringAnimation = () => {
  const [isToggled, setIsToggled] = useState(false);

  const springProps = useSpring({
    opacity: isToggled ? 1 : 0,
    transform: isToggled
      ? 'translateY(0px)'
      : 'translateY(50px)',
    backgroundColor: isToggled ? '#10b981' : '#3b82f6',
    config: { tension: 300, friction: 30 },
  });

  return (
    <div>
      <button onClick={() => setIsToggled(!isToggled)}>
        アニメーション切り替え
      </button>

      <animated.div
        style={{
          ...springProps,
          width: 200,
          height: 100,
          borderRadius: 8,
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'white',
          margin: '20px 0',
        }}
      >
        Spring アニメーション
      </animated.div>
    </div>
  );
};

export default BasicSpringAnimation;

useTransition を使ったリストアニメーション

typescriptimport React, { useState } from 'react';
import { useTransition, animated } from 'react-spring';

const SpringListAnimation = () => {
  const [items, setItems] = useState([
    { id: 1, text: 'アイテム 1' },
    { id: 2, text: 'アイテム 2' },
    { id: 3, text: 'アイテム 3' },
  ]);

  const transitions = useTransition(items, {
    from: { opacity: 0, transform: 'translateX(-100px)' },
    enter: { opacity: 1, transform: 'translateX(0px)' },
    leave: { opacity: 0, transform: 'translateX(100px)' },
    config: { tension: 200, friction: 25 },
  });

  const addItem = () => {
    const newId =
      Math.max(...items.map((item) => item.id)) + 1;
    setItems([
      ...items,
      { id: newId, text: `アイテム ${newId}` },
    ]);
  };

  const removeItem = (id: number) => {
    setItems(items.filter((item) => item.id !== id));
  };

  return (
    <div>
      <button onClick={addItem}>アイテムを追加</button>

      <div style={{ marginTop: '16px' }}>
        {transitions((style, item) => (
          <animated.div
            style={{
              ...style,
              padding: '12px',
              margin: '8px 0',
              backgroundColor: '#f3f4f6',
              borderRadius: '8px',
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center',
            }}
          >
            <span>{item.text}</span>
            <button onClick={() => removeItem(item.id)}>
              削除
            </button>
          </animated.div>
        ))}
      </div>
    </div>
  );
};

export default SpringListAnimation;

よくあるエラーと解決法

bash# Error: Invalid hook call. Hooks can only be called inside of the body of a function component
# 原因: フックが条件文の中で呼び出されている
typescript// 問題のあるコード
const BadSpringComponent = ({
  shouldAnimate,
}: {
  shouldAnimate: boolean;
}) => {
  if (shouldAnimate) {
    const springProps = useSpring({ opacity: 1 }); // エラー:条件付きフック呼び出し
  }

  return <div>コンテンツ</div>;
};

// 修正されたコード
const FixedSpringComponent = ({
  shouldAnimate,
}: {
  shouldAnimate: boolean;
}) => {
  const springProps = useSpring({
    opacity: shouldAnimate ? 1 : 0, // フックは常に呼び出す
  });

  return (
    <animated.div style={springProps}>
      コンテンツ
    </animated.div>
  );
};

ステップ 5:簡単なカスタムフック作成

独自のアニメーションロジックを再利用可能な形にまとめることで、コードの保守性が向上します。

フェードインアニメーション用のカスタムフック

typescriptimport { useState, useEffect } from 'react';

interface UseFadeInOptions {
  duration?: number;
  delay?: number;
  easing?: string;
}

const useFadeIn = (options: UseFadeInOptions = {}) => {
  const {
    duration = 300,
    delay = 0,
    easing = 'ease',
  } = options;
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsVisible(true);
    }, delay);

    return () => clearTimeout(timer);
  }, [delay]);

  const style = {
    opacity: isVisible ? 1 : 0,
    transition: `opacity ${duration}ms ${easing}`,
  };

  return { style, isVisible };
};

// 使用例
const FadeInExample = () => {
  const fadeIn1 = useFadeIn({ delay: 0 });
  const fadeIn2 = useFadeIn({ delay: 200 });
  const fadeIn3 = useFadeIn({ delay: 400 });

  return (
    <div>
      <div
        style={{
          ...fadeIn1.style,
          padding: '16px',
          backgroundColor: '#fee2e2',
        }}
      >
        最初に表示される要素
      </div>
      <div
        style={{
          ...fadeIn2.style,
          padding: '16px',
          backgroundColor: '#fef3c7',
        }}
      >
        次に表示される要素
      </div>
      <div
        style={{
          ...fadeIn3.style,
          padding: '16px',
          backgroundColor: '#ecfdf5',
        }}
      >
        最後に表示される要素
      </div>
    </div>
  );
};

export default FadeInExample;

スクロール検出アニメーション用のカスタムフック

typescriptimport { useState, useEffect, useRef } from 'react';

interface UseScrollAnimationOptions {
  threshold?: number;
  rootMargin?: string;
}

const useScrollAnimation = (
  options: UseScrollAnimationOptions = {}
) => {
  const { threshold = 0.1, rootMargin = '0px' } = options;
  const [isVisible, setIsVisible] = useState(false);
  const elementRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsVisible(entry.isIntersecting);
      },
      { threshold, rootMargin }
    );

    const currentElement = elementRef.current;
    if (currentElement) {
      observer.observe(currentElement);
    }

    return () => {
      if (currentElement) {
        observer.unobserve(currentElement);
      }
    };
  }, [threshold, rootMargin]);

  const animationStyle = {
    opacity: isVisible ? 1 : 0,
    transform: isVisible
      ? 'translateY(0)'
      : 'translateY(30px)',
    transition: 'opacity 0.6s ease, transform 0.6s ease',
  };

  return { elementRef, isVisible, animationStyle };
};

// 使用例
const ScrollAnimationExample = () => {
  const animation1 = useScrollAnimation();
  const animation2 = useScrollAnimation({ threshold: 0.3 });

  return (
    <div>
      <div
        style={{
          height: '100vh',
          backgroundColor: '#f3f4f6',
        }}
      >
        スクロールしてください
      </div>

      <div
        ref={animation1.elementRef}
        style={{
          ...animation1.animationStyle,
          padding: '40px',
          backgroundColor: '#dbeafe',
          margin: '20px 0',
        }}
      >
        スクロールで表示される要素 1
      </div>

      <div
        ref={animation2.elementRef}
        style={{
          ...animation2.animationStyle,
          padding: '40px',
          backgroundColor: '#fce7f3',
          margin: '20px 0',
        }}
      >
        スクロールで表示される要素 2
      </div>

      <div
        style={{
          height: '100vh',
          backgroundColor: '#f3f4f6',
        }}
      >
        最後のセクション
      </div>
    </div>
  );
};

export default ScrollAnimationExample;

ローディング状態管理用のカスタムフック

typescriptimport { useState, useCallback } from 'react';

interface UseLoadingAnimationOptions {
  minDuration?: number;
}

const useLoadingAnimation = (
  options: UseLoadingAnimationOptions = {}
) => {
  const { minDuration = 1000 } = options;
  const [isLoading, setIsLoading] = useState(false);

  const startLoading = useCallback(
    async (asyncOperation: () => Promise<any>) => {
      setIsLoading(true);
      const startTime = Date.now();

      try {
        await asyncOperation();
      } finally {
        const elapsedTime = Date.now() - startTime;
        const remainingTime = Math.max(
          0,
          minDuration - elapsedTime
        );

        setTimeout(() => {
          setIsLoading(false);
        }, remainingTime);
      }
    },
    [minDuration]
  );

  return { isLoading, startLoading };
};

// 使用例
const LoadingExample = () => {
  const { isLoading, startLoading } = useLoadingAnimation({
    minDuration: 2000,
  });
  const [data, setData] = useState<string>('');

  const fetchData = async () => {
    await startLoading(async () => {
      // 実際のAPI呼び出しをシミュレート
      await new Promise((resolve) =>
        setTimeout(resolve, 500)
      );
      setData('データの読み込みが完了しました!');
    });
  };

  return (
    <div style={{ padding: '20px' }}>
      <button onClick={fetchData} disabled={isLoading}>
        データを読み込む
      </button>

      {isLoading && (
        <div
          style={{
            marginTop: '20px',
            padding: '20px',
            backgroundColor: '#fef3c7',
            borderRadius: '8px',
            textAlign: 'center',
          }}
        >
          <div
            className='spinner'
            style={{
              width: '20px',
              height: '20px',
              border: '2px solid #f3f3f3',
              borderTop: '2px solid #3498db',
              borderRadius: '50%',
              animation: 'spin 1s linear infinite',
              margin: '0 auto 10px',
            }}
          ></div>
          読み込み中...
        </div>
      )}

      {data && !isLoading && (
        <div
          style={{
            marginTop: '20px',
            padding: '20px',
            backgroundColor: '#ecfdf5',
            borderRadius: '8px',
          }}
        >
          {data}
        </div>
      )}

      <style jsx>{`
        @keyframes spin {
          0% {
            transform: rotate(0deg);
          }
          100% {
            transform: rotate(360deg);
          }
        }
      `}</style>
    </div>
  );
};

export default LoadingExample;

まとめ

初心者が押さえるべき基本原則

React アニメーション実装において、初心者の方が成功するために重要なポイントをまとめます。

段階的な学習の重要性 いきなり複雑なライブラリを使わず、CSS アニメーションから始めて、徐々にステップアップしていくことが成功の秘訣です。基礎をしっかりと理解してから次のステップに進むことで、つまずくことなく学習を継続できます。

パフォーマンスを意識した実装 美しいアニメーションでも、パフォーマンスが悪ければユーザー体験は損なわれます。transformopacity を中心とした実装を心がけ、不要な再レンダリングを避けることが重要です。

アクセシビリティへの配慮 すべてのユーザーが快適に利用できるよう、prefers-reduced-motion への対応や適切な ARIA 属性の設定を忘れずに行いましょう。

エラーハンドリングの習慣化 アニメーション実装では予期しないエラーが発生しやすいため、適切なエラーハンドリングとクリーンアップ処理を習慣化することが大切です。

実用性を重視した機能選択 多機能なライブラリの全ての機能を使う必要はありません。プロジェクトの要件に合った機能のみを選択し、シンプルで保守しやすいコードを書くことを心がけましょう。

これらの原則を守りながら実践を重ねることで、確実にスキルアップできるはずです。最初は小さなアニメーションから始めて、徐々に複雑な表現にチャレンジしていきましょう。継続的な学習と実践が、魅力的な Web アプリケーション開発につながります。

関連リンク