I’m working on an Expo-managed React-Native for Web app and I’m trying to get some specific drag & drop functionality to work. I’ve tried a variety of changes to DraggableWord but everything I do either 1) creates the deprecation warning or 2) causes the DraggableWord to lose draggability (it simply acts like regular text).
I think the problem is inside DraggableWord but I’m not sure.
Here is a fully working, self-contained component script that should throw the warning when you test it.
import React, { useState, useEffect, useRef } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, useAnimatedRef } from 'react-native-reanimated';
const theme = {
colors: {
primaryTwo: '#007bff',
successTwo: '#28a745',
error: '#dc3545',
text: '#212529',
textMuted: '#6c757d',
grey: '#f8f9fa',
border: '#dee2e6',
},
};
const SentenceBlankAnimatedDropZone = ({ blankIndex, droppedWord, isCorrect, answer, hoverStates, onLayoutMeasured }) => {
const viewRef = useRef(null);
const isAttempted = droppedWord !== null;
const animatedDropZoneStyle = useAnimatedStyle(() => {
const isHovering = hoverStates.value[blankIndex] || false;
return {
borderColor: isHovering ? theme.colors.primaryTwo : (isAttempted ? (isCorrect ? theme.colors.successTwo : theme.colors.error) : theme.colors.text),
borderWidth: isHovering ? 3 : 1,
};
});
const handleLayout = () => {
if (viewRef.current && typeof viewRef.current.getBoundingClientRect === 'function') {
const rect = viewRef.current.getBoundingClientRect();
onLayoutMeasured(blankIndex, {
x: rect.left + window.scrollX,
y: rect.top + window.scrollY,
width: rect.width,
height: rect.height,
});
}
};
const styles = StyleSheet.create({
dropZone: { borderWidth: 1, borderRadius: 4, minWidth: 80, height: 40, justifyContent: 'center', alignItems: 'center', marginHorizontal: 5, paddingHorizontal: 8 },
blankNumber: { color: theme.colors.textMuted, fontSize: 16 },
droppedWordText: { fontSize: 22, color: theme.colors.text },
});
return (
<Animated.View
ref={viewRef}
onLayout={handleLayout}
style={[styles.dropZone, animatedDropZoneStyle]}
>
{isAttempted ? (<Text style={styles.droppedWordText}>{isCorrect ? droppedWord : answer}</Text>) : (<Text style={styles.blankNumber}>{blankIndex + 1}</Text>)}
</Animated.View>
);
};
const DraggableWord = ({ choice, answer, onDrop, dropZoneLayout, isAttempted, isCorrect, droppedWord, blankIndex, hoverStates }) => {
const opacity = useSharedValue(1);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const animatedRef = useAnimatedRef();
useEffect(() => { if (!isAttempted) { opacity.value = 1; } }, [isAttempted]);
const panGesture = Gesture.Pan()
.runOnJS(true)
.enabled(!isAttempted)
.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 } });
// THIS WORKS BUT PRODUCES THE WARNING
return (
<GestureDetector gesture={panGesture}>
<Animated.View ref={animatedRef} style={animatedStyle}>
<Text style={styles.wordText}>{choice}</Text>
</Animated.View>
</GestureDetector>
);
};
const WordBank = ({ blank, blankIndex, onDrop, dropZoneLayout, droppedWord, isCorrect, hoverStates }) => {
const styles = StyleSheet.create({
bankContainer: { alignItems: 'center', marginVertical: 10, padding: 10, borderWidth: 1, borderColor: theme.colors.border, borderRadius: 8, width: '100%' },
bankTitle: { color: theme.colors.textMuted, fontSize: 16, marginBottom: 5 },
wordsWrapper: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center' },
});
return (
<View style={styles.bankContainer}>
<Text style={styles.bankTitle}>Bank {blankIndex + 1}</Text>
<View style={styles.wordsWrapper}>
{blank.choices.map((choice) => (
<DraggableWord
key={choice} choice={choice} answer={blank.answer} onDrop={onDrop}
dropZoneLayout={dropZoneLayout} isAttempted={droppedWord !== null}
isCorrect={isCorrect} droppedWord={droppedWord} blankIndex={blankIndex}
hoverStates={hoverStates}
/>
))}
</View>
</View>
);
};
const CaseDragAndDrop = () => {
const sentenceWithBlank = "The quick brown fox [BLANK] over the lazy dog.";
const blanks = [
{
answer: "jumps",
choices: ["runs", "sleeps", "jumps"],
},
];
const numBlanks = blanks.length;
const [droppedWords, setDroppedWords] = useState(() => Array(numBlanks).fill(null));
const [correctness, setCorrectness] = useState(() => Array(numBlanks).fill(null));
const [dropZoneLayouts, setDropZoneLayouts] = useState({});
const hoverStates = useSharedValue({});
const handleDrop = (blankIndex, word) => {
if (droppedWords[blankIndex]) return;
setDroppedWords(prev => { const newWords = [...prev]; newWords[blankIndex] = word; return newWords; });
setCorrectness(prev => { const newCorrectness = [...prev]; newCorrectness[blankIndex] = word === blanks[blankIndex].answer; return newCorrectness; });
};
const handleDropZoneLayout = (index, layout) => {
if (JSON.stringify(dropZoneLayouts[index]) !== JSON.stringify(layout)) {
setDropZoneLayouts(prev => ({ ...prev, [index]: layout }));
}
};
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', padding: 20, width: '100%', maxWidth: 700, alignSelf: 'center' },
sentenceOuterContainer: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', paddingHorizontal: 10, lineHeight: 50 },
sentenceText: { fontSize: 22, color: theme.colors.text, marginHorizontal: 2, lineHeight: 40 },
wordBanksContainer: { width: '100%', marginTop: 20 },
});
let blankCounter = 0;
const renderedSentence = sentenceWithBlank.split(/([BLANK])/g).map((segment, index) => {
if (segment === '[BLANK]') {
const currentBlankIndex = blankCounter++;
return (
<SentenceBlankAnimatedDropZone
key={`blank-${currentBlankIndex}`}
blankIndex={currentBlankIndex}
droppedWord={droppedWords[currentBlankIndex]}
isCorrect={correctness[currentBlankIndex]}
answer={blanks[currentBlankIndex].answer}
hoverStates={hoverStates}
onLayoutMeasured={handleDropZoneLayout}
/>
);
}
return segment ? <Text style={styles.sentenceText} key={`text-${index}`}>{segment}</Text> : null;
});
return (
<View style={styles.container}>
<View style={{ height: 40 }} />
<View style={styles.sentenceOuterContainer}>{renderedSentence}</View>
<View style={{ height: 20 }} />
<View style={styles.wordBanksContainer}>
{blanks.map((blank, index) => (
<WordBank
key={`bank-${index}`} blank={blank} blankIndex={index} onDrop={handleDrop}
dropZoneLayout={dropZoneLayouts[index]} droppedWord={droppedWords[index]}
isCorrect={correctness[index]} hoverStates={hoverStates}
/>
))}
</View>
</View>
);
};
export default function App() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<CaseDragAndDrop />
</GestureHandlerRootView>
);
}