【问题标题】:react-spring how to use animated(Component)react-spring 如何使用动画(组件)
【发布时间】:2020-10-07 00:45:25
【问题描述】:

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

react 组件显然不只是为了好玩而放在里面,但幸运的是 react-spring 可以让你通过做动画自定义组件

const AnimatedComponent = animated(Component)

根据the docs

但是我该如何使用它呢?我一直在尝试,但 typescript 只是给出了一些关于缺少 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。

【问题讨论】:

  • 你从哪里得到打字稿消息,它到底是什么?
  • @PeterAmbruzs 我添加了错误信息
  • 是的,我知道如何创建自定义动画组件,但不知道如何更改自定义组件以使动画工作。一定有一些传下来的道具或其他东西。动画组件在动画时如何知道要渲染什么?
  • @PeterAmbruzs 我不知道你为什么删除你的答案。我打算接受它,因为它是正确的。我有明显的解决方案,但其他东西让它崩溃了,我认为必须有其他方法来做到这一点。
  • 我只是觉得这种形式太琐碎了。我将其发布为对话启动器,以解决您的问题。我取消删除它。

标签: javascript reactjs typescript react-spring


【解决方案1】:

背景

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

常规react-spring动画组件

您可能已经看到,所有常规 DOM 节点都以 react-spring 等效项的形式存在。例如animation.spananimation.div 等...这些用react-spring 工作所需的功能包装了本机DOM 元素。这里值得注意的是这两个微妙之处:

  • 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基本用法

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

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。委派到班级

让我们考虑一下。您正在将一些包装属性传递给这些 animated 组件。这些属性的类型为SpringValue&lt;T&gt;,它们可以通过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 元素上可用的道具,我们的自定义组件在该元素上附加转发的 ref。更多关于这方面的内容。

上面的组件很幼稚,因为animated 包装器无法在不重新渲染的情况下更新被包装的组件。为什么?仅仅因为我们包装的组件不接受 ref 并且要求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 元素中,这不会导致任何更改,因为此属性不是本机 @987654368 上的属性@DOM 元素。

沙盒

我提供了两个沙盒:

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

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

结论

  • 在使用react-spring hooks 时始终使用api方法。
  • 在将组件包装在 animated 中时,始终让组件接受引用。
  • 如果您希望能够在不重新渲染的情况下更新包装的组件,您只能将react-spring 的行为附加到一个元素。 react-spring 使用 ref 进行更新,并且由于您不能同时将 ref 附加到多个元素(forwardRef 也不提供添加多个 refs 的方法),因此您不能在多个地方使用弹簧在包装组件中而不重新渲染它,否则您将无法获得预期的功能。对于这样的组件,最好将 SpringValue&lt;T&gt; 作为 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 需要组装正确的 style prop,因为 color 不是 div 元素的原生 prop,因此更新 @987654385 @ via a ref 只会导致 color 属性被设置在 div 元素上,它什么都不做。另一方面,当我们在父级中添加 style 属性时,animated 组件将检测到 style 属性之一是 SpringValue 并相应地更新它会产生预期的效果。

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

【讨论】:

  • 多么详细的答案。我想我还是很困惑,我们可以使用 animated() 来为除了 style 之外的 props 设置动画,还是 react-spring 的限制?
  • 你想要动画的哪些道具最终不会成为样式道具?给我一个用例:)
  • 我正在考虑为 gl-react 着色器设置动画,其中道具控制着色器制服。 Gl-react 文档确实有一个使用“动画”库的demo,但该示例已经过时并且使用样式值作为更新着色器道具的黑客。我知道必须可以使用 react-spring 为非样式道具制作动画,因为 @react-spring/three 做到了。但是对于如何将他们的方法应用于我自己的组件,我有点困惑。
  • 好吧,不知道这一切,但是是的,你可以为非原生属性设置动画,但效率不高,因为 React 组件会在每个动画帧上重新渲染。不过在大多数情况下它应该足够好。只需将您的组件包装在animated 中并跳过您确保您的组件可以接受 ref 的部分,您应该很高兴:) 另一方面,如果您的组件仅使用该值来控制一些本机 DOM 属性,那么你最好直接短路它(不必是样式,只是一些原生 DOM 属性)
【解决方案2】:

只是为了开始对话。

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

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

问题出在哪里?

【讨论】:

  • 我刚刚陷入了同样的问题,但我的情况是我的自定义组件有 6 个道具,我需要这些道具来渲染组件,但与动画无关。那我该如何通过呢?
  • @SoumalyaBhattacharya 新的动画组件与原始组件具有相同的属性。您应该只切换具有相同签名的新组件。
猜你喜欢
  • 2021-03-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-23
  • 1970-01-01
  • 2021-03-19
  • 1970-01-01
  • 2020-07-28
相关资源
最近更新 更多