You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

将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变化,所以能正常运行。

解决步骤

  1. 移除useAnimatedStyle的依赖数组:SharedValue是Reanimated的响应式值,useAnimatedStyle会自动监听其.value的变化,不需要手动添加到依赖数组中。
  2. 修正后的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 21:07:33