Skip to content

Latest commit

 

History

History
725 lines (569 loc) · 30.3 KB

react-transition-group.md

File metadata and controls

725 lines (569 loc) · 30.3 KB

React Transition Group Guide 🤘

На главную

React Transition Group предоставляет простые компоненты для легкой реализации переходов, связанных с рендерингом компонентов. RTG не является библиотекой для анимации, вместо этого, она предоставляет стадии перехода, управляет классами, группирует элементы и манипулирует 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 основных состояния перехода:

  • 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) => {}}

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

Несколько слов о том, как применяются эти классы:

  1. Они объединяются с существующими классами компонента, так что если вы хотите определить некоторые базовые стили, то можете использовать className, не опасаясь, что базовые стили будут перезаписаны.
  2. Если компонент монтируется с in={false}, никакие классы к нему не применяются (включая *-exit-done).
  3. 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))
                  }}
                >
                  &times;
                </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;
}