Introduction to click events in React Native
React Native provides 3 main primitives to handle click events.
- TouchableHighlight
- TouchableOpacity
- TouchableWithoutFeedback
Using any of these is a pretty standard affair. Just wrap our component that needs to respond to click events.
// imports...
<TouchableHighlight onPress={() => console.log('Clicked')}>
<Text>Click me</Text>
</TouchableHighlight>
Problem with Touchables
Though they work well for most of the use cases, there are some tricky situations which they cannot handle. Consider the case of handling a single click, double click and a long press event, all on the same element.
PanResponder to the rescue
PanResponder provides a predicatable wrapper to the lower level Gesture Responder System API. It provides much granular control over the touch events and also gives access to useful meta info like touch start position, touch end position, velocity of the gesture etc.
Let’s learn how to make a View
component respond to touch events using PanResponder.
import { View, PanResponder, Text } from 'react-native';
const MyComponent = () => {
const responder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderStart: (event) => {
console.log('Touch has started !');
},
onPanResponderRelease: (event, gestureState) => {
console.log('Touch has ended !');
},
onPanResponderTerminate: () => {},
});
return (
<View {...responder.panHandlers}>
<Text>Click Me </Text>
</View>
);
};
onStartShouldSetPanResponder
should return true
to allow the view to become the responder at the
start of a touch event.
onMoveShouldSetPanResponder
should return true
to allow the view to become the responder at the
start of a drag event.
onPanResponderStart
callback is fired when the PanResponder
registers touch events.
onPanResponderRelease
callback is fired when the touch has been released.
onPanResponderTerminate
callback is fired when the responder has been taken from the View. This
can happen when other views makes a call to onPanResponderTerminationRequest
or it can be taken by
the OS without asking (happens with control center/ notification center on iOS).
To make the double click work, we need to use a counter and set a maximum time duration between the
click so as to treat it as a double click. 400ms of delay between the clicks is a good place to
start. We’ll use the handleTap
to determine the type of click event based on the timer.
const MyComponent = () => {
const [isTerminated, setTerminated] = useState(false);
const [touchStartTime, setTouchStartTime] = useState(0);
const [lastTap, setLastTap] = useState(0);
const DOUBLE_PRESS_DELAY = 400;
const handleTap = (event, gestureState) => {
const timeNow = Date.now();
if (lastTap && timeNow - lastTap < DOUBLE_PRESS_DELAY) {
console.log('Handle double press');
} else {
setLastTap(timeNow);
const timeout = setTimeout(() => {
setLastTap(0);
console.log('Handle single press');
}, DOUBLE_PRESS_DELAY);
}
};
const responder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderStart: () => {
const timeout = setTimeout(() => {
if (!isTerminated) {
setTouchStartTime(Date.now());
}
});
},
onPanResponderRelease: (event, gestureState) => {
handleTap(event, gestureState);
},
onPanResponderTerminate: () => {
setTerminated(true);
},
});
return (
<View {...responder.panHandlers}>
<Text>Click Me </Text>
</View>
);
};
And now to make the long press work we need another counter with a delay of 700ms. We’ll first
check if it is a long press before checking it was a single press or a double press. We’ll use
handlePressOut
to determine the type of click and deligate the action for it.
const MyComponent = () => {
const [isTerminated, setTerminated] = useState(false);
const [touchStartTime, setTouchStartTime] = useState(0);
const [lastTap, setLastTap] = useState(0);
const [longPressTimer, setLongPressTimer] = useState(0);
const [singlePressTimer, setSinglePressTimer] = useState(0);
const DOUBLE_PRESS_DELAY = 400;
const LONG_PRESS_DELAY = 700;
const cancelLongPressTimer = () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
setLongPressTimer(0);
}
};
const cancelSinglePressTimer = () => {
if (singlePressTimer) {
clearTimeout(singlePressTimer);
setSinglePressTimer(0);
}
};
const handleTap = (event, gestureState) => {
cancelSinglePressTimer();
const timeNow = Date.now();
if (lastTap && timeNow - lastTap < DOUBLE_PRESS_DELAY) {
console.log('Handle double press');
} else {
setLastTap(timeNow);
const timeout = setTimeout(() => {
setLastTap(0);
console.log('Handle single press');
}, DOUBLE_PRESS_DELAY);
setSinglePressTimer(timeout);
}
};
const handlePressOut = (event, gestureState) => {
const elapsedTime = Date.now() - touchStartTime;
if (elapsedTime > LONG_PRESS_DELAY) {
console.log('Handle long press');
} else {
handleTap(event, gestureState); // handles the single or double click
}
setTouchStartTime(0);
};
const responder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderStart: () => {
cancelLongPressTimer();
const timeout = setTimeout(() => {
if (!isTerminated) {
setTouchStartTime(Date.now());
}
});
setLongPressTimer(timeout);
},
onPanResponderRelease: (event, gestureState) => {
handlePressOut(event, gestureState);
},
onPanResponderTerminate: () => {
setTerminated(true);
},
});
return (
<View {...responder.panHandlers}>
<Text>Click Me </Text>
</View>
);
};
Conclusion
I’ve made react-native-gifted-touch which
exactly does this so you can handle multiple clicks on the same element effortlessly. The default time
delays in the library can be configured using props
to better suit your requirements. Feel free to
check it out.