Expo管理的React Native Web应用中拖拽组件出现findDOMNode已弃用警告的问题排查
我看到你在Expo管理的React Native Web应用里,用react-native-gesture-handler和Reanimated实现拖拽填空功能时,遇到了findDOMNode弃用警告的棘手问题——要么警告弹窗跳个不停,要么拖拽功能直接失效,而且问题明显集中在DraggableWord组件里。这个场景在React 18+的严格模式下特别常见,核心原因是GestureDetector无法正确获取Animated.View的原生DOM节点,只能回退到已弃用的findDOMNode API来兜底。下面是具体的排查思路和可直接落地的修复方案:
问题根源拆解
你的DraggableWord里,GestureDetector直接包裹了Animated.View,并且给Animated.View绑定的是useAnimatedRef(专为Reanimated动画设计的引用),而非原生DOM ref。在Web环境下,react-native-gesture-handler需要获取原生DOM节点来监听鼠标/触摸事件,当它从AnimatedRef拿不到有效节点时,就会调用已被React 18弃用的findDOMNode,触发警告。
分步修复方案
我们要做的是让GestureDetector直接拿到原生DOM节点的引用,同时保留Reanimated动画所需的AnimatedRef,具体修改如下:
1. 给DraggableWord添加组合引用
在DraggableWord内部同时创建原生DOM ref和Animated ref,再通过一个组合函数把两者绑定到同一个Animated.View上,兼顾动画和事件监听的需求。
2. 给PanGesture指定监听的原生ref
通过PanGesture的.ref()方法明确指定要监听的原生DOM节点,彻底避免GestureDetector内部调用findDOMNode。
3. 确保依赖版本兼容(可选但推荐)
升级到最新版的依赖,能避免旧版本的Web兼容性bug:
react-native-gesture-handler@^2.12.0+react-native-reanimated@^3.3.0+- Expo SDK 49+
修复后的完整DraggableWord组件
把你原有的DraggableWord替换成下面的代码即可:
const DraggableWord = ({ choice, answer, onDrop, dropZoneLayout, isAttempted, isCorrect, droppedWord, blankIndex, hoverStates }) => { const opacity = useSharedValue(1); const translateX = useSharedValue(0); const translateY = useSharedValue(0); // 1. 创建原生DOM ref和Reanimated专用ref const nativeDomRef = useRef(null); const animatedRef = useAnimatedRef(); // 组合ref:同时绑定到Animated.View,兼顾动画和事件监听 const combinedRef = (node) => { nativeDomRef.current = node; animatedRef.current = node; }; useEffect(() => { if (!isAttempted) { opacity.value = 1; } }, [isAttempted]); const panGesture = Gesture.Pan() .runOnJS(true) .enabled(!isAttempted) .ref(nativeDomRef) // 2. 明确指定Gesture监听的原生DOM节点 .onUpdate((event) => { translateX.value = event.translationX; translateY.value = event.translationY; let isHovering = false; if (dropZoneLayout) { isHovering = ( event.absoluteX > dropZoneLayout.x && event.absoluteX < dropZoneLayout.x + dropZoneLayout.width && event.absoluteY > dropZoneLayout.y && event.absoluteY < dropZoneLayout.y + dropZoneLayout.height ); } hoverStates.value = { ...hoverStates.value, [blankIndex]: isHovering }; }) .onEnd((event) => { hoverStates.value = { ...hoverStates.value, [blankIndex]: false }; const wordIsOverDropZone = dropZoneLayout && ( event.absoluteX > dropZoneLayout.x && event.absoluteX < dropZoneLayout.x + dropZoneLayout.width && event.absoluteY > dropZoneLayout.y && event.absoluteY < dropZoneLayout.y + dropZoneLayout.height ); if (wordIsOverDropZone) { onDrop(blankIndex, choice); if (choice === answer) { opacity.value = withTiming(0, { duration: 200 }); } else { translateX.value = withSpring(0); translateY.value = withSpring(0); } } else { translateX.value = withSpring(0); translateY.value = withSpring(0); } }); const animatedStyle = useAnimatedStyle(() => { let backgroundColor = theme.colors.grey; if (isAttempted) { if (choice === answer) { backgroundColor = theme.colors.successTwo; } else if (choice === droppedWord && !isCorrect) { backgroundColor = theme.colors.error; } } return { transform: [{ translateX: translateX.value }, { translateY: translateY.value }], zIndex: translateX.value !== 0 || translateY.value !== 0 ? 100 : 1, opacity: opacity.value, backgroundColor, padding: 10, margin: 5, borderRadius: 8, borderWidth: 1, borderColor: theme.colors.border, }; }); const styles = StyleSheet.create({ wordText: { fontSize: 18, color: theme.colors.text } }); // 3. 使用组合ref绑定Animated.View return ( <GestureDetector gesture={panGesture}> <Animated.View ref={combinedRef} style={animatedStyle}> <Text style={styles.wordText}>{choice}</Text> </Animated.View> </GestureDetector> ); };
验证修复效果
- 启动Expo Web的严格模式测试,确认
findDOMNode弃用警告消失; - 测试完整的拖拽流程:拖拽单词、悬停时的边框高亮、放下后的正确/错误状态反馈、复位动画等功能是否正常;
- 如果仍有异常,可以临时禁用React严格模式(仅用于验证),确认是否是其他代码逻辑的问题。
内容来源于stack exchange

