React компоненты

Использование Motion One в React строится вокруг идеи декларативного управления анимациями с сохранением контроля над жизненным циклом компонентов. Библиотека предоставляет низкоуровневые примитивы animate, motion, timeline, которые можно адаптировать под React-архитектуру без необходимости в дополнительном слое абстракций.

Базовая модель работы в React

React управляет DOM через виртуальное дерево, тогда как Motion One напрямую работает с реальными DOM-узлами. Это создаёт необходимость связывания элемента через ref.

Основная схема:

  • создаётся ref на DOM-элемент
  • после монтирования компонента запускается анимация
  • при размонтировании при необходимости выполняется очистка
import { useEffect, useRef } from "react";
import { animate } from "motion";

export function FadeInBox() {
  const ref = useRef(null);

  useEffect(() => {
    if (!ref.current) return;

    const controls = animate(
      ref.current,
      { opacity: [0, 1], transform: ["translateY(20px)", "translateY(0px)"] },
      { duration: 0.6, easing: "ease-out" }
    );

    return () => controls.stop();
  }, []);

  return <div ref={ref}>Контент</div>;
}

Ключевой момент заключается в том, что animate возвращает контроллер, позволяющий остановить анимацию при размонтировании компонента.


Анимации при изменении состояния

В React анимации часто зависят от состояния. Motion One не требует отдельной системы transition — достаточно отслеживать изменения state.

import { useEffect, useRef, useState } from "react";
import { animate } from "motion";

export function TogglePanel() {
  const ref = useRef(null);
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (!ref.current) return;

    animate(
      ref.current,
      {
        height: open ? 200 : 0,
        opacity: open ? 1 : 0
      },
      { duration: 0.4 }
    );
  }, [open]);

  return (
    <div>
      <button onCl ick={() => setOpen(v => !v)}>toggle</button>
      <div ref={ref} style={{ overflow: "hidden" }}>
        Контент панели
      </div>
    </div>
  );
}

Особенность подхода заключается в том, что Motion One не требует предварительного описания состояний анимации — достаточно описать переход между текущим и целевым значением.


Использование motion для декларативных компонентов

В React можно использовать обёртку motion, позволяющую работать с анимацией через props-подобный интерфейс.

import { motion } from "motion/react";

export function Card() {
  return (
    <motion.div
      initial={{ opacity: 0, scale: 0.95 }}
      animate={{ opacity: 1, scale: 1 }}
      transition={{ duration: 0.5 }}
    >
      Карточка
    </motion.div>
  );
}

Такой подход снижает необходимость ручного управления ref, но сохраняет совместимость с Motion One API.


Анимация списков и динамических элементов

При работе с массивами данных ключевой задачей становится синхронизация анимаций с жизненным циклом элементов.

import { useEffect, useRef } from "react";
import { animate } from "motion";

export function List({ items }) {
  const refs = useRef([]);

  useEffect(() => {
    refs.current.forEach((el, index) => {
      if (!el) return;

      animate(
        el,
        { opacity: [0, 1], transform: ["translateX(10px)", "translateX(0px)"] },
        { delay: index * 0.05, duration: 0.3 }
      );
    });
  }, [items]);

  return (
    <div>
      {items.map((item, i) => (
        <div
          key={item.id}
          ref={(el) => (refs.current[i] = el)}
        >
          {item.title}
        </div>
      ))}
    </div>
  );
}

Важная особенность заключается в том, что управление DOM-коллекцией происходит вручную через массив refs, что даёт полный контроль над порядком анимации.


Прерывание и переиспользование анимаций

Motion One позволяет управлять жизненным циклом анимации через объект управления.

import { useEffect, useRef } from "react";
import { animate } from "motion";

export function ProgressBar({ value }) {
  const ref = useRef(null);
  const controls = useRef(null);

  useEffect(() => {
    if (!ref.current) return;

    if (controls.current) {
      controls.current.stop();
    }

    controls.current = animate(
      ref.current,
      { width: `${value}%` },
      { duration: 0.3 }
    );
  }, [value]);

  return <div ref={ref} style={{ height: 8, background: "black" }} />;
}

Такой подход предотвращает наложение анимаций при быстром изменении состояния.


Использование timeline в React

Для сложных последовательностей применяется timeline, позволяющий строить цепочки анимаций.

import { useEffect, useRef } from "react";
import { timeline } from "motion";

export function Sequence() {
  const ref = useRef(null);

  useEffect(() => {
    if (!ref.current) return;

    const sequence = timeline([
      [ref.current, { opacity: 1 }, { duration: 0.3 }],
      [ref.current, { transform: "scale(1.1)" }, { duration: 0.2 }],
      [ref.current, { transform: "scale(1)" }, { duration: 0.2 }]
    ]);

    return () => sequence.stop();
  }, []);

  return <div ref={ref}>Блок</div>;
}

Timeline особенно полезен для UI-компонентов, где требуется строгое упорядочивание фаз анимации.


Анимации входа и выхода компонентов

При условном рендеринге React не предоставляет встроенной анимации размонтирования, поэтому используется ручное управление.

import { useEffect, useRef, useState } from "react";
import { animate } from "motion";

export function Modal() {
  const ref = useRef(null);
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    if (!ref.current) return;

    if (visible) {
      animate(ref.current, { opacity: [0, 1], scale: [0.9, 1] });
    } else {
      animate(ref.current, { opacity: [1, 0], scale: [1, 0.9] });
    }
  }, [visible]);

  return (
    <div>
      <button onCl ick={() => setVisible(v => !v)}>open</button>
      {visible && (
        <div ref={ref} style={{ position: "fixed", inset: 0 }}>
          Модальное окно
        </div>
      )}
    </div>
  );
}

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


Синхронизация с React Strict Mode

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

import { useEffect, useRef } from "react";
import { animate } from "motion";

export function SafeAnimation() {
  const ref = useRef(null);
  const started = useRef(false);

  useEffect(() => {
    if (!ref.current || started.current) return;

    started.current = true;

    const controls = animate(ref.current, {
      opacity: [0, 1]
    });

    return () => controls.stop();
  }, []);

  return <div ref={ref}>Контент</div>;
}

Комбинирование Motion One с хуками React

Архитектурно удобный подход заключается в выделении анимации в кастомные хуки.

import { useEffect } from "react";
import { animate } from "motion";

export function useFadeIn(ref) {
  useEffect(() => {
    if (!ref.current) return;

    const controls = animate(ref.current, {
      opacity: [0, 1]
    });

    return () => controls.stop();
  }, [ref]);
}

Использование:

export function Block() {
  const ref = useRef(null);
  useFadeIn(ref);

  return <div ref={ref}>Элемент</div>;
}

Такой подход разделяет визуальную логику и структуру компонента, упрощая масштабирование интерфейса.


Производительность и поведение в React-рендере

Motion One работает вне React render cycle, поэтому:

  • анимации не вызывают повторный render
  • изменения происходят напрямую в DOM
  • React используется только для описания структуры

Это снижает нагрузку на reconciliation, но требует аккуратного управления побочными эффектами.

Особенно важно учитывать:

  • очистку анимаций при размонтировании
  • предотвращение дублирования эффектов
  • контроль повторных запусков в development режиме

Синхронизация нескольких элементов

При сложных интерфейсах часто требуется координация нескольких DOM-узлов.

import { useEffect, useRef } from "react";
import { animate } from "motion";

export function Dashboard() {
  const header = useRef(null);
  const content = useRef(null);

  useEffect(() => {
    animate(header.current, { opacity: [0, 1], y: [-10, 0] });
    animate(content.current, { opacity: [0, 1], y: [10, 0] });
  }, []);

  return (
    <div>
      <div ref={header}>Header</div>
      <div ref={content}>Content</div>
    </div>
  );
}

Такая модель позволяет строить сложные композиции без внешнего state manager для анимаций.