react-spring 如何使用 animated(Component)

react-spring how to use animated(Component)

我正在尝试解决图像滑块中的一些性能问题,我发现使用 animated.img 比使用 animated.div 和内部一些反应组件产生更好的性能。

react 组件显然不仅仅是为了好玩而放在里面,但幸运的是 react-spring 允许您通过执行

来为自定义组件设置动画
const AnimatedComponent = animated(Component)

根据the docs

但是我该如何使用它呢?我一直在尝试,但打字稿只是给出了一些关于缺少 269 种不同类型道具的非常无用的信息。

编辑 添加错误

vscode 显示打字稿错误,但这可能并不重要。由于我不知道要传递什么道具才能为组件设置动画,所以我对它不起作用并不感到惊讶,但错误消息并不能真正帮助我确定我需要做什么。

' 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)

我删除了一些第一个道具,因为我从我试图制作动画的组件中认出了它们,并且我知道它们存在。

有人试过用这个吗?一个如何使用它的例子会非常好。

如果重要的话,我正在使用 9.0.0-rc.3 版本的 react-spring。

只是为了开始对话。

让我们从文档的示例开始。假设您有一个第三方 Donut 组件。它有一个百分比 属性。并且您想基于此 属性 制作动画。所以你可以使用动画作为甜甜圈的包装。

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

是哪里出问题了?

背景

react-spring 不像 css 转换 api 那样依赖于时间,而是从物理环境中进行转换和动画。为了在 React 中以可接受的性能实现这一点,它绕过 React 并对相关 DOM 节点本身进行修改。

常规react-spring动画组件

如您所见,所有常规 DOM 节点都作为 react-spring 等价物存在。例如 animation.spananimation.div 等...这些将本机 DOM 元素包装为 react-spring 工作所需的必要功能。这里值得注意的是这两个微妙之处:

  • react-spring 的功能附加到单个 DOM 节点
  • 因为该功能附加到本机 DOM 节点,所以仅使用本机 DOM 元素属性

这两个事实对我们如何使用包装在 animated 中的自定义组件都有影响。

我们的自定义组件

让我们使用 React 功能组件和 Typescript 处理一个简单的场景,看看如何将其转换为自定义 react-spring 组件。

假设您有一个 div,您希望在单击它后从一种颜色过渡到另一种颜色时为其背景颜色设置动画。

1。没有 react-spring

基本方法是:

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。 react-spring 基本用法

useSpringreact-spring 基本用法做同样的事情会导致

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。 react-spring 最佳实践用法

更好的方法是使用 api functions 这样我们就不必在每次颜色更改时都重新渲染组件。需要明确的是,当您使用此方法时,您不会更改任何传递给要设置动画的组件的道具,因此您可以通过 api 更改其状态而无需重新渲染它,只要 Comp 本身不会重新渲染。

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。委派给 class

让我们考虑一下。您正在将一些包装的 属性 传递给这些 animated 组件。这些属性属于 SpringValue<T> 类型,它们可以通过 new 或例如 useSpring 实例化。我们构建自定义组件的第一步是简单地将这些作为属性传递给其中包含 animated 组件的组件:

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。使用包裹在 animated
中的天真的自定义组件

现在我们准备好进行替换并将我们的 属性 包装在 animated 中,而不是在我们的组件中使用 animated 本机元素。

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

请注意,现在我们的包装组件看起来像一个常规组件,并且没有显示与 react-spring 一起使用的迹象。尽管如此,正如我们将要看到的,为了与 react-spring 的集成按预期工作,仍然有一些额外的要求。请注意,我们如何不再提供 backgroundColor 作为道具,而是使用 style。此外,传递给我们的自定义组件的所有动画道具都是在我们的自定义组件附加转发引用的原生 div 元素上可用的道具。更多关于这个的进一步下降。

上面的组件很幼稚,因为 animated 包装器无法在不重新渲染的情况下更新包装的组件。为什么?仅仅是因为我们包装的组件不接受引用,所以要求 react-spring 能够在不重新渲染的情况下更新我们的组件是不合理的。

6.使用包含在 animated
中的适当自定义组件

让我们通过让它接受引用来增强我们的包装组件。

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

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

现在 react-spring 会感觉到我们包装的组件接受了一个 ref,因此它将避免在每次更改时重新渲染它,而是使用 ref 来更新它。因为更新是通过 ref 进行的,所以 props 是我们要更改的元素的实际 props 很重要;如果 React 需要将自定义道具映射到 DOM 元素上的实际道具,那么我们需要重新渲染每个新的动画值,这是次优的。尽管如此,当我们不让我们的自定义组件接受引用时,这正是会发生的事情。因此,即使我们继续使用自定义属性 backgroundColor 而不是 style,版本 5 也能正常工作。然而,版本号 6 不起作用,属性 backgroundColor 将简单地添加到设置 ref 的 DOM 元素中,这不会导致任何更改,因为此属性 不是原生 div DOM 元素上的 属性。

沙盒

我已经制作了两个可用的沙箱:

  • 第一个沙箱显示了这个答案中的组件。每个组件在呈现时都会在控制台中打印输出。检查这些打印输出并验证上述行为。 Sandbox

  • 第二个沙盒主题相同,但稍微高级一些。这里渲染的数量是保持最新的,这样我们就可以验证不同的行为。此沙盒中的 take-aways 之一是我们使用 api 所做的所有动画更改都是“免费”的,因为它不会增加受影响组件的渲染数量。当 parent 像往常一样重新渲染时,所有 children 也会重新渲染。底部添加了要点列表。 Sandbox

结论

  • 在使用 react-spring 钩子时始终使用 api 方法。
  • animated.
  • 中包装组件时,始终让它们接受 ref
  • 如果您希望能够在不重新渲染的情况下更新您的包装组件,您只能将 react-spring 的行为附加到一个元素。 react-spring 使用 ref 进行更新,因为你不能同时将 ref 附加到多个元素(forwardRef 也没有提供添加多个 ref 的方法),你不能使用你的在包装组件中的多个位置弹出而不重新渲染它,否则您将无法获得预期的功能。对于这样的组件,最好将 SpringValue<T> 作为 props 传递,并在组件内部使用 animated 原生元素。

不要:

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

const Wrapped = animated(NestedComp)

做:

const NestedComp = ({ springStyle1, springStyle2 }) => {
    ...
    return (
        <animated.div style={ springStyle1 }>
            <animated.div style={ springStyle2 }>
                ....
            </animated.div>
        </animated.div>
    )
}
  • 确保您在元素的动画中使用的自定义道具与您附加 ref 的原生 DOM 元素的道具兼容,否则 react-spring 无法正确更新直接设置元素(相反,它只是设置 属性,如果 DOM 元素上不存在,则没有任何效果)。

不要:

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

const WrappedComp = animated(Comp)

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

做:

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 }}
         />
    )
}

在上面,我们想在不使用 React 的情况下更新 Comp,但是 React 需要 assemble 正确的 style 属性,因为 color 不是原生的div 元素的 prop,因此通过 ref 更新 color 只会导致 color prop 被设置在 div 元素上,什么都不做。另一方面,当我们在 parent 中添加 style 属性 时,animated 组件将检测到其中一个 style 道具是 SpringValue 并相应地更新它会达到预期的效果。

最后,请记住,如果我们不是在用 api 更新我们的自定义组件之后,我们可以简单地避免设计我们的自定义组件,以便它可以接受一个 ref 并使用我们想要的任何 prop 名称; react-spring 现在无论如何都会在每个动画帧上重新渲染组件,因此 React 会将所有自定义道具映射到正确的原生 DOM 元素道具。尽管如此,希望这种执行策略不可取。