search

Home  >  Q&A  >  body text

Why does omitting 0ms sleep break my css transitions?

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粉659516906P粉659516906260 days ago738

reply all(2)I'll reply

  • P粉022285768

    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"}
    ); }

    reply
    0
  • P粉106711425

    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.

    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;
    }
    
    
    

    reply
    0
  • Cancelreply