react-router-transition
Version:
A thin layer over react-motion for animating routes in react-router.
215 lines (181 loc) • 4.68 kB
JavaScript
import React, {
cloneElement,
createElement,
useEffect,
useRef,
useState,
} from 'react';
import PropTypes from 'prop-types';
import { Route, Switch, matchPath, useLocation } from 'react-router-dom';
import TransitionMotion from 'react-motion/lib/TransitionMotion';
import spring from 'react-motion/lib/spring';
// Helpers
function ensureSpring(styles) {
const obj = {};
for (var key in styles) {
const value = styles[key];
if (typeof value === 'number') {
obj[key] = spring(value);
} else {
obj[key] = value;
}
}
return obj;
}
function identity(v) {
return v;
}
function noop() {}
// Components
function RouteTransition({
children,
className,
atEnter,
atActive,
atLeave,
wrapperComponent = 'div',
didLeave = noop,
mapStyles = identity,
runOnMount = false,
}) {
const defaultStyles =
runOnMount === false
? null
: children == undefined
? []
: [
{
key: children.key,
data: children,
style: atEnter,
},
];
const styles =
children == undefined
? []
: [
{
key: children.key,
data: children,
style: ensureSpring(atActive),
},
];
return (
<TransitionMotion
defaultStyles={defaultStyles}
styles={styles}
willEnter={() => atEnter}
willLeave={() => ensureSpring(atLeave)}
didLeave={didLeave}
>
{(interpolatedStyles) => (
<div className={className}>
{interpolatedStyles.map((config) => {
const props = {
style: mapStyles(config.style),
key: config.key,
};
return wrapperComponent !== false
? createElement(wrapperComponent, props, config.data)
: cloneElement(config.data, props);
})}
</div>
)}
</TransitionMotion>
);
}
RouteTransition.propTypes = {
className: PropTypes.string,
wrapperComponent: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.element,
PropTypes.string,
PropTypes.func,
]),
atEnter: PropTypes.object.isRequired,
atActive: PropTypes.object.isRequired,
atLeave: PropTypes.object.isRequired,
didLeave: PropTypes.func,
mapStyles: PropTypes.func,
runOnMount: PropTypes.bool,
};
// AnimatedRoute
// The key-getter for RouteTransition. It's either on or off.
function getKey({ pathname }, path, exact) {
return matchPath(pathname, { exact, path }) ? 'match' : 'no-match';
}
function AnimatedRoute({
render,
component,
path,
exact,
strict,
sensitive,
children,
...routeTransitionProps
}) {
const location = useLocation();
return (
<RouteTransition {...routeTransitionProps}>
<Route
key={getKey(location, path, exact)}
path={path}
exact={exact}
strict={strict}
sensitive={sensitive}
location={location}
component={component}
render={render}
children={children}
/>
</RouteTransition>
);
}
// AnimatedSwitch
const NO_MATCH = {
key: 'no-match',
};
// Not every location object has a `key` property (e.g. HashHistory).
function getLocationKey(location) {
return typeof location.key === 'string' ? location.key : '';
}
// Some superfluous work, but something we need to do in order
// to persist matches/allow for nesting/etc.
function getMatchedRoute(children, { pathname }) {
const childrenArray = React.Children.toArray(children);
for (let i = 0; i < childrenArray.length; i++) {
const child = childrenArray[i];
const matches = matchPath(pathname, {
exact: child.props.exact,
path: child.props.path,
});
if (matches) {
return child;
}
}
return NO_MATCH;
}
let counter = 0;
function AnimatedSwitch({ children, ...routeTransitionProps }) {
const location = useLocation();
const match = useRef(null);
const key = useRef(null);
const nextMatch = getMatchedRoute(children, location);
if (match.current === null) {
// Persist a reference to the most recent match
match.current = nextMatch;
key.current = getLocationKey(location);
} else if (match.current.key !== nextMatch.key) {
// Update the key given to Switch anytime the matched route changes
match.current = nextMatch;
key.current = getLocationKey(location) + ++counter;
}
return (
<RouteTransition {...routeTransitionProps}>
<Switch key={key.current} location={location}>
{children}
</Switch>
</RouteTransition>
);
}
export { ensureSpring, spring, RouteTransition, AnimatedRoute, AnimatedSwitch };