简体   繁体   中英

react-spring how to use animated(Component)

I am trying to sort out some performance problems in an image slider and I have discovered that using animated.img yields much better performance than using animated.div with some react component inside.

The react component obviously isn't just placed inside for the fun of it, but luckily react-spring lets you animate a custom component by doing

const AnimatedComponent = animated(Component)

as per the docs

But how do I use it? I have been trying but typescript just gives some really unhelpful message about missing 269 different types of props.

EDIT added error

The typescript error is shown by vscode, but it might not be important. Since I have no idea what props to pass in order to animate the component I am not surprised that it does not work, but the error message is not really helping in determining what I need to do.

' is missing the following properties from type 'AnimatedProps<{ title: string | FluidValue<string, any>; id: string | FluidValue<string, any>; article?: { title: string; metaTitle: string; metaDescription: string; description: string; showRelatedArticles: boolean; elements: ({ ...; } | ... 4 more ... | { ...; })[]; } | null | undefined; ... 269 more ...; key?: st...': title, id, slot, animate, and 257 more.ts(2740)

I stripped some of the first props since I recognise them from the component I am trying to animate and I know that they are present.

Have someone tried using this? An example of how to use it would be really nice.

I am on the 9.0.0-rc.3 version of react-spring if that matters.

Background

react-spring does not rely on timing, like the css transition api, but instead does transitions and animations from a physics context. To achieve this with acceptable performance in React, it bypasses React and makes modifications to the relevant DOM nodes themselves.

Regular react-spring animation components

As you might have seen, all regular DOM nodes exist as react-spring equivalents. For example animation.span , animation.div etc... These wrap the native DOM elements with the necessary functionality for react-spring to work. What is worth to notice here are these two subtleties:

  • The functionality of react-spring is attached to a single DOM node
  • Because the functionality is attached to a native DOM node, only native DOM element props are used

Both of these facts have implications for how we can use custom components wrapped in animated .

Our custom component

Let's work with a simple scenario using React functional components and Typescript and see how you can translate it into a custom react-spring component.

Let's say you have a div whose background color you want to animate when transitioning from one color to another after clicking it.

1. Without react-spring

The basic approach would be:

const Comp: FC = () => {

    const [color, setColor] = useState<string>("green")

    return (
        <div
            style={{ 
                backgroundColor: color,
                transition: "background-color 1s"
            }}
            onClick={ () => setColor(color => color === "blue" ? "green" : "blue") }
        />
    )
}
2. With react-spring basic usage

Doing the same thing with react-spring basic usage of useSpring would result in

const Comp: FC = () => {
    
    const [color, setColor] = useState<string>("green")
    const springColor = useSpring({ backgroundColor: color})

    return (
        <animated.div
            style={springColor}
            onClick={ () => setColor(color => color === "blue" ? "green" : "blue") }
        />
    )
}
3. With react-spring best practices usage

Even better is to use the api functions so that we don't have to rerender the component on every color change. To be clear, when you use this method, you're not changing any of the props passed to the component you want to animate, and so you can change its state via the api without rerendering it, as long as Comp itself doesn't rerender.

const Comp: FC = () => {

    const [springColor, api] = useSpring(() => ({ backgroundColor: "green" }))

    return (
        <animated.div
            style={springColor}
            onClick={ () => api.start({ backgroundColor: springColor.backgroundColor.goal === "blue" ? "green" : "blue" })}
        />
    )
}
4. With delegation to class

Let's think about it. You're passing some wrapped property to these animated components. These properties are of type SpringValue<T> and they can be instantiated by means of new or via, for example, useSpring . Our first step towards building a custom component would be to simply pass these as properties to a component that has an animated component in it:

export interface CompProps {
    color: SpringValue<string>;
    onChangeColor: () => void;
}

const Comp: FC<CompProps> = (props: CompProps) => {

    return (
        <animated.div
            style={{ backgroundColor: props.color }}
            onClick={props.onChangeColor}
        />
    )
}

const Parent: FC = () => {

    const [springColor, api] = useSpring(() => ({ backgroundColor: "green" }));

    return (
        <Comp
            color={springColor.backgroundColor}
            onChangeColor={() => api.start({
                backgroundColor: springColor.backgroundColor.goal === "blue" ? "green" : "blue"
            })}
        />
    )
}
5. With a naive custom component wrapped in animated

Now we are ready to do the replacement and wrap our property in animated instead of using the animated native element inside our component.

export interface CompProps {
    style: CSSProperties;
    onChangeColor: () => void;
}

const Comp: FC<CompProps> = (props: CompProps) => {

    return (
        <div
            style={props.style}
            onClick={props.onChangeColor}
        />
    )
}

const WrappedComp: AnimatedComponent<FC<CompProps>> =
    animated(Comp)

const Parent: FC = () => {

    const [springColor, api] = useSpring(() => ({ backgroundColor: "green" }));

    return (
        <WrappedComp
            style={{ backgroundColor: springColor.backgroundColor }}
             onChangeColor={() =>
                 api.start({
                     backgroundColor:
                         springColor.backgroundColor.goal === "blue" ? "green" : "blue"
                  })
             }
         />
    )
}

Note well that now our wrapped component looks like a regular component and it shows no sign of being used together with react-spring . Nonetheless, as we shall see, there are still some additional requirements on it for the integration with react-spring to work as intended. Note how we are no longer suppling backgroundColor as a prop but instead we use style . Also, all the props passed to our custom component are props available on the native div element on which our custom component attaches the forwarded ref. More on this further down.

The above component is naive, because the animated wrapper can't update the wrapped component without rerendering. Why? Simply because our wrapped component does not accept a ref and so demanding from react-spring to be able to update our component without rerendering it is unreasonable.

6. With a proper custom component wrapped in animated

Let's enhance our wrapped component by letting it accept a ref.

const Comp: FC<CompProps & RefAttributes<HTMLDivElement>> = 
    forwardRef<HTMLDivElement,CompProps>(
        (props, ref) => {

            return (
                <div 
                    ref={ref} 
                    style={props.style} 
                    onClick={props.onChangeColor} 
                />
            )
        }
    )

Now react-spring will sense that our wrapped component accepts a ref and so it will refrain from rerendering it on every change and instead use the ref to update it. Because updates take place via a ref, it is important that the props are actual props on the element we want to change; if React is needed to map the custom props to the actual props on the DOM element, then we are required to rerender on every new animation value, which is suboptimal. Nonetheless, this is exactly what happens when we DON'T enable our custom component to take a ref. Therefore, version 5 would have worked even if we had continued to use the custom props backgroundColor instead of style . Version number 6 had however not worked, and the property backgroundColor , would simply have been added to the DOM element on which the ref was set, which would not result in any change since this property is not a property on the native div DOM element.

Sandboxes

I have made two sandboxes available:

  • The first sandbox shows the components in this answer. Each component makes a printout in the console whenever it renders. Check these printouts and verify the behaviours described above.Sandbox

  • The second sandbox is on the same theme but slightly more advanced. Here the number of renderings is kept up to date, so that we can verify different behaviours. One of the take-aways from this sandbox is that all the animated changes that we do using the api are "free" in the sense that it does not increase the number of renderings for the affected components. When the parent rerenders, as usual, all children rerender as well. A list of important points has been added at the bottom. Sandbox

Conclusion

  • Always use the api methods when using react-spring hooks.
  • Always make your components accept a ref when wrapping them in animated .
  • If you want to be able to update your wrapped component without rerendering it, you can only attach the behaviour of react-spring to ONE element. react-spring does its updates using a ref and since you can't attach a ref to multiple elements at the same time (nor does forwardRef provide a way to add multiple refs), you can't use your springs in multiple places in a wrapped component without rerendering it or you will not arrive at the expected functionality. With such a component, it is better to pass SpringValue<T> as props and use animated native elements inside the component instead.

Don't:

const NestedComp = ({ style1, style2 }) => {
    ...
    return (
        <div style={ style1 }>
            <div style={ style2 }>
                ....
            </div>
        </div>
    )
}

const Wrapped = animated(NestedComp)

Do:

const NestedComp = ({ springStyle1, springStyle2 }) => {
    ...
    return (
        <animated.div style={ springStyle1 }>
            <animated.div style={ springStyle2 }>
                ....
            </animated.div>
        </animated.div>
    )
}
  • Make sure that the custom props you use on your element is compatible with the props on the native DOM element on which you attach the ref, otherwise react-spring can't properly update the element directly (instead it just sets the property which, if it doesn't exist on the DOM element, has no effect).

Don't:

const Comp = ({ color }) => {
    ...
    return (
        <div style={{ backgroundColor: props.color, height: "100px" }} />
    )
}

const WrappedComp = animated(Comp)

const Parent = () => {
    ...
    const [springProp, api] ) = useSpring(() => ({ color: "green" }))
    ...
    return <WrappedComp color={ springProp.color } />
}

Do:

const Comp = ({ style }) => {
    ...
    return (
        <div style={props.style} />
    )
}

const WrappedComp = animated(Comp)

const Parent = () => {
    ...
    const [springProp, api] ) = useSpring(() => ({ color: "green" }))
    ...
    return (
         <WrappedComp 
             style={{ backgroundColor: springProp.color }}
         />
    )
}

In the above, we want to update Comp without using React, but React is required to assemble the proper style prop since color is not a natural prop of a div element, therefore updating color via a ref will only lead to the color prop being set on the div element which does nothing. On the other hand, when we add the style property already in the parent, the animated component will detect that one of the style props is a SpringValue and updating it accordingly will have the expected effect.

Finally, remember that if we are not after updating our custom component with the api, we could simply refrain from designing our custom component so that it can take a ref and use whichever prop names we want; react-spring will anyways now rerender the component on every animation frame and so React will map all the custom props to the correct native DOM element props. Nonetheless, this strategy of execution is hopefully not desirable.

Just to start the conversation.

Let's start with the documentation's example. Let's suppose you have a third party Donut component. It has a percent property. And you want to animate based on this property. So you can use animated as a wrapper around Donut.

const AnimatedDonut = animated(Donut)
// ...
const props = useSpring({ value: 100, from: { value: 0 } })
return <AnimatedDonut percent={props.value} />

Is it where the problem occurs?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM