Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to run animations in a useEffect hook?

I've started introducing some React hooks into my code, specifically the useEffect and I can't seem to find out whether what I'm doing is considered safe or not. Essentially I'm running animations on the DOM within the hook, and I want to ensure that's not going to break any DOM snapshots for example.

Here's an example, I've modified from my full example to try and be concise to illustrate what's happening:

export function GrowingCircle(props) {

  const root = useRef(null);          // This is the root element we draw to

  // The actual rendering is done whenever the data changes
  useEffect(() => {

    const radius = props.width / 2;

    d3.select(root.current)
      .transition()
      .duration(1000)
      .attr("r", radius);

  }, [props.width]);

  return (
      <svg width={props.width} height="100%">
        <circle ref={root} cx="0" cy="0" r="0" fill="red" />
      </svg>
    );
}

The part I'm concerned about is the .transition() is going to run frequent updates on the DOM for 1 second, and I'm unsure if that's going to mess up the react rendering?


A follow up question (as often we don't have control of the animation rendering like in this example). Would the following where the circle is no longer within the JSX change things?

export function GrowingCircle(props) {

  const root = useRef(null);          // This is the root element we draw to

  // The actual rendering is done whenever the data changes
  useEffect(() => {

    const radius = props.width / 2;

    d3.select(root.current)
      .append("circle")
      .attr("cx", 0)
      .attr("cy", 0)
      .attr("fill", "red")
      .transition()
      .duration(1000)
      .attr("r", radius);

  }, [props.width]);

  return (
      <svg ref={root} width={props.width} height="100%">
      </svg>
    );
}
like image 556
Ian Avatar asked Sep 06 '19 11:09

Ian


People also ask

Can we use props in useEffect?

useEffect() is for side-effects. A functional React component uses props and/or state to calculate the output. If the functional component makes calculations that don't target the output value, then these calculations are named side-effects.

Does useEffect run after paint?

useEffect runs asynchronously after a render is painted to the screen, unblocking the browser paint process. So that looks like: We cause a render somehow (through the state, or the parent re-renders or context change).

Should I use useState or useEffect?

The useState hook is used for storing variables that are part of your application's state and will change as the user interacts with your website. The useEffect hook allows components to react to lifecycle events such as mounting to the DOM, re-rendering, and unmounting.

Does useLayoutEffect run before render?

The useLayoutEffect hook works synchronously. It runs immediately after React has performed all DOM mutations. It will run after every render but before the screen is updated.


2 Answers

DOM elements rendered by React can be modified. However if React needs to re-render because the Virtual DOM doesn't match with the current DOM (the one that React has as the current) it might replace the modified parts, and changes made out of React could be lost.

React modifies the necessary parts so modifications might remain if React only modified a part of the element or they might be removed. So, as far as I understand, we can not trust modifications will remain. Only if we knew for sure the Component output will not change after the custom modifications.

My suggestion is to use React to keep track of any change:

  • Use useState and useEffect to modify style properties.
  • Use CSS classes and handle the animation using CSS.
  • Use an animation library which is made for React (take a look at Framer motion).
like image 182
Alvaro Avatar answered Oct 30 '22 23:10

Alvaro


I have been in your same scenario, to give you a short answer, since there is no real D3 - React implementation, you need to draw your own boundaries of imperative versus declarative rendering. However in both your example, you aren't really breaking any rules.

In your second example you're passing the reins to D3 to do the full rendering, while React simple keeps a ref to the top-level svg element. While in your first example, you're rendering a single circle, and only managing its transition with D3.

However in the 1st example, since it looks like you never change anything in the DOM after declaration, React should geneally never interfere. Here's a third example achieving something similar to what you wrote :

export function GrowingCircle(props) {

  const root = useRef(null);          // This is the root element we draw to

  const [radius,setRadius] = useState(0);  //initial radius value

  useEffect(() => {

    d3.select(root.current)
      .transition()
      .duration(1000)
      .attr("r", props.width / 2)
      .on("end", () => {setRadius(props.width / 2)});   //this is necessary

  }, [props.width]);

  return (
      <svg width={props.width} height="100%">
        <circle ref={root} cx="0" cy="0" r={radius} fill="red" />
      </svg>
    );
}

I've ran into a similar problem myself, and from what I understood, during the transition d3 acts on your desired value directly, causing it not to trigger react's re-render mechanism. But once it's done, I'm assuming it directly tries to act on the radius value, and your circle snaps back to its 0 original value. So you simply add an .on('end') event to update its state, and there you have it!

I feel like this is as close as it gets to the react way, where d3 only selects elements rendered based on react state.

like image 40
Zee Avatar answered Oct 30 '22 23:10

Zee