T-CREATOR

3 分で理解!React Spring を使った自然なアニメーション入門

3 分で理解!React Spring を使った自然なアニメーション入門

たった 3 分で React Spring の魅力を体感してみませんか?

この記事では、物理ベースアニメーションライブラリ「React Spring」の基本概念から実装まで、短時間で効率的に学べる内容をご紹介します。難しい理論は後回しにして、まずは「動く」体験から始めましょう!

背景

なぜ React Spring が「自然」なのか

従来の CSS アニメーションや JavaScript アニメーションは、時間軸に沿った直線的な変化を基本としています。しかし、React Spring は物理法則に基づいてアニメーションを生成するため、まるで現実世界のような自然な動きを実現できます。

従来のアニメーション vs React Spring

項目従来のアニメーションReact Spring
動きの基準時間(duration)物理法則(tension, friction)
動きの質感機械的、均一自然、有機的
中断時の処理不自然な停止スムーズな状態変化
実装の複雑さ複雑な計算が必要直感的な設定

物理ベースアニメーションの魅力

React Spring では、以下の物理パラメータでアニメーションを制御します:

  • tension(張力): アニメーションの速さ
  • friction(摩擦): アニメーションの滑らかさ
  • mass(質量): アニメーションの重さ

これらのパラメータを調整することで、「重いボールが跳ねる動き」や「軽い羽毛が舞う動き」など、直感的に理解できる動きを表現できます。

課題

従来の CSS アニメーションの限界

CSS アニメーションでは、以下のような問題がありました:

1. 不自然な動きの停止

css/* 問題のあるCSS例 */
.element {
  transition: transform 0.3s ease;
  transform: translateX(0);
}

.element.moved {
  transform: translateX(100px);
}

この例では、アニメーション途中で状態が変更されると、急激に方向転換してしまい不自然な動きになります。

2. 複雑な連続アニメーションの実装困難

javascript// 複雑になりがちなCSS+JavaScript実装
const animateSequence = async () => {
  element.style.transform = 'translateX(100px)';
  await new Promise((resolve) => setTimeout(resolve, 300));
  element.style.transform = 'translateY(100px)';
  await new Promise((resolve) => setTimeout(resolve, 300));
  element.style.opacity = '0.5';
  // さらに複雑になっていく...
};

3. レスポンシブ対応の難しさ

デバイスサイズや性能に応じて、アニメーション速度や動きを調整するのが困難でした。

ユーザーが求める自然な動きとは

現代のユーザーは、以下のような動きを期待しています:

  • 予測可能な動き: 物理法則に従った直感的な動作
  • 滑らかな状態遷移: 急激な変化ではなく、自然な流れ
  • インタラクティブな反応: ユーザーの操作に応じた適切なフィードバック

解決策

React Spring の核心機能

React Spring は、以下の 4 つの主要フックで構成されています:

フック名用途特徴
useSpring単一値のアニメーション最も基本的、簡単
useTransition要素の追加・削除リストアニメーションに最適
useChainアニメーション連携複数アニメーションを順次実行
useSprings複数要素の制御大量の要素を効率的に制御

最短で習得する学習戦略

30 秒理解法: まず動かしてから理解する

  1. 基本コードをコピペ: まずは動く状態を作る
  2. 値を変更してみる: tension, friction を変更して違いを体感
  3. 用途に応じて応用: 実際のプロジェクトで使ってみる

この順序で学習することで、理論的な理解より先に「感覚的な理解」を得られます。

具体例

30 秒で作る基本のバウンスアニメーション

まずは最もシンプルな例から始めましょう。

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

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

  const springProps = useSpring({
    transform: isToggled
      ? 'translateY(-50px)'
      : 'translateY(0px)',
    config: { tension: 300, friction: 10 }, // バウンシーな設定
  });

  return (
    <div style={{ padding: '50px' }}>
      <animated.div
        style={{
          ...springProps,
          width: '100px',
          height: '100px',
          backgroundColor: '#ff6b6b',
          borderRadius: '50%',
          cursor: 'pointer',
        }}
        onClick={() => setIsToggled(!isToggled)}
      >
        クリック!
      </animated.div>
    </div>
  );
};

export default BasicBounce;

ポイント解説:

  • tension: 300: 高い値で素早い動き
  • friction: 10: 低い値でバウンシーな動き
  • animated.div: 通常の div の代わりに使用

よくあるエラーと解決法:

bash# Error: Cannot read properties of undefined (reading 'tension')
# 原因: configの設定ミス
typescript// 間違った書き方
const springProps = useSpring({
  transform: 'translateY(-50px)',
  config: { tension, friction }, // 変数が未定義
});

// 正しい書き方
const springProps = useSpring({
  transform: 'translateY(-50px)',
  config: { tension: 300, friction: 10 }, // 値を明示
});

1 分で完成!ドラッグ&ドロップ

React Spring とジェスチャーライブラリを組み合わせた実用的な例です。

bashyarn add @use-gesture/react
typescriptimport React from 'react';
import { useSpring, animated } from 'react-spring';
import { useDrag } from '@use-gesture/react';

const DragAndDrop = () => {
  const [{ x, y }, api] = useSpring(() => ({ x: 0, y: 0 }));

  const bind = useDrag(({ active, movement: [mx, my] }) => {
    api.start({
      x: active ? mx : 0,
      y: active ? my : 0,
      config: { tension: 500, friction: 50 },
    });
  });

  return (
    <div style={{ padding: '100px' }}>
      <animated.div
        {...bind()}
        style={{
          x,
          y,
          width: '100px',
          height: '100px',
          backgroundColor: '#4ecdc4',
          borderRadius: '10px',
          cursor: 'grab',
          touchAction: 'none', // タッチデバイス対応
        }}
      >
        ドラッグして!
      </animated.div>
    </div>
  );
};

export default DragAndDrop;

重要なポイント:

  • touchAction: 'none': モバイル対応必須
  • active ? mx : 0: ドラッグ中は現在位置、離すと元の位置に戻る

よくあるエラーと解決法:

bash# Error: Failed to execute 'addEventListener' on 'EventTarget': parameter 1 is not of type 'string'
# 原因: useDragの設定ミス
typescript// 間違った書き方
const bind = useDrag(({ movement }) => {
  // movementの分割代入を間違えている
  api.start({ x: movement.x, y: movement.y });
});

// 正しい書き方
const bind = useDrag(({ movement: [mx, my] }) => {
  // movementは配列で [x, y] の形式
  api.start({ x: mx, y: my });
});

2 分でマスター!連続アニメーション

複数のアニメーションを順次実行する例です。

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

const SequentialAnimation = () => {
  const [isAnimating, setIsAnimating] = useState(false);

  // 各アニメーションのref
  const scaleRef = useSpringRef();
  const scaleSpring = useSpring({
    ref: scaleRef,
    from: { scale: 1 },
    to: { scale: isAnimating ? 1.5 : 1 },
  });

  const colorRef = useSpringRef();
  const colorSpring = useSpring({
    ref: colorRef,
    from: { backgroundColor: '#3498db' },
    to: {
      backgroundColor: isAnimating ? '#e74c3c' : '#3498db',
    },
  });

  const moveRef = useSpringRef();
  const moveSpring = useSpring({
    ref: moveRef,
    from: { transform: 'translateX(0px)' },
    to: {
      transform: isAnimating
        ? 'translateX(200px)'
        : 'translateX(0px)',
    },
  });

  // アニメーションの実行順序とタイミング
  useChain(
    isAnimating
      ? [scaleRef, colorRef, moveRef]
      : [moveRef, colorRef, scaleRef],
    isAnimating
      ? [0, 0.3, 0.6] // 開始: 0秒, 0.3秒, 0.6秒
      : [0, 0.2, 0.4] // 終了: 少し早めに
  );

  return (
    <div style={{ padding: '100px' }}>
      <button onClick={() => setIsAnimating(!isAnimating)}>
        {isAnimating ? 'リセット' : 'アニメーション開始'}
      </button>

      <animated.div
        style={{
          ...scaleSpring,
          ...colorSpring,
          ...moveSpring,
          width: '80px',
          height: '80px',
          borderRadius: '10px',
          margin: '50px 0',
          cursor: 'pointer',
        }}
      />
    </div>
  );
};

export default SequentialAnimation;

useChain のポイント:

  • 第 1 引数: アニメーションの順序
  • 第 2 引数: 各アニメーションの開始タイミング(0-1 の範囲)

応用:スプリングチェーンの実装

複数の要素が連鎖的にアニメーションする高度な例です。

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

const SpringChain = () => {
  const [isActive, setIsActive] = useState(false);

  const numberOfItems = 8;
  const items = Array.from(
    { length: numberOfItems },
    (_, i) => i
  );

  const springs = useSprings(
    numberOfItems,
    items.map((_, index) => ({
      transform: isActive
        ? `translateY(-${(index + 1) * 20}px) scale(${
            1 + index * 0.1
          })`
        : 'translateY(0px) scale(1)',
      opacity: isActive ? 1 - index * 0.1 : 0.3,
      config: {
        tension: 300 - index * 20, // 後ろほど遅く
        friction: 30 + index * 10, // 後ろほどゆっくり
      },
      delay: index * 100, // 連鎖的に開始
    }))
  );

  return (
    <div style={{ padding: '100px', textAlign: 'center' }}>
      <button onClick={() => setIsActive(!isActive)}>
        チェーンアニメーション
      </button>

      <div
        style={{ position: 'relative', marginTop: '50px' }}
      >
        {springs.map((style, index) => (
          <animated.div
            key={index}
            style={{
              ...style,
              position: 'absolute',
              left: '50%',
              transform: style.transform,
              marginLeft: '-25px',
              width: '50px',
              height: '50px',
              backgroundColor: `hsl(${
                index * 45
              }, 70%, 50%)`,
              borderRadius: '25px',
              zIndex: numberOfItems - index,
            }}
          />
        ))}
      </div>
    </div>
  );
};

export default SpringChain;

高度なテクニック:

  • useSprings: 複数要素を効率的に制御
  • delay: 連鎖効果の演出
  • 動的なconfig: 要素ごとに異なる物理設定

よくあるエラーと解決法:

bash# Warning: Cannot update a component while rendering a different component
# 原因: useSpringsの第2引数で状態更新を行っている
typescript// 問題のあるコード
const springs = useSprings(
  numberOfItems,
  items.map(() => {
    setIsActive(true); // レンダリング中の状態更新は危険
    return { transform: 'translateY(0px)' };
  })
);

// 正しいコード
const springs = useSprings(
  numberOfItems,
  items.map((_, index) => ({
    transform: isActive
      ? 'translateY(-20px)'
      : 'translateY(0px)',
    // 状態更新はイベントハンドラーで行う
  }))
);

メモリリーク対策:

typescriptimport { useEffect } from 'react';

const OptimizedSpringChain = () => {
  const springs = useSprings(/* ... */);

  useEffect(() => {
    // コンポーネントアンマウント時のクリーンアップ
    return () => {
      springs.forEach((spring) => {
        if (spring.stop) spring.stop();
      });
    };
  }, [springs]);

  // ... rest of component
};

まとめ

React Spring 活用の次のステップ

この 3 分間の入門で、React Spring の基本的な使い方を体験できました。次のステップとして、以下の学習を進めることをお勧めします:

実践的なスキルアップ

  • 実際のプロジェクトでボタンアニメーションを実装してみる
  • モーダルやドロワーの開閉に React Spring を使ってみる
  • フォームのバリデーション表示にアニメーションを追加する

より高度な機能の習得

  • useTrail: 複数要素の追従アニメーション
  • useSpringValue: より細かい制御が可能な低レベル API
  • カスタムイージング関数の作成

パフォーマンス最適化

  • immediateプロパティでの即座実行
  • resetプロパティでのアニメーション初期化
  • メモ化を活用した不要な再計算の防止

実用的な応用例

  • ページ遷移アニメーション
  • データビジュアライゼーション
  • ゲーム的な UI 要素

React Spring の魅力は、複雑な物理計算を意識せずに自然な動きを実現できることです。ぜひ今日から実際のプロジェクトで活用して、ユーザーに愛されるインターフェースを作ってみてください!

関連リンク