I'm using React Native's Pan Responder. I need to update some state in a parent when a Pan Responder in it's children is dragged.
The complex bit is that I also need these children to insert their own element into the draggable area.
For context, here is simpler example that works fine https://snack.expo.io/@jamesweblondon/drag-items
import * as React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';
const items = ['1', '2', '3'];
const ITEM_HEIGHT = 100;
const Parent = () => {
const [y, setY] = useState(0);
const [index, setIndex] = useState(null);
return (
<View style={{ marginTop: 50 }}>
<Text>Index: {index}</Text>
<Text>Y: {y}</Text>
<View
style={{ height: ITEM_HEIGHT * items.length, backgroundColor: 'gold' }}>
{items.map((item, itemIndex) => {
const isBeingDragged = itemIndex === index;
const top =
isBeingDragged
? (ITEM_HEIGHT * itemIndex) + y
: (ITEM_HEIGHT * itemIndex);
return (
<View
style={{
top,
width: '100%',
position: 'absolute',
zIndex: isBeingDragged ? 1 : 0
}}
key={itemIndex}>
<Child
index={itemIndex}
setIndex={setIndex}
setY={setY}
item={item}
/>
</View>
);
})}
</View>
</View>
);
};
const Child = ({ index, setIndex, setY, item }) => {
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
setIndex(index);
},
onPanResponderMove: (evt, gestureState) => {
setY(gestureState.dy);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
setY(0);
setIndex(null);
},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => true,
})
).current;
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: 'tomato',
padding: 10,
borderBottomColor: 'black',
borderBottomWidth: 1,
height: ITEM_HEIGHT,
}}>
<View
{...panResponder.panHandlers}
style={{ background: 'grey', height: '100%', width: 40 }}
/>
<Text>Child {item}</Text>
</View>
);
};
export default Parent;
Here is my full example: https://snack.expo.io/@jamesweblondon/drag2
import * as React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';
const CHILD_A = 'CHILD_A';
const CHILD_B = 'CHILD_B';
const CHILD_A_HEIGHT = 100;
const CHILD_B_HEIGHT = 200;
const items = [
{ type: CHILD_A, text: '1' },
{ type: CHILD_B, text: '2' },
{ type: CHILD_A, text: '3' },
];
const Parent = () => {
const [y, setY] = useState(0);
const [index, setIndex] = useState(null);
const heights = items.map((item) =>
item.type === CHILD_A ? CHILD_A_HEIGHT : CHILD_B_HEIGHT
);
let heightsSum = 0;
const heightsCumulative = heights.map(
(elem) => (heightsSum = heightsSum + elem)
);
return (
<View style={{ marginTop: 50 }}>
<Text>Index: {index}</Text>
<Text>Y: {y}</Text>
<View style={{ height: heightsSum, backgroundColor: 'gold' }}>
{items.map((item, itemIndex) => {
if (item.type === CHILD_A) {
return (
<ChildA
index={itemIndex}
setIndex={setIndex}
setY={setY}
text={item.text}
DragHandle={(props) => (
<DragHandle
{...props}
index={itemIndex}
setIndex={setIndex}
setY={setY}
/>
)}
/>
);
}
return (
<ChildB
index={itemIndex}
setIndex={setIndex}
setY={setY}
text={item.text}
DragHandle={(props) => (
<DragHandle
{...props}
index={itemIndex}
setIndex={setIndex}
setY={setY}
/>
)}
/>
);
})}
</View>
</View>
);
};
const DragHandle = ({ index, setIndex, setY, children }) => {
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
setIndex(index);
},
onPanResponderMove: (evt, gestureState) => {
console.log(gestureState.dy); // This works when the line below is removed :)
setY(gestureState); // This does not work :(
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => true,
})
).current;
return (
<View
{...panResponder.panHandlers}
style={{ background: 'grey', height: '100%', width: 40, padding: 10 }}>
{children}
</View>
);
};
const ChildA = ({ index, setIndex, setY, text, DragHandle }) => {
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: 'gold',
padding: 10,
borderBottomColor: 'black',
borderBottomWidth: 1,
height: CHILD_A_HEIGHT,
}}>
<DragHandle>
<View
style={{ backgroundColor: 'goldenrod', width: '100%', height: '100%' }}
/>
</DragHandle>
<Text>Child A: {text}</Text>
</View>
);
};
const ChildB = ({ index, setIndex, setY, text, DragHandle }) => {
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: 'green',
padding: 10,
borderBottomColor: 'black',
borderBottomWidth: 1,
height: CHILD_B_HEIGHT,
}}>
<DragHandle>
<View
style={{ backgroundColor: 'lawngreen', width: '100%', height: '100%' }}
/>
</DragHandle>
<Text>Child B: {text}</Text>
</View>
);
};
export default Parent;
When you drag on the DragHandle
component these 2 functions fire once initially but don't again as you continue to drag:
console.log(gestureState.dy); // This works when the line below is removed :)
setY(gestureState); // This does not work :(
If I comment out this line: setY(gestureState); // This does not work :(
Then the console.log above it does work. It continues to log many times per second as you drag over it: console.log(gestureState.dy); // This works when the line below is removed :)
I therefore think it's to do with the Pan Responder being recreated when the parent's state changes but I'm not sure how to fix it. I'm also not sure why the simpler example doesn't have this issue.
import React from 'react';
import { useRef, useState } from 'react';
import { Text, View, PanResponder } from 'react-native';
const CHILD_A = 'CHILD_A';
const CHILD_B = 'CHILD_B';
const CHILD_A_HEIGHT = 100;
const CHILD_B_HEIGHT = 200;
const _items = [
{ type: CHILD_A, height: CHILD_A_HEIGHT, text: 'A' },
{ type: CHILD_B, height: CHILD_B_HEIGHT, text: 'B' },
{ type: CHILD_A, height: CHILD_A_HEIGHT, text: 'C' },
];
const Parent = () => {
const [y, setY] = useState(0);
const [index, setIndex] = useState(null);
const [items, setItems] = useState(_items);
const heights = items.map((item) =>
item.type === CHILD_A ? CHILD_A_HEIGHT : CHILD_B_HEIGHT
);
let heightsSum = 0;
const heightsCumulative = heights.map(
(elem) => (heightsSum = heightsSum + elem)
);
function setPosition(index, position) {
setItems(items => {
if (!items[index].hasOwnProperty('position')) {
items[index]['position'] = position;
}
return items;
})
}
return (
<View style={{ marginTop: 50 }}>
<Text>Index: {index}</Text>
<Text>Y: {y}</Text>
<View style={{ height: heightsSum, backgroundColor: 'gold' }}>
{items.map((item, itemIndex) => {
const isBeingDragged = itemIndex === index;
const top = isBeingDragged
? item.position + y
: item.position;
const childProps = {
top,
setPosition,
index: itemIndex,
setIndex: setIndex,
setY: setY,
text: item.text,
isBeingDragged: isBeingDragged
};
if (item.type === CHILD_A) {
return (
<ChildA
{...childProps}
/>
);
}
return (
<ChildB
{...childProps}
/>
);
})}
</View>
</View>
);
};
const DragHandle = ({ index, setIndex, setY, children }) => {
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
onPanResponderGrant: (evt, gestureState) => {
setIndex(index);
},
onPanResponderMove: (evt, gestureState) => {
setY(gestureState.dy);
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
setY(0);
setIndex(null);
},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => true,
})
).current;
return (
<View
{...panResponder.panHandlers}
style={{ background: 'grey', width: 40, padding: 10 }}>
{children}
</View>
);
};
const ChildA = ({ index, top, isBeingDragged, setPosition, setIndex, setY, text }) => {
return (
<View
style={{
top,
width: '100%',
position: top == undefined ? 'relative' : 'absolute',
zIndex: isBeingDragged ? 1 : 0,
}}
onLayout={(evt) => setPosition(index, evt.nativeEvent.layout.y)}
key={index}>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: 'gold',
padding: 10,
borderBottomColor: 'black',
borderBottomWidth: 1,
height: CHILD_A_HEIGHT,
}}>
<DragHandle index={index} setIndex={setIndex} setY={setY} text={text}>
<View
style={{
backgroundColor: 'goldenrod',
width: '100%',
height: '100%',
}}
/>
</DragHandle>
<Text>Child A: {text}</Text>
</View>
</View>
);
};
const ChildB = ({ index, top, isBeingDragged, setPosition, setIndex, setY, text }) => {
return (
<View
style={{
top,
width: '100%',
position: top == undefined ? 'relative' : 'absolute',
zIndex: isBeingDragged ? 1 : 0,
}}
onLayout={(evt) => setPosition(index, evt.nativeEvent.layout.y)}
key={index}>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: 'green',
padding: 10,
borderBottomColor: 'black',
borderBottomWidth: 1,
height: CHILD_B_HEIGHT,
}}>
<DragHandle index={index} setIndex={setIndex} setY={setY} text={text}>
<View
style={{
backgroundColor: 'lawngreen',
width: '100%',
height: '100%',
}}
/>
</DragHandle>
<Text>Child B: {text}</Text>
</View>
</View>
);
};
export default Parent;
Note: ChildA and ChildB components can also be reduced to single component
Here working example
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With