I'm trying to implement a FLIP animation to see if I understand it correctly.
In this codepen (please forgive the bad code, I'm just messing around), if I comment out the sleep, the smooth transition no longer works. The div changes position suddenly. This is strange because the sleep time is 0ms.
import React, { useRef, useState } from "https://esm.sh/react@18"; import ReactDOM from "https://esm.sh/react-dom@18"; let first = {} let second = {} const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const App = () => { const [start, setStart] = useState(true); const boxRefCb = async el => { if (!el) return; el.style.transition = ""; const x = parseInt(el?.getBoundingClientRect().x, 10); const y = parseInt(el?.getBoundingClientRect().y, 10); first = { x: second.x, y: second.y }; second = { x, y }; const dx = first.x - second.x; const dy = first.y - second.y; const transStr = `translate(${dx}px, ${dy}px)`; el.style.transform = transStr; await sleep(0); // comment me out el.style.transition = "transform .5s"; el.style.transform = ""; } return ( <> <div style={{ display: "flex", gap: "1rem", padding: "3rem"}}> <div ref={ start ? boxRefCb : null } style={{ visibility: start ? "" : "hidden", width: 100, height: 100, border: "solid 1px grey" }}></div> <div ref={ !start ? boxRefCb : null } style={{ visibility: !start ? "" : "hidden", width: 100, height: 100, border: "solid 1px grey" }}></div> </div> <button style={{ marginLeft: "3rem"}} onClick={() => setStart(start => !start)}>start | {start.toString()}</button> </> ); } ReactDOM.render(<App />, document.getElementById("root"))
I suspect this is some event loop magic that I don't understand. Can someone explain this to me?
P粉0222857682024-04-07 14:25:14
You are using a normal JavaScript solution to this problem, but React uses a virtual DOM and expects DOM elements to be re-rendered when state changes. Therefore, I recommend leveraging React state to update the XY position of elements in the virtual DOM, but still using CSS.
Working DemoHere Or the code can be found here:
import { useState, useRef, useLayoutEffect } from "react"; import "./styles.css"; type BoxXYPosition = { x: number; y: number }; export default function App() { const startBox = useRef(null); const startBoxPosition = useRef ({ x: 0, y: 0 }); const endBox = useRef (null); const [boxPosition, setBoxPosition] = useState ({ x: 0, y: 0 }); const { x, y } = boxPosition; const hasMoved = Boolean(x || y); const updatePosition = () => { if (!endBox.current) return; const { x: endX, y: endY } = endBox.current.getBoundingClientRect(); const { x: startX, y: startY } = startBoxPosition.current; // "LAST" - calculate end position const moveXPosition = endX - startX; const moveYPosition = endY - startY; // "INVERT" - recalculate position based upon current x,y coords setBoxPosition((prevState) => ({ x: prevState.x !== moveXPosition ? moveXPosition : 0, y: prevState.y !== moveYPosition ? moveYPosition : 0 })); }; useLayoutEffect(() => { // "FIRST" - save starting position if (startBox.current) { const { x, y } = startBox.current.getBoundingClientRect(); startBoxPosition.current = { x, y }; } }, []); // "PLAY" - switch between start and end animation via the x,y state and a style property return ( ); } Transition Between Points
{hasMoved ? "End" : "Start"}
P粉1067114252024-04-07 09:16:56
During sleep
, the browser may have time to recalculate the CSSOM box (also known as "execution reflow"). Without it, your transform
rules won't actually apply.
In fact, the browser will wait until it's really needed to apply your changes and update the entire page box model, because doing so can be very expensive.
When you do something like
element.style.color = "red"; element.style.color = "yellow"; element.style.color = "green";
All CSSOMs will see is the latest status, "green"
. The other two were discarded.
So in your code, when you don't let the event loop actually loop, you will never see the transStr
value either.
However, relying on 0ms setTimeout
is a problem call, there is nothing to ensure that the style is recalculated at that time. Instead, it's better to force a recalculation manually. Some DOM methods/properties do this synchronously. But keep in mind that reflow can be a very expensive operation, so be sure to use it occasionally, and if there are multiple places in your code that require this operation, be sure to connect them all so that a single reflow can be performed. p>
const el = document.querySelector(".elem"); const move = () => { el.style.transition = ""; const transStr = `translate(150px, 0px)`; el.style.transform = transStr; const forceReflow = document.querySelector("input").checked; if (forceReflow) { el.offsetWidth; } el.style.transition = "transform .5s"; el.style.transform = ""; } document.querySelector("button").onclick = move;
.elem { width: 100px; height: 100px; border: 1px solid grey; } .parent { display: flex; padding: 3rem; }