Развернуть анимацию с помощью requestAnimationFrame и React иногда не работает

#javascript #reactjs

#javascript #reactjs

Вопрос:

Я пытаюсь реализовать какую-то единую форму ввода с простой анимацией «развернуть» при переходе в режим редактирования / из него.

По сути, я создал элемент-призрак, содержащий значение, рядом с этим элементом находится кнопка со значком, работающая как редактирование / сохранение. Когда вы нажимаете на кнопку редактирования, вместо элемента-призрака должен отображаться ввод со значением, а ширина ввода должна увеличиваться / уменьшаться до заданной постоянной.

У меня пока что есть этот фрагмент кода, который в основном работает нормально, но для expand он иногда не анимируется, и я не знаю почему.

 toggleEditMode = () => {
    const { editMode } = this.state
    if (editMode) {
      this.setState(
        {
          inputWidth: this.ghostRef.current.clientWidth
        },
        () => {
          requestAnimationFrame(() => {
            setTimeout(() => {
              this.setState({
                editMode: false
              })
            }, 150)
          })
        }
      )
    } else {
      this.setState(
        {
          editMode: true,
          inputWidth: this.ghostRef.current.clientWidth
        },
        () => {
          requestAnimationFrame(() => {
            this.setState({
              inputWidth: INPUT_WIDTH
            })
          })
        }
      )
    }
  }
  

Вы можете посмотреть на пример здесь. Может кто-нибудь объяснить, что не так, или помочь мне найти решение? Если я добавлю еще один setTimeout(() => {...expand requestAnimationFrame here...}, 0) в код, он начнет работать, но код мне совсем не нравится.

Ответ №1:

В этом ответе подробно объясняется, что происходит, и как это исправить. Тем не менее, я бы на самом деле не предлагал это реализовывать.

Пользовательские анимации — это беспорядок, и есть отличные библиотеки, которые выполняют грязную работу за вас. Они оборачивают ref s и requestAnimationFrame код и вместо этого предоставляют вам декларативный API. В прошлом я использовал react-spring, и у меня это работало очень хорошо, но движение фреймера также выглядит неплохо.

Однако, если вы хотите понять, что происходит в вашем примере, читайте дальше.

Что происходит

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

Так почему же это, похоже, работает не так, как должно?

Обновления, запускаемые setState , являются асинхронными. React не гарантирует повторный рендеринг при setState вызове; setState это просто запрос на повторную оценку виртуального дерева DOM, который React выполняет асинхронно. Это означает, что setState может завершаться и обычно завершается без немедленного изменения DOM, и что фактическое обновление DOM может произойти только после того, как браузер отобразит следующий кадр.

Это также позволяет React объединять несколько setState вызовов в один повторный рендеринг, что он иногда и делает, поэтому DOM может не обновляться до завершения анимации.

Если вы хотите гарантировать изменение DOM в requestAnimationFrame , вам придется выполнить это самостоятельно, используя React ref :

 const App = () => {
  const divRef = useRef(null);
  const callbackKeyRef = useRef(-1);

  // State variable, can be updated using setTarget()
  const [target, setTarget] = useState(100);

  const valueRef = useRef(target);

  // This code is run every time the component is rendered.
  useEffect(() => {
    cancelAnimationFrame(callbackKeyRef.current);

    const update = () => {
      // Higher is faster
      const speed = 0.15;
      
      // Exponential easing
      valueRef.current
         = (target - valueRef.current) * speed;

      // Update the div in the DOM
      divRef.current.style.width = `${valueRef.current}px`;

      // Update the callback key
      callbackKeyRef.current = requestAnimationFrame(update);
    };

    // Start the animation loop
    update();
  });

  return (
    <div className="box">
      <div
        className="expand"
        ref={divRef}
        onClick={() => setTarget(target === 100 ? 260 : 100)}
      >
        {target === 100 ? "Click to expand" : "Click to collapse"}
      </div>
    </div>
  );
};
  

Вот рабочий пример.

В этом коде используются перехватчики, но та же концепция работает с классами; просто замените useEffect на componentDidUpdate , useState на состояние компонента и useRef на React.createRef .

Ответ №2:

Кажется, это лучшее направление для использования CSSTransition из react-transition-group в вашем компоненте:

 function Example() {
  const [tr, setIn] = useState(false);

  return (
    <div>
      <CSSTransition in={tr} classNames="x" timeout={500}>
        <input
          className="x"
          onBlur={() => setIn(false)}
          onFocus={() => setIn(true)}
        />
      </CSSTransition>
    </div>
  );
}
  

и в вашем модуле css:

 .x {
  transition: all 500ms;
  width: 100px;
}

.x-enter,
.x-enter-done {
  width: 400px;
}
  

Это позволяет избежать использования setTimeout s и requestAnimationFrame и сделало бы код более чистым.

Codesandbox:https://codesandbox.io/s/csstransition-component-forked-3o4x3?file=/index.js

Комментарии:

1. Ну, но у меня нет фиксированной ширины, она динамическая.