sidebar_position | title | description | keywords | tags | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
11 |
Руководство по React Transition Group |
Руководство по React Transition Group |
|
|
React Transition Group предоставляет простые компоненты для легкой реализации переходов (transitions) при рендеринге компонентов.
React Transition Group
не является библиотекой для анимации, вместо этого, он обрабатывает стадии перехода, управляет классами, группирует элементы и манипулирует DOM наиболее эффективным образом, облегчая реализацию визуальных переходов.
yarn add react-transition-group
# или
npm i react-transition-group
Компонент 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 основных состояния перехода:
entering
entered
exiting
exited
Эти состояния переключаются с помощью пропа 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) => {}}
Данный компонент следует использовать при реализации переходов и анимации с помощью 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
Данный компонент используется для управления рендерингом между переходами состояния. В зависимости от выбранного режима и дочернего ключа, которым является компонент 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
.
Данный компонент управляет набором "переходящих" компонентов (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;
}