React Transition Group
React Transition Group предоставляет простые компоненты для легкой реализации переходов (transitions) при рендеринге компонентов.
React Transition Groupне является библиотекой для анимации, вместо этого, он обрабатывает стадии перехода, управляет классами, группирует элементы и манипулирует DOM наиболее эффективным образом, облегчая реализацию визуальных переходов.
Установка
yarn add react-transition-group
# или
npm i react-transition-group
Компоненты
Transition
Компонент Transition позволяет описывать переход компонента из одного состояния в другое в течение времени с помощью простого декларативного синтаксиса. Он, обычно, используется для анимирования монтирования и размонтирования компонентов, но также может использоваться для описания других переходных состояний.
Обратите внимание: Transition - это платформонезависимый базовый компонент. В случае с CSS-переходами лучше использовать CSSTransition. Он наследует все возможности Transition, а также содержит дополнительный функционал для реализации CSS-переходов.
По умолчанию Transition не меняет поведение компонента, за рендеринг которого он отвечает, он лишь следит за состояниями "входа" (enter) и "выхода" (exit) этого компонента. Осмысление названных состояний и применение эффектов - это задача разработчика. Например, мы можем добавлять стили при входе и выходе компонента следующим образом:
import { Transition } from 'react-transition-group'
const duration = 300
const defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
}
const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
}
const Fade = ({ open }) => (
<Transition in={open} timeout={duration}>
{(state) => (
<div
style={{
...defaultStyle,
...transitionStyles[state],
}}
>
Аз есмь появление/исчезновение!
</div>
)}
</Transition>
)
Существует 4 основных состояния перехода:
enteringenteredexitingexited
Эти состояния переключаются с помощью пропа in. При значении данного пропа, равном true, начинается вхождение компонента. На этой стадии компонент переходит к состоянию entering, а после завершения перехода к состоянию entered. Рассмотрим пример:
function App() {
const [open, setOpen] = useState(false)
return (
<div>
<Transition in={open} timeout={500}>
{state => (
// ...
)}
</Transition>
<button onClick={() => setOpen(true)}>
Нажмите для начала вхождения компонента
</button>
</div>
)
}
При нажатии кнопки компонент перейдет к состоянию entering и будет находиться в этом состоянии на протяжении 500 мс (значение timeout), после чего перейдет в состояние entered.
При значении in, равном false, происходит то же самое, только состояние меняется от exiting до exited.
Пропы
-
nodeRef- ссылка на DOM-элемент, который "нуждается" в переходе. При использовании данного пропа,nodeне передается в колбеки (onEnterи др.), поскольку пользователь уже имеет прямой доступ к узлу. При изменении пропаkeyкомпонентаTransitionвTransitionGroup, вTransitionдолжен быть передан новыйnodeRef -
children- вместо React-элемента может быть использован функциональный компонент. Функция вызывается с текущим статусом перехода (entering,entered,exiting,exited), что может быть использовано для применения к компоненту пропов, зависящих от контекста:
<Transition in={open} timeout={150}>
{(state) => <MyComponent className={`fade fade-${state}`} />}
</Transition>
-
in- переключает состояния компонента -
mountOnEnter- по умолчанию дочерний компонент монтируется вместе с родительскимTransition. Для того, чтобы монтировать компонент "лениво" при первомin={true}, следует установитьmountOnEnter. После первого вхождения, компонент останется смонтированным, если не определеноunmountOnExit -
unmountOnExit- по умолчанию дочерний компонент остается смонтированным при достижении состоянияexited. УстановкаunmountOnExitозначает, что компонент будет размонтирован после выхода -
appear- по умолчанию вхождение элемента при первом монтировании не анимируется, независимо от значенияin. Для изменения этого поведения, следует установитьappearиinв значениеtrue
Обратите внимание: это не добавляет особых состояний, например, appearing/appeared, это добавляет лишь дополнительный переход вхождения. Тем не менее, в компоненте CSSTransition переход первого вхождения компонента представлен классами .appear-*, что предоставляет возможность их особой стилизации
-
enter- включает/отключает переходы входа -
exit- включает/отключает переходы выхода -
timeout- продолжительность перехода в мс. Требуется, если не переданaddEndListener. Можно определять одинtimeoutдля всех переходов:
timeout={500}
или для каждого перехода в отдельности:
timeout={{
appear: 500,
enter: 300,
exit: 500
}}
-
appear- по умолчанию имеет такое же значение, что иenter -
enter- по умочланию имеет значение0 -
exit- по умолчанию имеет значение0 -
addEndListener- добавляет пользовательский обработчик окончания перехода. Вызывается с соответствующим узлом DOM и колбекомdone. Позволяет реализовать более сложную логику окончания перехода. В этом случаеtimeoutиспользуется в качестве запасного варианта
Обратите внимание: при передаче пропа nodeRef, addEndListener не получает аргумента node:
addEndListener={(node, done) => {
// Используем CSS-событие `transitionend` в качестве индикатора окончания перехода
node.addEventListener('transitionend', done, false)
}}
-
onEnter- колбек, вызываемый перед применением статусаentering. Колбек получает дополнительный параметрisAppearingв качестве индикатора вхождения при первоначальном монтировании:onEnter={(node, isAppearing) => {}}. При наличииnodeRef,nodeне передается -
onEntering- колбек, вызываемый после применения статусаentering:onEntering={(node, isAppearing) => {}} -
onEntered- колбек, вызываемый после применения статусаentered:onEntered={(node, isAppearing) => {}} -
onExit- колбек, вызываемый перед применением статусаexiting:onExit={(node) => {}} -
onExiting- колбек, вызываемый после применения статусаexiting:onExiting={(node) => {}} -
onExited- колбек, вызываемый после применения статусаexited:onExited={(node) => {}}
CSSTransition
Данный компонент следует использовать при реализации переходов и анимации с помощью CSS. Он расширяет возможности Transition и наследует все его пропы.
CSSTransition применяет CSS-классы для состояний appear, enter и exit. Сначала применяется первый класс, например, className-enter, где className - произвольное название класса, указанное в пропе classNames, затем класс *-active, например, enter-active, свидетельствующий о начале перехода, после завершения перехода для фиксации состояния применяется класс *-done, например, enter-done.
function App() {
const [open, setOpen] = useState(false)
return (
<div>
<CSSTransition in={open} timeout={200} classNames='my-node'>
<div>{'Я буду получать классы my-node-*'}</div>
</CSSTransition>
</div>
)
}
CSSTransition запускает принудительную перерисовку, между enter и enter-active. Это позволяет выполнять переход между указанными состояниями, несмотря на то, что они добавляются почти одновременно. В частности, это делает возможным анимирование появления компонента.
.my-node-enter {
opacity: 0;
}
.my-node-enter-active {
opacity: 1;
transition: opacity 200ms;
}
.my-node-exit {
opacity: 1;
}
.my-node-exit-active {
opacity: 0;
transition: opacity 200ms;
}
Классы *-active представляют стили, которые будут анимироваться, поэтому свойство transition должно определяться только в них, в противном случае переход может быть неожиданным! Обычно, это не вызывает проблем при симметричности переходов, т.е. когда *-enter-active и *-exit являются одинаковыми, как в приведенном выше примере, но в более сложных случаях это становится критически важным.
Обратите внимание: при использовании пропа appear, убедитесь в определении стилей для классов *-appear.
Пример
import { useState, useRef } from 'react'
import { Container, Button, Alert } from 'react-bootstrap'
import { CSSTransition } from 'react-transition-group'
import './styles.css'
function Example() {
const [showButton, setShowButton] = useState(true)
const [showMessage, setShowMessage] = useState(false)
const ref = useRef()
return (
<Container className='pt-4'>
{showButton && (
<Button onClick={() => setShowMessage(true)}>Показать сообщение</Button>
)}
<CSSTransition
in={showMessage}
timeout={300}
classNames='alert'
unmountOnExit
onEnter={() => setShowButton(false)}
onExited={() => setShowButton(true)}
nodeRef={ref}
>
<Alert variant='primary' style={{ maxWidth: '480px' }} ref={ref}>
<Alert.Heading>Анимированное сообщение</Alert.Heading>
<p>Данное сообщение анимируется с помощью переходов</p>
<Button onClick={() => setShowMessage(false)}>Закрыть</Button>
</Alert>
</CSSTransition>
</Container>
)
}
export default Example
.alert-enter {
opacity: 0;
transform: scale(0.9);
}
.alert-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.alert-exit {
opacity: 1;
}
.alert-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}
Пропы
classNames- названия CSS-классов, применяемых при появлении, вхождении, выходе компонента, а также при завершении перехода. Может быть указано одно название, к которому через дефис будут добавляться соответствующие классы. Например, если указаноclassNames="fade:- fade-appear, fade-appear-active, fade-appear-done
- fade-enter, fade-enter-active, fade-enter-done
- fade-exit, fade-exit-active, fade-exit-done
Несколько слов о том, как применяются эти классы:
- Они объединяются с существующими классами компонента, так что если вы хотите определить некоторые базовые стили, то можете использовать
className, не опасаясь, что базовые стили будут перезаписаны. - Если компонент монтируется с
in={false}, никакие классы к нему не применяются (включая*-exit-done). fade-appear-doneиfade-enter-doneприменяются одновременно. Это позволяет определять различное поведение при завершении появления и обычного вхождения с помощью таких селекторов, как.fade-enter-done:not(.fade-appear-done). В противном случае, вы можете использоватьfade-enter-doneдля обработки обоих случаев.
Каждое название класса может быть определено отдельно:
classNames={{
appear: 'my-appear',
appearActive: 'my-active-appear',
appearDone: 'my-done-appear',
enter: 'my-enter',
enterActive: 'my-active-enter',
enterDone: 'my-done-enter',
exit: 'my-exit',
exitActive: 'my-active-exit',
exitDone: 'my-done-exit'
}}
Если вы хотите установить эти классы с помощью CSS-модулей:
import styles from './styles.css'
возможно, вам следует использовать camelCase, чтобы впоследствии иметь возможность распаковать свойства с помощью spread-оператора:
classNames={{ ...styles }}
-
onEnter- колбек, вызываемый после примененияenterилиappear -
onEntering- колбек, вызываемый после примененияenter-activeилиappear-active -
onEntered- колбек, вызываемый после удаленияenterилиappearи добавленияdone -
onExit- колбек, вызываемый после примененияexit -
onExiting- колбек, вызываемый после примененияexit-active -
onExited- колбек, вызываемый после удаленияexitи примененияexit-done
SwitchTransition
Данный компонент используется для управления рендерингом между переходами состояния. В зависимости от выбранного режима и дочернего ключа, которым является компонент Transition или CSSTransition, SwitchTransition выполняет согласованный переход между ними.
Если выбран режим out-in, SwitchTransition ждет удаления старого потомка перед добавлением нового. Если выбран режим in-out, SwitchTransition добавляет нового потомка, ждет его вхождения и только после этого удаляет старого потомка.
Обратите внимание: если вы хотите, чтобы удаление старого потомка и добавление нового происходило одновременно, используйте компонент TransitionGroup.
function App() {
const [state, setState] = useState(false)
return (
<SwitchTransition>
<CSSTransition
key={state ? 'Пока' : 'Привет'}
addEndListener={(node, done) =>
node.addEventListener('transitionend', done, false)
}
classNames='fade'
>
<button>{state ? 'Пока' : 'Привет'}</button>
</CSSTransition>
</SwitchTransition>
)
}
.fade-enter {
opacity: 0;
}
.fade-exit {
opacity: 1;
}
.fade-enter-active {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
}
.fade-enter-active,
.fade-exit-active {
transition: opacity 500ms;
}
Пример
import { useState, useRef } from 'react'
import { Button, Form } from 'react-bootstrap'
import { SwitchTransition, CSSTransition } from 'react-transition-group'
import './styles.css'
const modes = ['out-in', 'in-out']
function Example() {
const [mode, setMode] = useState(modes[0])
const [state, setState] = useState(true)
const ref = useRef()
return (
<>
<div className='mb-3'>Режим:</div>
<div className='mb-5 d-flex justify-content-center'>
{modes.map((m) => (
<Form.Check
className='m-2'
key={m}
label={m}
id={`mode=msContentScript${m}`}
type='radio'
name='mode'
checked={mode === m}
value={m}
onChange={(e) => {
setMode(e.target.value)
}}
/>
))}
</div>
<div>
<SwitchTransition mode={mode}>
<CSSTransition
key={state}
timeout={500}
classNames='fade'
nodeRef={ref}
>
<div className='mb-3' ref={ref}>
<Button onClick={() => setState((state) => !state)}>
{state ? 'Привет' : 'Пока'}
</Button>
</div>
</CSSTransition>
</SwitchTransition>
</div>
</>
)
}
export default Example
body {
padding: 2rem;
font-family: sans-serif;
text-align: center;
}
.fade-enter .btn {
opacity: 0;
transform: translateX(-100%);
}
.fade-enter-active .btn {
opacity: 1;
transform: translateX(0%);
}
.fade-exit .btn {
opacity: 1;
transform: translateX(0%);
}
.fade-exit-active .btn {
opacity: 0;
transform: translateX(100%);
}
.fade-enter-active .btn,
.fade-exit-active .btn {
transition: opacity 500ms, transform 500ms;
}
Пропы
mode- режим перехода;children- компонентTransitionилиCSSTransition.
TransitionGroup
Данный компонент управляет набором "переходящих" компонентов (Transition и CSSTransition) в списке. Он является машиной состояния (state machine) для управления монтированием и размонтированием компонентов в течение времени.
В приведенном ниже примере при удалении и добавлении задач в список, происходит автоматическое переключение пропа in в TransitionGroup.
Обратите внимание: TransitionGroup не определяет анимацию. Анимирование элемента списка зависит от переходного (переходящего) компонента. Это означает, что мы можем смешивать и объединять анимации разных элементов.
Пример
import { useState } from 'react'
import { Container, ListGroup, Button } from 'react-bootstrap'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { nanoid } from 'nanoid'
import './styles.css'
function Example() {
const [items, setItems] = useState([
{ id: '1', text: 'Eat' },
{ id: '2', text: 'Code' },
{ id: '3', text: 'Sleep' },
{ id: '4', text: 'Repeat' },
])
return (
<Container className='mt-4'>
<ListGroup className='mb-4' style={{ maxWidth: '480px' }}>
<TransitionGroup>
{items.map(({ id, text }) => (
<CSSTransition key={id} timeout={500} classNames='item'>
<ListGroup.Item>
<Button
style={{ marginRight: '1rem' }}
variant='danger'
size='sm'
onClick={() => {
setItems((items) => items.filter((item) => item.id !== id))
}}
>
×
</Button>
{text}
</ListGroup.Item>
</CSSTransition>
))}
</TransitionGroup>
</ListGroup>
<Button
onClick={() => {
const text = prompt('Введите тект задачи')
if (text) {
setItems((items) => [...items, { id: nanoid(5), text }])
}
}}
>
Добавить элемент
</Button>
</Container>
)
}
export default Example
.item-enter {
opacity: 0;
}
.item-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.item-exit {
opacity: 1;
}
.item-exit-active {
opacity: 0;
transition: opacity 500ms ease-in;
}
Пропы
-
component-TransitionGroupпо умолчанию рендеритdiv. Вы можете указать другой элемент илиnull, если хотите избежать рендеринга лишнего элемента -
children- набор компонентовTransition, пропinкоторых меняется при их вхождении и выходе. Данный компонент, обычно, используется в качестве обертки для нескольких компонентов, но может использоваться и для одного компонента. В этом случае изменение пропаkeyдочернего компонента при измменении его контента заставитTransitionGroupвыполнить переход -
appear- удобный проп для управления анимацией появления всех потомков. Обратите внимание, что определение данного пропа перезапишет соответствующие настройки дочерних компонентов -
enter- проп для управления анимацией вхождения всех потомков -
exit- проп для управления анимацией выхода всех потомков -
childFactory- иногда может потребоваться обновить потомка после его выхода. Обычно, это делается с помощьюcloneElement, однако, компонент может удаляться после выхода, что сделает его недоступным для потребителя. Для обновления "вышедшего" компонента можно использоватьchildFactoryв качестве обертки для каждого выходящего потомка
<TransitionGroup
childFactory={
(child,
{
classNames: 'newTransition',
timeout: newTimeout,
})
}
>
<CSSTransition key={newkey}>{/* ... */}</CSSTransition>
</TransitionGroup>
Использование с React Router
В случае с React Router, следует использовать CSSTransition для управления пропом in каждого маршрута. Самым сложным является анимирование выхода, поскольку маршруты меняются незамедлительно. Нам нужен способ сохранения старого маршрута в течение времени перехода. К счастью, проп children компонента Route может принимать функцию, которая не должна противоречить пропу render. В отличие от пропа render, функция из пропа children запускается только при совпадении маршрута. React Router передает объект, содержащий объект match, который существует при совпадении маршрута, в противном случае, он имеет значение null. Это позволяет нам управлять пропом in компонента CSSTransition.
Обратите внимание: при использовании React Transition Group с React Router старайтесь избегать использования компонента Switch, поскольку он рендерит только совпавший Route. Это сделает невозможным реализацию перехода выхода, поскольку существующий маршрут не будет совпадать с текущим URL и колбек children не запустится.
Пример
import { BrowserRouter as Router, Route, NavLink } from 'react-router-dom'
import { CSSTransition } from 'react-transition-group'
import { Container, Navbar, Nav } from 'react-bootstrap'
import './styles.css'
const Home = () => (
<>
<h1>Home</h1>
<p>Welcome to the Home Page</p>
</>
)
const About = () => (
<>
<h1>About</h1>
<p>This is the About Page</p>
</>
)
const Contact = () => (
<>
<h1>Contact</h1>
<p>This is the Contact Page</p>
</>
)
const routes = [
{ path: '/', name: 'Home', Component: Home },
{ path: '/about', name: 'About', Component: About },
{ path: '/contact', name: 'Contact', Component: Contact },
]
const Example = () => (
<Router>
<Navbar bg='light'>
<Nav className='mx-auto'>
{routes.map(({ path, name }) => (
<Nav.Link
key={path}
as={NavLink}
to={path}
activeClassName='active'
exact
>
{name}
</Nav.Link>
))}
</Nav>
</Navbar>
<Container className='container'>
{routes.map(({ path, Component }) => (
<Route key={path} path={path} exact>
{({ match }) => (
<CSSTransition
in={match !== null}
timeout={300}
classNames='page'
unmountOnExit
>
<div className='page'>
<Component />
</div>
</CSSTransition>
)}
</Route>
))}
</Container>
</Router>
)
export default Example
.container {
position: relative;
}
.page {
position: absolute;
left: 15px;
right: 15px;
}
.page-enter {
opacity: 0;
transform: scale(1.1);
}
.page-enter-active {
opacity: 1;
transform: scale(1);
transition: opacity 300ms, transform 300ms;
}
.page-exit {
opacity: 1;
transform: scale(1);
}
.page-exit-active {
opacity: 0;
transform: scale(0.9);
transition: opacity 300ms, transform 300ms;
}