ページ遷移も美しく!React Router × アニメーション最強パターン

Web 開発者の皆さん、ページ遷移でユーザーの心を掴んだ瞬間はありますか?
「あ、このサイト使いやすい!」そんな感動をユーザーに与えるのが、美しいページ遷移アニメーションの力です。今回は、React Router とアニメーションライブラリを組み合わせた最強パターンを、実際のコード例とともに徹底解説いたします。
単調なページ切り替えから卒業し、ユーザー体験を格段に向上させる技術を身につけましょう。
背景
従来のページ遷移の課題
従来の Web アプリケーションでは、ページ間の遷移は瞬間的な切り替えが主流でした。しかし、この「パッ」と切り替わる体験は、ユーザーにとって少し味気ないものです。
スマートフォンアプリでは当たり前になった滑らかな画面遷移が、Web でも求められるようになってきました。特に、以下のような課題が顕在化しています:
# | 課題 | 影響 |
---|---|---|
1 | 突然のページ切り替えによる違和感 | ユーザーの離脱率向上 |
2 | どこからどこへ移動したかわからない | ナビゲーション体験の低下 |
3 | 読み込み中の空白時間 | ユーザーの待機ストレス |
モダン Web アプリケーションでのアニメーション需要
現代の Web アプリケーションでは、デザイナーとの協業が不可欠です。デザイナーが Figma や Adobe XD で美しいプロトタイプを作成しても、実装時に「技術的に難しい」と断念するケースも多いのではないでしょうか。
React Router とアニメーションライブラリの組み合わせを習得することで、デザイナーの想いを 100%実現できるようになります。
React Router の基本概念とアニメーション連携の重要性
React Router は、単なるルーティングツール以上の価値を持っています。コンポーネント間の状態管理、URL と UI の同期、そしてアニメーション制御の基盤として機能します。
しかし、React Router 単体では、以下の制約があります:
typescript// React Router単体での基本的な遷移
import { Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<div>
<nav>
<Link to='/'>Home</Link>
<Link to='/about'>About</Link>
</nav>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</div>
);
}
この実装では、ページが瞬時に切り替わるだけで、ユーザーに「流れ」を感じさせることができません。
課題
React Router の標準機能だけでは実現困難な滑らかな遷移
React Router の標準機能は素晴らしいものですが、アニメーション面では限界があります。実際に遭遇する課題をご紹介しましょう。
よくあるエラー 1:アニメーション途中でのアンマウント
typescript// ❌ 問題のあるコード
function AnimatedRoute() {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
// アニメーション開始
setIsVisible(false);
// 200ms後にアンマウント... でもまだアニメーション中!
setTimeout(() => {
navigate('/new-page');
}, 200);
}, []);
return (
<div
className={`fade-out ${isVisible ? 'visible' : ''}`}
>
Content
</div>
);
}
このコードを実行すると、以下のエラーが発生することがあります:
vbnetWarning: 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.
パフォーマンスとアニメーションのトレードオフ
美しいアニメーションを実現したい気持ちと、パフォーマンスを保ちたい気持ちの間で悩むことはありませんか?
特に、以下のような状況では顕著に現れます:
# | 状況 | パフォーマンス影響 |
---|---|---|
1 | 大量の DOM 要素をアニメーション | 60FPS 維持困難 |
2 | 複数のアニメーションを同時実行 | CPU 使用率上昇 |
3 | モバイルデバイスでの表示 | バッテリー消費増大 |
複雑なルーティング構造でのアニメーション制御
ネストされたルートや動的ルートを含む複雑なアプリケーションでは、アニメーション制御が困難になります。
typescript// 複雑なルーティング構造の例
<Routes>
<Route path='/' element={<Layout />}>
<Route index element={<Home />} />
<Route path='users' element={<Users />}>
<Route path=':id' element={<UserDetail />} />
<Route path=':id/edit' element={<UserEdit />} />
</Route>
<Route path='products' element={<Products />}>
<Route
path=':category'
element={<ProductsByCategory />}
/>
</Route>
</Route>
</Routes>
このような構造で、親子関係を保ちながら適切なアニメーションを実現するのは、標準機能だけでは至難の業です。
解決策
React Router × アニメーションライブラリの選択肢
問題を解決するため、4 つの主要なアプローチをご紹介します。それぞれに特徴があり、プロジェクトの要件に応じて選択できます。
Framer Motion × React Router
特徴: 直感的な API、豊富なアニメーションプリセット、優れたパフォーマンス
typescript// Framer Motionの基本セットアップ
import { motion, AnimatePresence } from 'framer-motion';
import { useLocation } from 'react-router-dom';
function AnimatedRoutes() {
const location = useLocation();
return (
<AnimatePresence mode='wait'>
<motion.div
key={location.pathname}
initial={{ opacity: 0, x: -200 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 200 }}
transition={{ duration: 0.3 }}
>
<Routes location={location}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</motion.div>
</AnimatePresence>
);
}
メリット:
- 学習コストが低い
- TypeScript 完全対応
- 豊富なドキュメント
デメリット:
- バンドルサイズが大きい(約 20KB)
- 高度なカスタマイズには制限がある
React Transition Group × React Router
特徴: 軽量、細かい制御が可能、長い運用実績
typescript// React Transition Groupの実装例
import {
CSSTransition,
TransitionGroup,
} from 'react-transition-group';
function AnimatedRoutes() {
const location = useLocation();
return (
<TransitionGroup>
<CSSTransition
key={location.key}
timeout={300}
classNames='fade'
>
<Routes location={location}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</CSSTransition>
</TransitionGroup>
);
}
対応する CSS:
css.fade-enter {
opacity: 0;
transform: translateX(-20px);
}
.fade-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.fade-exit {
opacity: 1;
transform: translateX(0);
}
.fade-exit-active {
opacity: 0;
transform: translateX(20px);
transition: opacity 300ms, transform 300ms;
}
メリット:
- 軽量(約 5KB)
- CSS 完全制御
- 枯れた技術で安定性が高い
デメリット:
- CSS 記述が冗長
- 複雑なアニメーションには不向き
React Spring × React Router
特徴: 物理ベースアニメーション、高いカスタマイズ性、自然な動き
typescript// React Springの実装例
import { useTransition, animated } from '@react-spring/web';
function AnimatedRoutes() {
const location = useLocation();
const transitions = useTransition(location, {
from: {
opacity: 0,
transform: 'translate3d(100%, 0, 0)',
},
enter: {
opacity: 1,
transform: 'translate3d(0%, 0, 0)',
},
leave: {
opacity: 0,
transform: 'translate3d(-50%, 0, 0)',
},
config: { mass: 1, tension: 280, friction: 60 },
});
return transitions((style, item) => (
<animated.div style={style}>
<Routes location={item}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</animated.div>
));
}
メリット:
- 物理的に自然な動き
- 高いパフォーマンス
- 柔軟なカスタマイズ
デメリット:
- 学習コストが高い
- 設定が複雑
CSS Modules × React Router
特徴: 軽量、シンプル、CSS-in-JS 不要
typescript// CSS Modulesの実装例
import styles from './PageTransition.module.css';
function AnimatedRoutes() {
const location = useLocation();
const [displayLocation, setDisplayLocation] =
useState(location);
const [transitionStage, setTransitionStage] =
useState('fadeIn');
useEffect(() => {
if (location !== displayLocation) {
setTransitionStage('fadeOut');
}
}, [location, displayLocation]);
return (
<div
className={`${styles.pageTransition} ${styles[transitionStage]}`}
onAnimationEnd={() => {
if (transitionStage === 'fadeOut') {
setDisplayLocation(location);
setTransitionStage('fadeIn');
}
}}
>
<Routes location={displayLocation}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</div>
);
}
メリット:
- 最軽量
- CSS 完全制御
- 依存関係なし
デメリット:
- 実装が複雑
- 状態管理が必要
各ライブラリの特徴と使い分け
選択に迷った時は、以下の基準で判断することをお勧めします:
条件 | 推奨ライブラリ | 理由 |
---|---|---|
初学者・プロトタイピング | Framer Motion | 直感的で学習コストが低い |
パフォーマンス重視 | React Spring | 物理ベースで自然な動き |
軽量性重視 | CSS Modules | バンドルサイズを最小化 |
安定性重視 | React Transition Group | 枯れた技術で実績豊富 |
具体例
Framer Motion を使った基本実装
まずは最も人気の高い Framer Motion から始めましょう。実際のプロジェクトセットアップから解説いたします。
Step 1: 依存関係のインストール
bashyarn add framer-motion react-router-dom
yarn add -D @types/react-router-dom
Step 2: 基本的なアニメーション実装
typescript// App.tsx
import React from 'react';
import {
BrowserRouter as Router,
Routes,
Route,
} from 'react-router-dom';
import { AnimatePresence } from 'framer-motion';
import AnimatedRoutes from './components/AnimatedRoutes';
import Navigation from './components/Navigation';
function App() {
return (
<Router>
<div className='app'>
<Navigation />
<AnimatedRoutes />
</div>
</Router>
);
}
export default App;
Step 3: アニメーションコンポーネントの作成
typescript// components/AnimatedRoutes.tsx
import React from 'react';
import {
Routes,
Route,
useLocation,
} from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import Home from '../pages/Home';
import About from '../pages/About';
import Contact from '../pages/Contact';
// アニメーションの設定を定数として定義
const pageVariants = {
initial: {
opacity: 0,
x: '-100vw',
},
in: {
opacity: 1,
x: 0,
},
out: {
opacity: 0,
x: '100vw',
},
};
const pageTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.5,
};
function AnimatedRoutes() {
const location = useLocation();
return (
<AnimatePresence mode='wait'>
<motion.div
key={location.pathname}
initial='initial'
animate='in'
exit='out'
variants={pageVariants}
transition={pageTransition}
style={{
position: 'absolute',
width: '100%',
minHeight: '100vh',
}}
>
<Routes location={location}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/contact' element={<Contact />} />
</Routes>
</motion.div>
</AnimatePresence>
);
}
export default AnimatedRoutes;
よくあるエラーと解決方法
実装中に以下のエラーが発生することがあります:
sqlError: AnimatePresence can only have one child at a time.
Did you accidentally render multiple elements at once?
このエラーは、AnimatePresence
の子要素が複数存在する場合に発生します。解決方法:
typescript// ❌ 間違った実装
<AnimatePresence>
<motion.div key={location.pathname}>
<Routes location={location}>
<Route path="/" element={<Home />} />
</Routes>
</motion.div>
<div>Other content</div> {/* これが原因 */}
</AnimatePresence>
// ✅ 正しい実装
<AnimatePresence mode="wait">
<motion.div key={location.pathname}>
<Routes location={location}>
<Route path="/" element={<Home />} />
</Routes>
</motion.div>
</AnimatePresence>
React Transition Group によるクラシカルな実装
React Transition Group は、CSS 主体のアニメーションを提供します。細かい制御が可能で、既存の CSS アニメーションを活用できます。
Step 1: セットアップ
bashyarn add react-transition-group
yarn add -D @types/react-transition-group
Step 2: 基本実装
typescript// components/CSSTransitionRoutes.tsx
import React from 'react';
import {
Routes,
Route,
useLocation,
} from 'react-router-dom';
import {
CSSTransition,
TransitionGroup,
} from 'react-transition-group';
import './PageTransition.css';
function CSSTransitionRoutes() {
const location = useLocation();
return (
<TransitionGroup>
<CSSTransition
key={location.key}
timeout={500}
classNames='page'
unmountOnExit
>
<div className='page-container'>
<Routes location={location}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/contact' element={<Contact />} />
</Routes>
</div>
</CSSTransition>
</TransitionGroup>
);
}
export default CSSTransitionRoutes;
Step 3: CSS アニメーションの定義
css/* PageTransition.css */
.page-container {
position: absolute;
width: 100%;
min-height: 100vh;
top: 0;
left: 0;
}
/* 登場時のアニメーション */
.page-enter {
opacity: 0;
transform: translateX(100%);
}
.page-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* 退場時のアニメーション */
.page-exit {
opacity: 1;
transform: translateX(0);
}
.page-exit-active {
opacity: 0;
transform: translateX(-100%);
transition: opacity 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
transform 500ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
高度なアニメーションパターン
方向を意識したアニメーションを実装してみましょう:
typescript// hooks/usePageDirection.ts
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
const routeOrder = ['/', '/about', '/contact'];
export function usePageDirection() {
const location = useLocation();
const [direction, setDirection] = useState<
'forward' | 'backward'
>('forward');
const [prevPath, setPrevPath] = useState(
location.pathname
);
useEffect(() => {
const currentIndex = routeOrder.indexOf(
location.pathname
);
const prevIndex = routeOrder.indexOf(prevPath);
if (currentIndex > prevIndex) {
setDirection('forward');
} else if (currentIndex < prevIndex) {
setDirection('backward');
}
setPrevPath(location.pathname);
}, [location.pathname, prevPath]);
return direction;
}
この方向を使って、より自然なアニメーションを実現できます:
typescript// components/DirectionalRoutes.tsx
import React from 'react';
import {
CSSTransition,
TransitionGroup,
} from 'react-transition-group';
import { usePageDirection } from '../hooks/usePageDirection';
function DirectionalRoutes() {
const location = useLocation();
const direction = usePageDirection();
return (
<TransitionGroup>
<CSSTransition
key={location.key}
timeout={400}
classNames={`page-${direction}`}
unmountOnExit
>
<div className='page-container'>
<Routes location={location}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/contact' element={<Contact />} />
</Routes>
</div>
</CSSTransition>
</TransitionGroup>
);
}
React Spring での自然なアニメーション
React Spring は、物理法則に基づいた自然なアニメーションを提供します。バネの動きを模倣したアニメーションは、ユーザーに心地よい体験を提供します。
Step 1: セットアップ
bashyarn add @react-spring/web
Step 2: 基本実装
typescript// components/SpringRoutes.tsx
import React from 'react';
import {
Routes,
Route,
useLocation,
} from 'react-router-dom';
import { useTransition, animated } from '@react-spring/web';
function SpringRoutes() {
const location = useLocation();
const transitions = useTransition(location, {
from: {
opacity: 0,
transform: 'translate3d(100%, 0, 0) scale(0.8)',
},
enter: {
opacity: 1,
transform: 'translate3d(0%, 0, 0) scale(1)',
},
leave: {
opacity: 0,
transform: 'translate3d(-50%, 0, 0) scale(1.2)',
},
config: {
mass: 1,
tension: 280,
friction: 60,
},
exitBeforeEnter: true,
});
return transitions((style, item) => (
<animated.div
style={{
...style,
position: 'absolute',
width: '100%',
minHeight: '100vh',
}}
>
<Routes location={item}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/contact' element={<Contact />} />
</Routes>
</animated.div>
));
}
export default SpringRoutes;
物理パラメータの調整
React Spring の魅力は、物理パラメータを調整することで様々な「感触」を作れることです:
typescript// 異なる物理設定の例
const springConfigs = {
// 軽やかで素早い動き
gentle: { mass: 1, tension: 120, friction: 14 },
// 重厚で安定した動き
wobbly: { mass: 1, tension: 180, friction: 12 },
// 機械的で正確な動き
stiff: { mass: 1, tension: 210, friction: 20 },
// ゆっくりと優雅な動き
slow: { mass: 1, tension: 280, friction: 120 },
};
パフォーマンス最適化テクニック
美しいアニメーションを実現しながら、パフォーマンスを保つための実践的なテクニックをご紹介します。
1. GPU 加速の活用
css/* GPU加速を有効化 */
.page-container {
transform: translateZ(0);
will-change: transform, opacity;
}
/* アニメーション完了後にwill-changeを解除 */
.page-enter-done,
.page-exit-done {
will-change: auto;
}
2. React.memo と useMemo の活用
typescript// components/OptimizedPage.tsx
import React, { memo } from 'react';
const OptimizedPage = memo(
({ children }: { children: React.ReactNode }) => {
return <div className='page-content'>{children}</div>;
}
);
// 重い計算を含むコンポーネントの最適化
const HeavyComponent = memo(() => {
const expensiveValue = useMemo(() => {
return performExpensiveCalculation();
}, []);
return <div>{expensiveValue}</div>;
});
3. 遅延読み込みの実装
typescript// components/LazyRoutes.tsx
import React, { Suspense, lazy } from 'react';
import { motion } from 'framer-motion';
// 遅延読み込み対応のコンポーネント
const Home = lazy(() => import('../pages/Home'));
const About = lazy(() => import('../pages/About'));
const Contact = lazy(() => import('../pages/Contact'));
function LazyRoutes() {
return (
<Suspense fallback={<LoadingSpinner />}>
<AnimatePresence mode='wait'>
<motion.div
key={location.pathname}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<Routes location={location}>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/contact' element={<Contact />} />
</Routes>
</motion.div>
</AnimatePresence>
</Suspense>
);
}
4. パフォーマンス監視
typescript// hooks/usePerformanceMonitor.ts
import { useEffect } from 'react';
export function usePerformanceMonitor() {
useEffect(() => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry) => {
if (entry.name === 'measure-route-change') {
console.log(`Route change took: ${entry.duration}ms`);
// 閾値を超えた場合の警告
if (entry.duration > 100) {
console.warn('Route change is taking too long!');
}
}
});
});
observer.observe({ entryTypes: ['measure'] });
return () => observer.disconnect();
}, []);
}
// 使用例
function App() {
usePerformanceMonitor();
const handleRouteChange = () => {
performance.mark('route-change-start');
// ルート変更処理
performance.mark('route-change-end');
performance.measure('measure-route-change', 'route-change-start', 'route-change-end');
};
return (
// アプリケーションコンテンツ
);
}
実際のエラーケースと対処法
パフォーマンス最適化時によく遭遇するエラーをご紹介します:
sqlWarning: Each child in a list should have a unique "key" prop.
Check the top-level render call using <TransitionGroup>.
このエラーの解決方法:
typescript// ❌ 問題のあるコード
<TransitionGroup>
{routes.map((route) => (
<CSSTransition timeout={300} classNames="fade">
<Route path={route.path} element={route.element} />
</CSSTransition>
))}
</TransitionGroup>
// ✅ 正しい実装
<TransitionGroup>
{routes.map((route) => (
<CSSTransition
key={route.path} // key を追加
timeout={300}
classNames="fade"
>
<Route path={route.path} element={route.element} />
</CSSTransition>
))}
</TransitionGroup>
まとめ
React Router とアニメーションライブラリの組み合わせは、ユーザー体験を劇的に向上させる強力な手法です。
最適なライブラリ選択の指針
プロジェクトの特性に応じて、以下の指針で選択することをお勧めします:
開発チームの経験レベル
- 初心者チーム: Framer Motion
- 中級者チーム: React Transition Group
- 上級者チーム: React Spring
プロジェクトの要件
- プロトタイピング: Framer Motion
- プロダクション: React Transition Group
- 高度なアニメーション: React Spring
- 軽量性重視: CSS Modules
ベストプラクティス
-
パフォーマンスを最優先に
- GPU 加速を活用する
- 不要なリレンダリングを避ける
- 適切な遅延読み込みを実装する
-
ユーザビリティを考慮
- アニメーション時間は 0.3 秒以内に
- 一貫性のある動きを保つ
- アクセシビリティに配慮する
-
保守性を重視
- アニメーション設定を定数化する
- 再利用可能なコンポーネントを作成する
- 適切なエラーハンドリングを実装する
最後に
美しいページ遷移アニメーションは、技術的な挑戦であると同時に、ユーザーの心を動かすアートでもあります。
今回ご紹介したテクニックを活用して、ユーザーが「使っていて気持ちいい」と感じる Web アプリケーションを作成してください。小さな工夫の積み重ねが、大きな差を生み出します。
あなたのプロジェクトが、ユーザーにとって忘れられない体験を提供できることを心から願っています。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来