Table of Contents#
- Prerequisites
- Understanding React-Router v6 Changes
- Setting Up
react-transition-group - Integrating Transitions with React-Router v6
- Creating CSS Transition Classes
- Handling Edge Cases
- Complete Example
- Common Issues and Solutions
- Conclusion
- References
Prerequisites#
Before diving in, ensure you have the following:
- A React project set up (v16.8+ for Hooks support).
react-router-domv6 installed (npm install react-router-dom@6).- Basic knowledge of React Hooks (e.g.,
useState,useEffect,useLocation). - Familiarity with CSS transitions/animations.
We’ll also use react-transition-group (v4+), the official library for managing component transitions in React.
Understanding React-Router v6 Changes#
To solve compatibility issues, it’s critical to understand what changed in React-Router v6:
| v5 and Earlier | v6 | Impact on Transitions |
|---|---|---|
Switch component | Replaced with Routes | Routes renders the first matching Route, but unlike Switch, it does not unmount components by default. |
component/render props | Replaced with element prop | Routes now render components via <Route path="/" element={<Home />} />. |
useHistory hook | Replaced with useNavigate | Navigation is now handled via navigate(), but location tracking still uses useLocation. |
The biggest challenge for transitions is that Routes does not automatically unmount inactive routes. Without unmounting, react-transition-group can’t detect when to trigger exit animations. We’ll solve this by combining useLocation (to track route changes) with TransitionGroup (to manage enter/exit states).
Setting Up react-transition-group#
CSSTransitionGroup was deprecated in react-transition-group v2. Today, we use TransitionGroup (to manage a list of transitioning components) and CSSTransition (to apply CSS classes during transitions).
First, install the library:
npm install react-transition-group@4 # v4 is stable and widely usedImport the required components in your app:
import { TransitionGroup, CSSTransition } from 'react-transition-group';Integrating Transitions with React-Router v6#
The core idea is to wrap Routes in TransitionGroup and use CSSTransition to animate route entries/exits. Here’s a step-by-step breakdown:
Step 1: Track Location Changes with useLocation#
useLocation returns the current location object, which updates whenever the route changes. We’ll use this to trigger transitions:
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation(); // Tracks current route
// ...
}Step 2: Wrap Routes in TransitionGroup#
TransitionGroup manages the lifecycle of transitioning components. Each child must have a unique key to trigger re-renders when the route changes. We’ll use location.pathname as the key (unique per route):
<TransitionGroup>
{/* CSSTransition wraps the Routes */}
<CSSTransition
key={location.pathname} {/* Unique key for each route */}
timeout={300} {/* Duration of enter/exit animations (ms) */}
classNames="fade" {/* Prefix for CSS classes (e.g., fade-enter) */}
unmountOnExit {/* Unmount component after exit animation */}
>
{/* Pass location to Routes to ensure it re-renders on route change */}
<Routes location={location}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</CSSTransition>
</TransitionGroup>Key Props Explained:#
key={location.pathname}: EnsuresTransitionGrouprecognizes route changes as new children, triggering exit for the old route and enter for the new.timeout: Matches the duration of your CSS transitions (e.g., 300ms for a fade).classNames: Prefix for CSS classes (e.g.,fade-enter,fade-exit-active).unmountOnExit: Unmounts the old route component after the exit animation completes (critical for triggering transitions).location={location}: ForcesRoutesto re-render when the location changes, ensuring the new route content loads.
Creating CSS Transition Classes#
CSSTransition adds/removes CSS classes at specific stages of the transition. For a classNames="fade" prefix, define these classes:
| Class | Timing | Purpose |
|---|---|---|
fade-enter | Applied immediately when entering. | Define the initial state (e.g., opacity: 0). |
fade-enter-active | Applied after enter (with a timeout). | Define the final state (e.g., opacity: 1) and transition properties. |
fade-exit | Applied immediately when exiting. | Define the initial exit state (e.g., opacity: 1). |
fade-exit-active | Applied after exit (with a timeout). | Define the final exit state (e.g., opacity: 0) and transition properties. |
Example CSS (save as App.css):
/* Fade transition */
.fade-enter {
opacity: 0;
transform: translateY(20px); /* Optional: Add slide effect */
}
.fade-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 300ms, transform 300ms; /* Match CSSTransition timeout */
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: opacity 300ms;
}Handling Edge Cases#
1. Disabling Initial Mount Animation#
By default, the first route will trigger an enter animation on app load. To disable this:
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
// Skip animation on initial mount
const timer = setTimeout(() => setIsMounted(true), 10);
return () => clearTimeout(timer);
}, []);
// Pass `in={isMounted}` to CSSTransition
<CSSTransition
key={location.pathname}
in={isMounted} {/* Only animate after initial mount */}
timeout={300}
classNames="fade"
unmountOnExit
>
{/* ... */}
</CSSTransition>2. Directional Transitions (e.g., Slide Left/Right)#
To animate based on navigation direction (forward/back), use location.state to track direction:
Step 1: Pass direction in navigation links:
import { Link } from 'react-router-dom';
<Link to="/about" state={{ direction: 'forward' }}>About</Link>
<Link to="/" state={{ direction: 'back' }}>Home</Link>Step 2: Dynamically set classNames based on direction:
const { state } = location;
const direction = state?.direction || 'forward'; // Default to forward
<CSSTransition
key={location.pathname}
classNames={direction === 'forward' ? 'slide-right' : 'slide-left'}
timeout={300}
unmountOnExit
>
{/* ... */}
</CSSTransition>Step 3: Add CSS for slide-right and slide-left:
/* Slide right (forward navigation) */
.slide-right-enter { transform: translateX(100%); }
.slide-right-enter-active { transform: translateX(0); transition: transform 300ms; }
.slide-right-exit { transform: translateX(0); }
.slide-right-exit-active { transform: translateX(-100%); transition: transform 300ms; }
/* Slide left (back navigation) */
.slide-left-enter { transform: translateX(-100%); }
.slide-left-enter-active { transform: translateX(0); transition: transform 300ms; }
.slide-left-exit { transform: translateX(0); }
.slide-left-exit-active { transform: translateX(100%); transition: transform 300ms; }Complete Example#
Here’s the full App.js integrating all the above:
import { useState, useEffect } from 'react';
import { Routes, Route, useLocation, Link } from 'react-router-dom';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import './App.css';
// Sample pages
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const Contact = () => <h1>Contact</h1>;
function App() {
const location = useLocation();
const [isMounted, setIsMounted] = useState(false);
// Disable initial animation
useEffect(() => {
const timer = setTimeout(() => setIsMounted(true), 10);
return () => clearTimeout(timer);
}, []);
return (
<div>
{/* Navigation */}
<nav>
<Link to="/" state={{ direction: 'back' }}>Home</Link> |
<Link to="/about" state={{ direction: 'forward' }}>About</Link> |
<Link to="/contact" state={{ direction: 'forward' }}>Contact</Link>
</nav>
{/* Transition wrapper */}
<TransitionGroup>
<CSSTransition
key={location.pathname}
in={isMounted}
timeout={300}
classNames={location.state?.direction === 'back' ? 'slide-left' : 'slide-right'}
unmountOnExit
>
<Routes location={location}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</CSSTransition>
</TransitionGroup>
</div>
);
}
export default App;Common Issues and Solutions#
| Issue | Solution |
|---|---|
| Transitions not triggering | Ensure key={location.pathname} is set on CSSTransition, and location is passed to Routes. |
| Exit animations not running | Add unmountOnExit to CSSTransition to unmount inactive routes. |
| CSS classes not applying | Verify classNames prop matches your CSS prefix (e.g., classNames="fade" → .fade-enter). |
| Initial mount animation | Use the isMounted state trick to skip the first animation. |
Conclusion#
By combining react-transition-group with React-Router v6’s useLocation and Routes, you can create smooth, customizable route transitions. The key steps are:
- Track route changes with
useLocation. - Wrap
RoutesinTransitionGroupandCSSTransition. - Define CSS classes for enter/exit states.
- Handle edge cases like initial mounts and directional animations.
With these tools, you can elevate your app’s UX and keep up with React-Router’s evolving API.