In React Native, how would you make a Pressable calculator delete key remove one digit per tap and keep deleting while held, without the repeat getting stuck?
answer
- map each phase to one callback
- start the repeat on the long press
- one stop point for every ending
- tap is swallowed after a long press
- clear on unmount, read latest callback
basics
~20 sDelete one digit in onPress, start an interval in onLongPress, and clear it in onPressOut plus an unmount cleanup. onPressOut fires on release, slide-off and parent takeover, while onPress is skipped after a long press when onLongPress is set.
solid answer
~40 sMap each phase to the callback that matches it. `onPress` deletes one digit, because it fires only for a completed tap. `onLongPress` starts a `setInterval` that keeps deleting; it fires once, while the finger is still down, after `delayLongPress`. The repeat must stop in `onPressOut`, since every end of an active press goes through it: release inside, sliding past the press area, or a scrolling parent taking the touch. Do not stop it in `onPress`: with an `onLongPress` handler present, Pressability skips `onPress` after a long press, so that interval would never be cleared. Also clear the timer on unmount, clear any old timer before starting a new one, and call the latest delete function through a ref or a functional state update so each tick does not delete from a stale closure.
code
tsx · 41 linesimport {useCallback, useEffect, useRef} from 'react';
import {Pressable, Text} from 'react-native';
type DeleteKeyProps = {deleteLast: () => void};
const REPEAT_MS = 80;
export function DeleteKey({deleteLast}: DeleteKeyProps) {
const timer = useRef<ReturnType<typeof setInterval> | null>(null);
const latestDelete = useRef(deleteLast);
useEffect(() => {
latestDelete.current = deleteLast;
}, [deleteLast]);
const stopRepeat = useCallback(() => {
if (timer.current != null) {
clearInterval(timer.current);
timer.current = null;
}
}, []);
const startRepeat = useCallback(() => {
stopRepeat();
latestDelete.current();
timer.current = setInterval(() => latestDelete.current(), REPEAT_MS);
}, [stopRepeat]);
useEffect(() => stopRepeat, [stopRepeat]);
return (
<Pressable
onPress={deleteLast}
onLongPress={startRepeat}
onPressOut={stopRepeat}
delayLongPress={400}
style={({pressed}) => ({padding: 24, opacity: pressed ? 0.6 : 1})}>
<Text>DEL</Text>
</Pressable>
);
}go deeper
Recall which callback fires once on a hold (onLongPress) and which fires whenever the finger stops pressing (onPressOut), and that a timer you start you must also stop.
Explain why onPress is the wrong stop point: it is skipped after a long press when onLongPress is set. Show the interval started on the long press and cleared on onPressOut.
Cover every ending of the press, including slide-off and a parent scroll taking over, plus unmount cleanup, stale closures, double starts and the 10-point drift that cancels a long press.
Decide whether hold-to-repeat belongs in a shared key primitive with tested timing constants, so every repeating control in the app stops on the same guarantees.
## The requirement A calculator's delete key usually has two behaviours: a **tap removes one digit**, and **holding keeps deleting** until the finger lifts, the way a phone keyboard's backspace does. With React Native's `Pressable` this is a few lines of code, and it is also a reliable interview probe. A candidate who has only read the prop list tends to write a repeat that never stops, or one that deletes an extra digit on release. ## The design Map each part of the gesture to the callback whose semantics match it: 1. **`onPress`**: delete one digit. It fires only for a completed tap. 2. **`onLongPress`**: start the repeat. It fires once, while the finger is still down, after `delayLongPress` (500 ms by default; a keyboard-like feel often uses a bit less). 3. **`onPressOut`**: stop the repeat. This is the load-bearing choice. 4. **Unmount cleanup**: stop the repeat if the key disappears mid-hold, for example when the screen changes. Because an `onLongPress` handler is passed, Pressability suppresses `onPress` for a gesture whose long press already fired. The release after a hold therefore does not delete one more digit. ## Why onPressOut is the only safe stop Every way an active press can end passes through `onPressOut`: - **Release inside the key**: the press deactivates and `onPressOut` fires. - **Sliding outside the press area** (the view plus `hitSlop` plus `pressRetentionOffset`): `onPressOut` fires as the finger leaves. - **A parent taking the touch**: `Pressable` is `cancelable` by default, so a scrolling ancestor can terminate the press, and that termination fires `onPressOut`. Compare the tempting alternatives: | Stop the repeat in… | What goes wrong | |---|---| | `onPress` | never called after a long press when `onLongPress` is set, so the repeat never stops | | the next `onPressIn` | a hold that ends in a slide-off keeps deleting until the user touches the key again | | a `setTimeout` guess | stops too early for a long hold or too late for a short one | | nothing, relying on unmount | the display empties while the user watches | ## Traps inside the handlers - **Stale closures.** An interval started in `onLongPress` captures the delete function from that render. If deletion reads display state from a closure, every tick deletes from the same stale value. Keep the latest callback in a ref, or make the delete a functional state update. - **Double starts.** Clear any existing timer before starting a new one, so a second long press can never leave an orphaned interval. - **Thumb drift.** Pressability cancels the long-press timer if the finger moves more than 10 points from where the press activated. The repeat then never starts, and the release fires `onPress`: one digit deleted, which is a reasonable outcome but surprises testers. - **Re-entry.** If the user slides off and back on, `onPressIn` fires again but `onLongPress` does not; the repeat stays stopped. That is usually right: sliding off reads as "I changed my mind". - **Empty display.** Make the delete itself a no-op on an empty display, so a repeat that outlives the digits cannot underflow. ## The pressed style and onPressOut timing For a quick tap, Pressability holds `onPressOut` back so the key looks pressed for at least about 130 ms, and `onPress` may run first. That does not affect this design: a quick tap never started a repeat, so the delayed `onPressOut` finds no timer to clear. After a long hold, the minimum has long passed and `onPressOut` runs as soon as the finger lifts. ## When to reach for something else This pattern covers a hold-to-repeat key. It is not a gesture system. Continuous drags, velocity-based flings, or gestures that must run off the JavaScript thread belong to the gesture-library topics, and the responder negotiation that lets a parent steal a touch is its own subject. For a keypad, `Pressable` plus a timer and `onPressOut` as the single stop point is the whole answer. ## How to explain it in an interview State the mapping (tap on `onPress`, start on `onLongPress`, stop on `onPressOut`), justify the stop point with the three ways a press ends, then name one trap: `onPress` is skipped after a long press, so a stop placed there leaks.
- What does the user see if their thumb drifts slightly while holding this delete key?If the finger moves more than 10 points from where the press activated before `delayLongPress` elapses, Pressability cancels the long-press timer. The repeat never starts, and because the press is still active, lifting fires `onPress`, so exactly one digit is deleted.
- Should the repeat restart if the user slides off the key and back on while still holding?With `Pressable` it does not: sliding off fires `onPressOut`, which stops the repeat, and sliding back fires `onPressIn` but not `onLongPress` again. That usually matches intent, since sliding off signals a change of mind. Restarting would need your own logic in `onPressIn`.
saying these in an interview costs you the question
- Stop the repeat in onPress, because it always runs when the finger lifts.
- onLongPress keeps firing repeatedly for as long as the finger stays down.
- A parent ScrollView taking over the touch ends the press with onPress.
- Small finger movement inside the key never affects whether onLongPress fires.
- An interval started in a press handler is cleared automatically on unmount.