Skip to main content

React Spring

React Spring - это основанная на физике упругости (spring - пружина, рессора) библиотека для реализации анимации в React-приложениях. Разработчики данной библиотеки утверждают, что она способна удовлетворить большую часть потребностей, возникающих при разработке пользовательских интерфейсов.

Примеры использования.

Установка

yarn add react-spring
# или
npm i react-spring

Хуки

Основы

В настоящее время react-spring предоставляет 5 хуков (примитивов для анимации):

  • useSpring - простая пружина, перемещающая данные из точки "а" в точку "б"
  • useSprings - несколько пружин, для списков, где каждая пружина перемещает данные...
  • useTrail - несколько пружин с общим набором данных, одна пружина следует за другой
  • useTransition - для реализации переходов при монтировании/размонтировании компонентов (например, для элементов списка, которые добавляются/удаляются/обновляются). Обратите внимание: частью разрабатываемого в настоящее время командой React конкуретного режима является хук useTransition, предназначенный для обработки переходов между состояниями компонента. Как будет решена эта коллизия, покажет время
  • useChain - для постановки в очередь или выстраивания в цепочку нескольких анимаций

Самым простым является хук useSpring

import { useSpring, animated } from 'react-spring'

function App() {
const props = useSpring({ opacity: 0, from: { opacity: 1 } })
return <animated.div style={props}>Помогите! Я исчезаю</animated.div>
}

animated - это специальная фабрика, библиотека для выполнения анимации за пределами React (по соображениям производительности). Что делает animated? Она расширяет нативные элементы, чтобы они могли принимать анимированные значения (как styled в стилизованных компонентах).

Пружина анимирует значение от одного состояния до другого. Обновления суммируются, пружина запоминает переданные значения. Полученные пропы не являются статическими, они автоматически обновляются.

animated способна анимировать не только HTML-элементы, как в приведенном выше примере, но также React-компоненты, нативные компоненты, стилизованные компоненты и т.д.

// компонент React
const MyAnimatedComponent = animated(MyComponent)

// React Native
const AnimatedView = animated(View)

// styled-components, emotion и т.д.
const AnimatedListItem = styled(animated.li)`
...
`

Передаваемые значения - это не совсем стили

Их можно использовать так:

const props = useSpring({
opacity: 1,
from: { opacity: 0 }
})
return <animated.h1 style={props}>Привет!</animated.h1>

Можно анимировать атрибуты:

const props = useSpring({
x: 100,
from: { x: 0 }
})
return (
<animated.svg strokeDashoffset={props.x}>
<path d="...">
</animated.svg>
)

Можно анимировать текстовое содержимое:

const props = useSpring({
number: 1,
from: {
x: 0
}
})
return <animated.span>{props.number}</animated.span>

Можно анимировать прокрутку страницы:

const props = useSpring({
scroll: 100,
from: {
scroll: 0
}
})
return <animated.div scrollTop={props.scroll} />

Или генерировать пропы компонента:

const AnimatedCircle = animate(Circle)

const props = useSpring({
value: 100,
from: {
value: 0
}
})
return <AnimatedCircle percent={props.value} />

Передняя интерполяция (up-front interpolation)

Обратите внимание: интерполяция (здесь) - это переход значения из одного состояния в другое через одно или более промежуточных состояний.

Пружины умеют работать не только с числами, но и с:

  • цветами (названия, rgb, rgba, hsl, hsla, градиенты)
  • абсолютными значениями (см, мм, дюймы, пиксели, пункты, пики)
  • относительными значениями (em, ex, ch, rem, vw, vh, vmin, vmax, %)
  • углами (deg, rad, grad, turn)
  • единицами flex и grid (fr и т.д.)
  • всеми HTML-атрибутами
  • путями SVG (до тех пор, пока совпадает количество точек, в противном случае, следует использовать кастомную интерполяцию)
  • массивами
  • строковыми паттернами (transform, border, boxShadow и т.п.)
  • не анимируемыми строковыми значениями (visibility, pointerEvents и др.)
  • scrollTop/scrollLeft
const props = useSpring({
vector: [0, 10, 30],
display: 'block',
padding: 20,
background: 'linear-gradient(to right, #009fff, #ec2f4b)',
transform: 'translate3d(0px, 0, 0) scale(1) rotateX(0deg)',
boxShadow: '0px 10px 20px 0px rgba(0, 0, 0, 0.4)',
borderBottom: '10px solid #2d3747',
shape: 'M20,20 L20,380 L380,380 L380,20 L20,20 Z',
textShadow: '0px 5px 15px rgba(255, 255, 255, 0.5)'
})

Интерполяция представления (view interpolation)

В случае, когда нам требуется зафиксировать или экстраполировать значения, можно воспользоваться функцией interpolate, которую содержит каждое анимированное значение. Данная функция принимает функцию или объект с диапазоном значений. Из интерполяций можно формировать цепочки, позволяющие подключать одни вычисления к другим или повторно использовать произведенные ранее вычисления.

Причина, по которой интерполяцию лучше выполнять в представлении, а не в пружине, заключается в том, что интерполяция представления работает немного быстрее и занимает меньше места

import { useSpring, animated, interpolate } from 'react-spring'

const { o, xyz, color } = useSpring({
from: {
o: 0,
xyz: [0, 0, 0],
color: 'red'
}
o: 1,
xyz: [10, 20, 5],
color: 'green'
})

return (
<animated.div
style={{
// По-возможности, всегда используйте обычные анимированные значения,
// когда они просто передаются
color,
// Пока не потребуется их интерполировать
background: o.interpolate(o => `rgba(210, 57, 77, ${o})`),
// Что также работает с массивами
transform: xyz.interpolate((x, y, z) => `translate3d(${x}px, ${y}px, ${z}px)`),
// Для комбинации нескольких значений используется утилита `interpolate`
border: interpolate([o, color], (o, c) => `${o * 10}px solid ${c}`),
// Можно создавать диапазоны и даже цепочки из нескольких интерполяций
padding: o.interpolate({ range: [0, 0.5, 1], output: [0, 0, 10] }).interpolate(o => `${o}%`),
// Разрешена интерполяция строк
borderColor: o.interpolate({ range: [0, 1], output: ['red', '#ffaabb'] }),
// Существует сокращенный синтаксис для диапазонов без настроек
opacity: o.interpolate([0.1, 0.2, 0.6, 1], [1, 0.1, 0.5, 1])
}}
>
{o.interpolate(n => n.toFixed(2))} /* интерполяция текста */
</animated.div>
)

Заметки

Анимирование значения auto

Одним из отличий хуков от пропов для рендеринга является то, что хуки "не знают" о представлении. Поэтому хуки не умеют работать со значением auto. Это может показаться недостатком, однако, вычисление размера элемента с помощью таких хуков как useResizeAware или useMeasure никогда не было таким простым, как сейчас.

const [ref, {height}] = useMeasure()
const props = useSpring({ height })
return (
<animated.div style={{ overflow: 'hidden', ...props }}>
<div ref={ref}>Контент</div>
</animated.div>
)
Имитация ключевых кадров
/*
0% { transform: scale(1); }
25% { transform: scale(.97); }
35% { transform: scale(.9); }
45% { transform: scale(1.1); }
55% { transform: scale(.9); }
65% { transform: scale(1.1); }
75% { transform: scale(1.03); }
100% { transform: scale(1); }
*/

const props = useSpring({ x: state ? 1 : 0 })
return (
<animated.div
style={{
transfrom: props.x
.interpolate({
range: [0, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 1],
output: [1, 0.97, 0.9, 1.1, 0.9, 1.1, 1.03, 1]
})
.interpolate(x => `scale(${x})`)
}}
/>
)

Общий интерфейс

Настройки

Для настройки анимации используется объект config, передаваемый useSpring и другим хукам

useSpring({ config: { duration: 250 }, ... })
СвойствоЗначение по умолчаниюОписание
mass (масса)1вес пружины
tension (натяжение)170энергичность растяжения/сжатия пружины
friction (трение)26сопротивление пружины
clamp (ограничение)falseесли true, тогда пружина останавливается при достижении границы
precision (точность)0.01точность
velocity (скорость)0начальная скорость
duration (продолжительность)undefinedустановление значения больше 0 приводит к переключению анимации с основанной на упругости на основанную на продолжительности
easing (характер движения)t => tпо умолчанию является линейным

Пресеты

Существует набор общих пресетов, охватывающих некоторые распространенные случаи

import { ..., config } from 'react-spring'

useSpring({ ..., config: config.default })
СвойствоЗначение
config.defaut{ mass: 1, tension: 170, friction: 26 }
config.gentle{ mass: 1, tension: 120, friction: 14 }
config.wobbly{ mass: 1, tension: 180, friction: 12 }
config.stiff{ mass: 1, tension: 210, friction: 20 }
config.slow{ mass: 1, tension: 280, friction: 60 }
config.molasses{ mass: 1, tension: 280, friction: 120 }

Свойства

useSpring({ from: {...}, to: {...}, delay: 100, onRest: () => ... })

Все примитивы наследуют следующие свойства (некоторые могут предоставлять дополнительный функционал):

СвойствоТипОписание
fromobjБазовые значение, опционально
toobj/fn/arr(obj)Конечные значения
delaynum/fnЗадержка в мс перед началом анимации. Может быть функцией для отдельных ключей: key => delay
immediatebool/fnЕсли true, анимация отключается. Может быть функцией для отдельных ключей
configobj/fnНастройки пружины (масса, натяжение, трение и т.д.). Может быть функцией для отдельных ключей
resetboolЕсли true, анимация начинается с самого начала
reverseboolЕсли true, from и to меняются местами. Это имеет смысл только при совместном использовании с reset
onStartfnКолбек, вызываемый в начала анимации
onRestfnКолбек, вызываемый после остановки всех анимаций
onFramefnПокадровый (frame by frame) колбек, первый передаваемый аргумент является значением анимации

Интерполяция

value.interpolate принимает объект следующей формы:

СвойствоЗначение по умолчаниюОписание
extrapolateLeftextendЛевая экстраполяция. Возможные значения: identity/clamp/extend
extrapolateRightextendПравая экстраполяция. Возможные значения: identity/clamp/extend
extrapolateextendСокращенный синтаксис для установки левой и правой экстраполяций
range[0, 1]Массив входных значений
outputundefinedМассив соответствующих (mapped) выходных значений
mapundefinedФильтр, применяемый к входному значению

Сокращение для диапазона: value.interpolate([...inputRange], [...outputRange]). Может быть функцией: value.interpolate(v => ...)

useSpring

import { useSpring, animated } from 'react-spring'

Преобразует обычные значения в анимируемые.

Повторный рендеринг компонента с изменившимися пропами приводит к обновлению анимации

const props = useSpring({ opacity: toggle ? 1 : 0 })

useSpring возвращает пропы (props) и две функции: для обновления анимации (set) и ее остановки (stop)

const [props, set, stop] = useSpring(() => ({ opacity: 1 }))

// Обновляем пружину новыми пропами
set({ opacity: toggle ? 1 : 0 })
// Останавливаем анимацию
stop()

Для запуска анимации props передается в атрибут style компонента

return <animated.div style={props}>Я появляюсь и исчезаю</animated.div>

Заметки

Сокращение для пропа to

Любое неопознанное свойство становится свойством to, например, opacity: 1 становится to: { opacity: 1 }

// Это
const props = useSpring({ opacity: 1, color: 'red' })
// является сокращением для этого
const props = useSpring({ to: { opacity: 1, color: 'red' } })
Асинхронные цепочки/скрипты

Проп to также позволяет превращать анимацию в скрипт и объединять анимации. Поскольку такие анимации будут асинхронными, передача свойства from с начальными значениями является обязательной, иначе, пропы будут пустыми.

Вот как можно создать скрипт

const props = useSpring({
to: async (next, cancel) => {
await next({ opacity: 1, color: '#ffaaee' })
await next({ opacity: 0, color: 'rgb(14, 26, 19)' })
},
from: { opacity: 0, color: 'red' }
})
// ...
return <animated.div style={props}>Я исчезаю и появляюсь</animated.div>

А так можно создать цепочку из анимаций

const props = useSpring({
to: [
{
opacity: 1,
color: '#ffaaee'
},
{
opacity: 0,
color: 'rgb(14, 26, 19)'
}
],
from: {
opacity: 0,
color: 'red'
}
})
// ...
return <animated.div style={props}>Туда и обратно</animated.div>

useSprings

import { useSprings, animated } from 'react-spring'

Создает несколько пружин. Каждая пружина имеет собственные настройки. Используется для статических списков и т.д.

Повторный рендеринг компонента с изменившимися пропами приводит к обновлению анимации

const springs = useSprings(number, items.map(i => ({ opacity: i.opacity })))

useSprings возвращает пружины (springs) в виде массива и две функции: для обновления анимации (set) и ее остановки (stop)

const [springs, set, stop] = useSprings(number, index => ({ opacity: 1 }))

// Обновляем пружины новыми пропами
set(index => ({ opacity: 0 }))
// Останавливаем все пружины
stop()

springs - это массив, содержащий анимированные пропы

return springs.map(props => <animated.div style={props} />)

useTrail

import { useTrail, animated } from 'react-spring'

Создает несколько пружин с общими настройками, каждая пружина следует за предыдущей. Используется для последовательной (запланированной) анимации

Повторный рендеринг компонента с изменившимися пропами приводит к обновлению анимации

const trail = useTrail(number, { opacity: 1 })

useTrail возвращает анимации (trail) в виде массива и две функции: для обновления анимации (set) и ее остановки (stop)

const [trail, set, stop] = useTrail(number, () => ({ opacity: 1 }))

// Обновляем анимации
set({ opacity: 0 })
// Останавливаем их
stop()

trail - массив, содержащий анимированные пропы

return trail.map(props => <animated.div style={props} />)

useTransition

import { useTransition, animated } from 'react-spring'

Анимированная TransitionGroup. useTransition принимает элементы, ключи (которые могут иметь значение null, если элементы являются атомарными) и жизненные циклы. Данный хук предназначен для анимирования добавления/удаления элементов.

Можно анимировать переходы массивов

const [items, set] = useState([...])
const transitions = useTransition(items, item => item.key, {
from: { transform: 'translate3d(0, -40px, 0)' },
enter: { transform: 'translate3d(0, 0px, 0)' },
leave: { transform: 'translate3d(0, -40px, 0)' }
})
return transitions.map(({ item, props, key }) => <animated.div key={key} style={props}>{item.text}</animated.div>)

Переключение компонентов

const [toggle, set] = useState([...])
const transitions = useTransition(toggle, null, {
from: { position: 'absolute', opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
})
return transitions.map(({ item, key, props }) =>
item
? <animated.div style={props}>👍</animated.div>
: <animated.div style={props}>👎</animated.div>
)

Отображение монтирования/размонтирования одного компонента

const [show, set] = useState(false)
const transitions = useTransition(show, null, {
from: { position: 'absolute', opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
})
return transitions.map(({ item, key, props }) =>
item && <animated.div key={key} style={props}>✌️</animated.div>
)

Свойства

Кроме общих, useTransition принимает объект со следующими настройками

СвойствоТипОписание
initialobj/fnНачальные значения (значения первого запуска анимации), опционально (может иметь значение null)
fromobj/fnНачальные значения, опционально
enterobj/fn/arr(obj)Стили, применяемые при монтировании элементов
updateobj/fn/arr(obj)Стили, применяемые при обновлении элементов (можно обновлять сам хук)
leaveobj/fn/arr(obj)Стили, применяемые при размонтировании элементов
trailnumЗадержка в мс перед началом анимации, применяется при каждом добавлении/обновлении/удалении
uniquebool/fnЕсли true, тогда ключи элементов будут сохраняться
resetbool/fnИспользуется совместно с reset, "вхождение" элементов запускается с самого начала
onDestroyedfnКолбек, вызываемый после исчезновения объектов

Заметки

Переходы через несколько состояний

Жизненные циклы initial/from/enter/update/leave могут быть объектами, массивами или функциями. Функция позволяет возвращать объект, массив или функцию для осуществления перехода через несколько состояний. При передаче массива можно реализовать базовый мультипереход без доступа к элементу

useTransition(items, item => item.id, {
enter: item => [
{
opacity: item.opacity,
height: item.height
},
{
life: '100%'
}
],
leave: item => async (next, cancel) => {
await next({ life: '0%' })
await next({ opacity: 0 })
await next({ height: 0 })
},
from: {
life: '0%',
opacity: 0,
height: 0
}
})
Переход между маршрутами (routes)
const location = useLocation()
const transitions = useTransition(location, location => location.pathname, {...})
return transitions.map(({ item, props, key }) => (
<animated.div key={key} style={props}>
<Switch location={item}>
<Route path="/a" component={A}>
<Route path="/b" component={B}>
<Route path="/c" component={C}>
</Switch>
</animated.div>
))

useChain

import { useChain, animated } from 'react-spring'

Устанавливает порядок выполнения определенных ранее хуков анимации. useChain принимает массив со ссылками (refs) на анимации, что блокирует самостоятельный запуск анимаций. Порядок элементов в массиве определяет порядок выполнения анимаций

// Создаем пружину и добавляем к ней ссылку
const springRef = useRef()
const props = useSpring({..., ref: springRef})
// Создаем переход и добавляем к нему ссылку
const transitionRef = useRef()
const transitions = useTransition({..., ref: transitionRef})
// Сначала запускает пружину, затем переход
useChain([springRef, transitionRef])
// Используем анимированные пропы как обычно
return (
<animated.div style={props}>
{transitions.map(({ item, key, props }) => (
<animated.div key={key} style={props} />
))}
</animated.div>
)

В качестве второго аргумента useChain принимает опциональный объект с настройками: timeSteps и timeFrame (по умолчанию имеет значение 1000 мс). timeSteps - массив с отступами от 0 до 1, определяющими начало и конец timeFrame

// Пружина будет запущена сразу: 0.0 * 1000 мс = 0 мс
// Переход начнется через: 0.5 * 1000 мс = 500 мс
useChain([springRef, transitionRef], [0, 0.5] /*1000*/)

Пропы для рендеринга

Spring

Аналог useSpring

import { Spring } from 'react-spring/renderprops'

<Spring
from={{ opacity: 1 }}
to={{ opacity: 0 }}>
{props => <div style={props}>Помогите! Я исчезаю</div>}
</Spring>

Интерполяция

<Spring
from={{
width: 100,
padding: 0,
background:
'linear-gradient(to right, #30e8bf, #ff8235)',
transform:
'translate3d(400px,0,0) scale(2) rotateX(90deg)',
boxShadow: '0px 100px 150px -10px #2D3747',
borderBottom: '0px solid white',
shape: 'M20,380 L380,380 L380,380 L200,20 L20,380 Z',
textShadow: '0px 5px 0px white'
}}
to={{
width: 'auto',
padding: 20,
background:
'linear-gradient(to right, #009fff, #ec2f4b)',
transform:
'translate3d(0px,0,0) scale(1) rotateX(0deg)',
boxShadow: '0px 10px 20px 0px rgba(0,0,0,0.4)',
borderBottom: '10px solid #2D3747',
shape: 'M20,20 L20,380 L380,380 L380,20 L20,20 Z',
textShadow: '0px 5px 15px rgba(255,255,255,0.5)'
}}>
{props => ...}
</Spring>

Настройки

import { Spring, config } from 'react-spring/renderprops'

// пресет
return <Spring config={config.default} />
// кастомизация
<Spring config={{tension: 0, friction: 2, precision: 0.1}} />
// кастомизация по ключу
<Spring to={{width: 10, color: 'red'}} config={key => (key === 'width' ? config.slow : config.wobbly)} />

Trail

Аналог useTrail

import { Trail } from 'react-spring/renderprops'

<Trail
items={items}
keys={item => item.key}
from={{transform: 'translate3d(0, -40px, 0)'}}
to={{transform: 'translate3d(0, 0px, 0)'}}
>
{item => props => <span style={props}>{item.text}</span>}
</Trail>

Transition

Аналог useTransition

import { Transition } from 'react-spring/renderprops'

const items = [...]

<Transition
items={items} keys={item => item.key}
from={{ transform: 'translate3d(0, -40px, 0)' }}
enter={{ transform: 'translate3d(0, 0px, 0)' }}
leave={{ transform: 'translate3d(0, -40px, 0)' }}
>
{item => props => <div style={props}>{item.text}</div>}
</Transition>

Keyframes

Позволяет формировать цепочки, композиции из анимаций.

Keyframes - это фабрика для расширения spring и trail. Сначала определяются именованные слоты с анимируемыми свойствами. Все неизвестные свойства считаются свойствами to. Возвращается компонент со специальным свойством state с названием одного из слотов.

Слот принимает:

  • объект со свойствами
  • массив объектов, выстраиваемых в цепочку
  • функцию, позволяющую создавать анимации в виде скриптов или формировать циклы
// Допускается создание ключевых кадров для `Spring` и `Trail`
const Container = Keyframes.Spring({
// Единичное значение
show: { opacity: 1 },
// Цепочка из анимаций (массив)
showAndHide: [ { opacity: 1 }, { opacity: 0 } ],
// Функция с побочными эффектами с доступом к пропам компонента
wiggle: async (next, cancel, ownProps) => {
await next({ x: 100, config: config.wobbly })
await delay(1000)
await next({ x: 0, config: config.gentle })
}
})

return <Container state="showAndHide">{styles => <div style={styles}>Hello</div>}</Container>

Parallax

import { Parallax, ParallaxLayer } from 'react-spring/renderprops-addons'

Parallax создает "прокручиваемый" (scroll) контейнер. Ему передается любое количество ParallaxLayer, которые перемещаются согласно их отступам (offsets) и скоростям (speeds)

<Parallax pages={3} scrolling={false} horizontal ref={ref => (this.parallax = ref)}>
<ParallaxLayer offset={0} speed={0.5}>
<span onClick={() => this.parallax.scrollTo(1)}>Layers can contain anything</span>
</ParallaxLayer>
</Parallax>

Настройки

Parallax
СвойствоТипRequiredDefaultОписание
configobjfalseconfig.slowНастройки пружины
scrollingboolfalsetrueОпределяет "прокручиваемость" контента
horizontalboolfalsefalseОпределяет направление прокрутки
pagesnumtrue-Определяет общее пространство входящего контента, где каждая страница занимает 100% видимого контейнера
ParallaxLayer
СвойствоТипRequiredDefaultОписание
factornumfalse1Размер страницы (1 = 100%, 1.5 = 150% и т.д.)
offsetnumfalse0Определяет, где будет слой после начала прокрутки (0 = начало, 1 = первая страница и т.д.)
speednumfalse0Определяет скорость сдвига слоя в соответствии с его отступом, значения могут быть как положительными, так и отрицательными