#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. Ну, но у меня нет фиксированной ширины, она динамическая.