1

I thought react-spring useSpring() causes the component to re-render a lot, so if it is a component that already has a lot of CPU intensively work to do, then react-spring is not best suited for the task.

I can see that it re-renders a lot, in their example:

https://codesandbox.io/s/musing-dew-9tfi9?file=/src/App.tsx

(by looking at the console.log output, which has a lot of print out of renderCount. The print out is a lot more when we change the duration to 5000 which is 5 seconds).

Likewise, if it is a component that is similar to react-spring, it'd render a lot:

https://codesandbox.io/s/wonderful-currying-9pheq

However, the following code:

let renderCount = 0

export default function App() {
  const styles = useSpring({
    loop: true,
    to: [
      { opacity: 1, color: '#ffaaee' },
      { opacity: 0.5, color: 'rgb(14,26,19)' },
      { transform: 'translateX(100px)' },
      { transform: 'translateX(0px)' },
    ],
    from: { opacity: 0, color: 'red', transform: 'translateX(0px)' },
    config: { duration: 500 },
  })

  console.log('renderCount', ++renderCount)

  return <a.div style={styles}>I will fade in and out</a.div>
}

Demo: https://codesandbox.io/s/dazzling-rgb-j2bx3?file=/src/App.tsx

We can see that the renderCount hardly get printed out at all. react-spring should need to keep on updating the style of the component, so after a minute, I'd expect a lot of print out of renderCount like the first two examples above, but it does not.

How and why doesn't react-spring cause a lot of re-rendering in this case, and how do we know in what situation would react-spring cause a lot of re-rendering (and how to prevent it)?

nonopolarity
  • 146,324
  • 131
  • 460
  • 740

1 Answers1

5

react-spring updates styles incrementally to create animations (as opposed to css animations with transition).

Naive animations outside React

If react-spring was to exist outside of React (which it OBVIOUSLY doesn't because then it wouldn't be named react-spring), this could most easily be done by modifying a given element's style by means of Javascript according to some predetermined pattern based on multiple factors (like delay, duration, etc....). One scenario could be

...
setTimeout(() => document.getElementById("#el").style.opacity = 0.34,100)
setTimeout(() => document.getElementById("#el").style.opacity = 0.39,150)
setTimeout(() => document.getElementById("#el").style.opacity = 0.42,200)
...
setTimeout(() => document.getElementById("#el").style.opacity = 1.0, 1000)

Exactly how this would be implemented is of course not the point of this answer and the above would be a very naive implementation, but this is basically what could go on if we wanted to make some animated transition where the interpolation between two endpoints would be calculated and implemented by ourselves (using spring physics) as opposed to in the browser (with css transition).

Naive animations in React

In React, we know that the preferred way to do things is to provide changes inside React, which React then processes after which necessary changes to the DOM is handled by React. Taking the previous (naive) example to React, this would imply some scheme where a state storing the opacity would be updated repeatedly until the desired endpoint was reached.

const Component = () => {
    ...
    const [opacity, setOpacity] = useState(0)

    useEffect(() => {
        ...
        setTimeout(() => setOpacity(0.34),100)
        setTimeout(() => setOpacity(0.39),150)
        setTimeout(() => setOpacity(0.42),200)
        ...
        setTimeout(() => setOpacity(1.0), 1000)
    }, [])

    return (
        <div style={{ opacity }} ... />
    )
}

This would work, but as one would expect, it could be quite burdensome since animations are supposed to happen fast and smooth and React rerendering on every animation frame could be problematic; if the component within which animation took place was expensive to render, the animation itself could be suffering and not look very good.

react-spring in React

The solution to this problem by react-spring is to do updates OUTSIDE of React via refs instead. The previous toy example could look like:

const Component = () => {
    ...
    const ref = useRef(null)

    useEffect(() => {
        if(ref.current) {
            ...
            setTimeout(() => ref.current.style.opacity = 0.34,100)
            setTimeout(() => ref.current.style.opacity = 0.39,150)
            setTimeout(() => ref.current.style.opacity = 0.42,200)
            ...
            setTimeout(() => ref.current.style.opacity = 1.0, 1000)
        }
    }, [])
    ...
    return (
        <div ref={ref} ... />
    )
}

Again, this is an example, exactly how one would implement this in the best way (as in react-spring) is a different story. But we can agree on that if we would log to the console every time the above component rendered, it would only log once even though the opacity would continue to change.

To sum up, when react-spring is used optimally, it uses refs to update properties on DOM elements whereupon React is by-passed. Thanks to this, a component may render only once but still make repeated animations. This particularly applies to the situation when the api is used to perform updates (as oppose to storing a state in a parent component which is set every time we want an animation to take place):

const [spring, api] = useSpring(() => ({ <INITIAL PROPS> })) // use api to make changes
const spring = useSpring({ <INITIAL PROPS }) // rerender component to update props

When using the basic HTML elements supplied by react-spring (such as animated.div, animated.span etc...), react-spring takes care of attaching a ref on the corresponding DOM element and via this ref, it manages to animate the element and therefore also all the content in it. When creating your own custom component wrapped in animated, it is your concern to make sure that your custom component can take a ref (via forwardRef) and to pass it on to the element which should be animated, if you want optimal animations. If you don't do this, the element will be rerendered on every animation frame by react-spring. Even though this works too, it is suboptimal from a performance point of view.

Your examples

In your examples some other things are at play as well. In the first example, the hook useMeasure is being used from react-use-measure. This hook will continuously provide different values from the child component (here height is provided) whereupon the parent will rerender. Since Tree components are nested, whenever ONE Tree component changes height, all parent Tree components whose heights will be changed will also rerender. We therefore see quite a lot of rerendering. Also, because StrictMode is enabled, the number is doubled. useSpring is used without api but it doesn't matter here since the parent rerenders a lot due to useMeasure anyways.

In your second example with react-spring, the api is not used either but since the animation is looped, it doesn't require any state in the parent to be set and so it doesn't rerender. Because the parent doesn't rerender, the animated component doesn't rerender either and so also in this case, it doesn't matter if we use the api or not. In this example if we would like to update the animated props however, using the api to do so would cause the parent NOT to rerender whereas otherwise, the state holding the props to update to would reside in the parent (or a grand-parent) and so the animated component would rerender as well when the parent rerenders.

fast-reflexes
  • 4,891
  • 4
  • 31
  • 44