背景
react-spring 不像 css 转换 api 那样依赖于时间,而是从物理上下文中进行转换和动画。为了在 React 中以可接受的性能实现这一点,它绕过了 React 并对相关的 DOM 节点本身进行了修改。
常规react-spring动画组件
您可能已经看到,所有常规 DOM 节点都以 react-spring 等效项的形式存在。例如animation.span、animation.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<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 元素上可用的道具,我们的自定义组件在该元素上附加转发的 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<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 需要组装正确的 style prop,因为 color 不是 div 元素的原生 prop,因此更新 @987654385 @ via a ref 只会导致 color 属性被设置在 div 元素上,它什么都不做。另一方面,当我们在父级中添加 style 属性时,animated 组件将检测到 style 属性之一是 SpringValue 并相应地更新它会产生预期的效果。
最后,请记住,如果我们没有在使用 api 更新自定义组件之后,我们可以简单地避免设计自定义组件,以便它可以获取 ref 并使用我们想要的任何 prop 名称; react-spring 现在无论如何都会在每个动画帧上重新渲染组件,因此 React 会将所有自定义道具映射到正确的原生 DOM 元素道具。尽管如此,这种执行策略还是不可取的。