将React Reanimated动画逻辑移至Hook引发错误的原因排查
React Native Reanimated 自定义Hook迁移报错解析
问题描述
将React Native Reanimated动画逻辑从组件内迁移到自定义Hook后,出现错误:ReanimatedError: [Reanimated] Tried to synchronously call a non-worklet function on the UI thread,移回组件内则正常运行。
Hook代码(报错版本)
import { useState } from "react"; import { Dimensions, Easing } from "react-native"; import { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"; const screenWidth = Dimensions.get('screen').width; export const useWelcomeSlider = () => { const [step, setStep] = useState(0); const [final, setFinal] = useState(false); const strokeRotation = useSharedValue(-20); const fillRotation = useSharedValue(0); const imageOpacity = useSharedValue(1); const strokeStyle = useAnimatedStyle(() => { return { transform: [{ rotate: `${strokeRotation.value}deg` }], }; }, [strokeRotation]); const fillStyle = useAnimatedStyle(() => { return { transform: [{ rotate: `${fillRotation.value}deg` }], }; }, [fillRotation]); const imageStyle = useAnimatedStyle(() => { return { opacity: imageOpacity.value, }; }, [imageOpacity]); const buttonStyle = useAnimatedStyle(() => ({ width: withTiming(step < 2 ? 86 : screenWidth - 32, { duration: 200 }, () => { if (step === 2) { runOnJS(setFinal)(true); } }), })); const handleStepChange = () => { const strokeTargetRotation = (step + 1) * 20; const fillTargetRotation = (step + 1) * 20; strokeRotation.value = withTiming(strokeTargetRotation, { duration: 300, easing: Easing.linear, }); fillRotation.value = withTiming( fillTargetRotation, { duration: 500, easing: Easing.linear, }, () => { imageOpacity.value = withTiming(1, { duration: 200 }); }, ); imageOpacity.value = withTiming(0, { duration: 200 }, () => { runOnJS(setStep)(step + 1); imageOpacity.value = withTiming(1, { duration: 500 }); }); }; return { strokeStyle, fillStyle, imageStyle, buttonStyle, final, step, handleStepChange } }
组件内正常代码
export const Welcome = () => { const [step, setStep] = useState(0); const [final, setFinal] = useState(false); const strokeRotation = useSharedValue(-20); const fillRotation = useSharedValue(0); const imageOpacity = useSharedValue(1); // ref const bottomSheetRef = useRef<BottomSheetModal>(null); const strokeStyle = useAnimatedStyle(() => { return { transform: [{rotate: `${strokeRotation.value}deg`}], }; }); const fillStyle = useAnimatedStyle(() => { return { transform: [{rotate: `${fillRotation.value}deg`}], }; }); const imageStyle = useAnimatedStyle(() => { return { opacity: imageOpacity.value, }; }); const buttonStyle = useAnimatedStyle(() => ({ width: withTiming(step < 2 ? 86 : screenWidth - 32, {duration: 200}, () => { if (step === 2) { runOnJS(setFinal)(true); } }), })); const handleStepChange = (stepModifier: number) => { const nextStep = step + stepModifier; if (nextStep < 0 || nextStep > 2) { bottomSheetRef.current?.present(); return; } const strokeTargetRotation = (step + 1) * 20; const fillTargetRotation = (step + 1) * 20; strokeRotation.value = withTiming(strokeTargetRotation, { duration: 300, easing: Easing.linear, }); fillRotation.value = withTiming( fillTargetRotation, { duration: 500, easing: Easing.linear, }, () => { imageOpacity.value = withTiming(1, {duration: 200}); }, ); imageOpacity.value = withTiming(0, {duration: 200}, () => { runOnJS(setStep)(step + stepModifier); imageOpacity.value = withTiming(1, {duration: 500}); }); }; return ( <SafeAreaView style={{paddingTop: 100, flex: 1, justifyContent: 'space-between'}}> <Presentation> <Animated.View style={[fillStyle, {position: 'absolute'}]}> <BlobFill /> </Animated.View> <Animated.Image style={[imageStyle, {position: 'absolute', ...imagesSizes[step]}]} source={imagesSources[step]} /> <Animated.View style={[strokeStyle, {position: 'absolute', top: 20}]}> <BlobStroke /> </Animated.View> </Presentation> <StepInfo> <Heading></Heading> <Description> </Description> </StepInfo> <StepIndicator step={step} /> <Footer> <AnimatedButton size="large" label={!final ? '' : ''} onPress={() => handleStepChange(1)} style={buttonStyle} /> </Footer> <BottomSheetModal backdropComponent={renderBackdrop} ref={bottomSheetRef} index={0} snapPoints={['25%']}> <View style={{gap: 12, paddingHorizontal: 24, paddingTop: 24}}> <Button size="large" label="" onPress={() => {}} /> <Button variant="secondary" size="large" label="" onPress={() => {}} /> </View> </BottomSheetModal> </SafeAreaView> ); };
问题原因及解决方法
核心原因
Reanimated的useAnimatedStyle回调会被编译成worklet函数,在UI线程执行。而自定义Hook中,你给useAnimatedStyle添加了包含SharedValue(strokeRotation、fillRotation、imageOpacity)的依赖数组,这会触发Reanimated错误地尝试将整个SharedValue对象同步到UI线程——SharedValue的正确访问方式是在worklet内通过.value属性,直接依赖整个对象会触发非worklet函数调用的错误。
另外,组件内代码没有给useAnimatedStyle添加依赖数组,Reanimated会自动跟踪SharedValue的.value变化,所以能正常运行。
解决步骤
- 移除
useAnimatedStyle的依赖数组:SharedValue是Reanimated的响应式值,useAnimatedStyle会自动监听其.value的变化,不需要手动添加到依赖数组中。 - 修正后的Hook代码:
import { useState } from "react"; import { Dimensions, Easing } from "react-native"; import { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"; const screenWidth = Dimensions.get('screen').width; export const useWelcomeSlider = () => { const [step, setStep] = useState(0); const [final, setFinal] = useState(false); const strokeRotation = useSharedValue(-20); const fillRotation = useSharedValue(0); const imageOpacity = useSharedValue(1); // 移除多余的依赖数组,Reanimated自动跟踪SharedValue变化 const strokeStyle = useAnimatedStyle(() => { return { transform: [{ rotate: `${strokeRotation.value}deg` }], }; }); const fillStyle = useAnimatedStyle(() => { return { transform: [{ rotate: `${fillRotation.value}deg` }], }; }); const imageStyle = useAnimatedStyle(() => { return { opacity: imageOpacity.value, }; }); const buttonStyle = useAnimatedStyle(() => ({ width: withTiming(step < 2 ? 86 : screenWidth - 32, { duration: 200 }, () => { if (step === 2) { runOnJS(setFinal)(true); } }), })); const handleStepChange = () => { const strokeTargetRotation = (step + 1) * 20; const fillTargetRotation = (step + 1) * 20; strokeRotation.value = withTiming(strokeTargetRotation, { duration: 300, easing: Easing.linear, }); fillRotation.value = withTiming( fillTargetRotation, { duration: 500, easing: Easing.linear, }, () => { imageOpacity.value = withTiming(1, { duration: 200 }); }, ); imageOpacity.value = withTiming(0, { duration: 200 }, () => { runOnJS(setStep)(step + 1); imageOpacity.value = withTiming(1, { duration: 500 }); }); }; return { strokeStyle, fillStyle, imageStyle, buttonStyle, final, step, handleStepChange } }
总结
自定义Hook中错误添加的SharedValue依赖数组,导致Reanimated在UI线程同步调用非worklet函数。移除这些不必要的依赖数组后,Hook就能和组件内代码一样正常运行。
内容的提问来源于stack exchange,提问作者Michał Staniewski
相关产品推荐
相关产品推荐

