100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
React Native
50 minintermediate

Handling Touch, Gestures and Press States

A button that only responds to a clean tap and ignores everything else feels wrong the instant a real thumb touches it, because a real touch is never a single clean event — it's a sequence: the finger lands, it may move slightly before lifting, it may leave the button's bounds entirely before returning, or it may get interrupted by an incoming call's system alert mid-press. An app that models touch as "onPress fires or it doesn't" is quietly ignoring the entire sequence a real interaction actually goes through, and it shows up as buttons that fire when they shouldn't, or stay visually "stuck" pressed after a finger has already left.

`Pressable` exists specifically to expose that full sequence — press in, press out, long press, and the specific question of whether the finger is still inside the component's bounds when it lifts — rather than collapsing it down to a single boolean outcome. This exercise builds a press-state state machine using `Pressable`'s actual callback sequence, so the difference between "looks pressed" and "is actually still within the region that should trigger the action" becomes a concrete, testable piece of logic rather than an assumption.

Analogy🏏Cricket
🏏 Think of it like cricket: An umpire judging whether a fielder completed a legal catch doesn't just check "did the ball touch the fielder's hands" as a single yes-or-no fact — a full catch is a sequence that has to hold together from first contact through to full control, and a ball that touches the hands, gets fumbled, brushes the ground, and is only then grabbed cleanly is not a catch, even though "the ball touched the fielder's hands" was technically true at some point in that sequence. An umpire who only checked the single fact "did the hands touch the ball" and ignored the rest of the sequence would wrongly give batters out on juggled, grounded catches constantly, which is exactly why the actual law requires the fielder to maintain control throughout, not just make initial contact. Just as a legal catch is judged by the full sequence from contact to control, not a single touch event, a correct button press has to be judged by the full sequence from finger-down to finger-up-while-still-in-bounds, not a single onPress-style event collapsing everything into one boolean. Just as a fumbled-then-grabbed ball is genuinely different from a clean catch even though both involve "the ball touched the hands" at some point, a finger that lands on a button and then drags off before lifting is genuinely different from a real tap, even though both involve "the finger touched the button" at some point. The insight is that some outcomes are only correctly judged by their full sequence, and collapsing that sequence into a single checkpoint produces exactly the kind of wrong call that a proper, sequence-aware model exists to prevent.

Problem Statement

You're building a custom "swipe-to-confirm" style button used for a high-stakes action — think a payment-confirmation button — where accidentally triggering it from a careless, glancing touch would be a genuinely bad outcome, so the button needs to track press state carefully rather than relying on a naive tap handler. The component needs to distinguish four distinct states as the user interacts with it: idle (no touch), pressed-in-bounds (finger down, currently inside the button), pressed-out-of-bounds (finger down, but has moved outside the button's region without lifting), and the terminal outcome of a lift — which should only count as a confirmed action if the finger was still in-bounds at the moment it lifted.

Your task is to implement a `PressStateMachine` class that models this sequence using the same event vocabulary `Pressable` actually exposes — `onPressIn`, a bounds-check-equivalent event representing the finger moving in or out of the region, and `onPressOut` — and correctly computes whether a given full sequence of events should count as a confirmed press. This mirrors, in pure logic that's easy to test, exactly the reasoning a real `Pressable`-based component has to get right using its real callback props.

Analogy🏏Cricket
🏏 Think of it like cricket: A run-out decision hinges on a precise sequence of positions and timings — where the batter's bat or body was, relative to the crease, at the exact moment the bails came off — and a third umpire reviewing the replay has to reconstruct that full sequence from the available footage, not just check a single frame in isolation, because a batter who was out of the crease a moment earlier but has since grounded the bat safely before the bails were dislodged is not out, even though an earlier single frame alone might have suggested otherwise. Reconstructing the sequence correctly, frame by frame, is precisely what separates a correct decision from a wrong one, and it's exactly the kind of judgment call that has to track state over time rather than checking one static fact. Just as a run-out decision requires reconstructing a sequence of positions over time rather than checking one frame, this exercise's `PressStateMachine` requires reconstructing a sequence of press-in, bounds-change, and press-out events over time rather than checking one static touch fact. Just as the batter's final position at the exact moment the bails come off is what actually decides the outcome, the finger's in-bounds or out-of-bounds status at the exact moment of the press-out event is what actually decides whether this exercise's button should confirm. The insight is that some decisions are only correctly made by tracking state through an entire sequence of events, and the final state at the decisive moment — not any earlier moment in that sequence — is what the correct judgment actually depends on.

Requirements

Implement a `PressStateMachine` class with three methods matching `Pressable`'s real callback shape: `pressIn()` (called when the finger first lands on the component, corresponding to `onPressIn`), `boundsChange(inBounds: boolean)` (called whenever the finger crosses into or out of the component's region while still down, corresponding to the bounds-tracking a real gesture responder performs), and `pressOut()` (called when the finger lifts, corresponding to `onPressOut`). The class must expose a `currentState` getter returning one of `"idle"`, `"pressed-in-bounds"`, or `"pressed-out-of-bounds"`, correctly reflecting the most recent sequence of calls.

`pressOut()` must return a boolean: `true` if the state immediately before the press-out was `"pressed-in-bounds"` (a confirmed action), `false` otherwise, and calling `pressOut()` must also reset `currentState` back to `"idle"`, ready for the next interaction. A sequence of `pressIn()` → `boundsChange(false)` → `boundsChange(true)` → `pressOut()` (the finger drags out and then drags back in before lifting) must correctly return `true`, because the finger was back in-bounds at the moment it actually lifted — the earlier excursion out of bounds does not, by itself, disqualify the interaction.

Analogy🏏Cricket
🏏 Think of it like cricket: A DRS review for a stumping doesn't disqualify a batter just because part of their foot was briefly airborne and outside the crease line at some point during a shot — what actually matters is whether any part of the bat or the batter's body was grounded inside the crease at the precise moment the bails were removed, and a batter whose foot was briefly out but was safely grounded back inside the crease by the decisive instant is not out, regardless of that earlier, now-irrelevant excursion. A commentator who insists "the foot was out of the crease at some point, so he should be out" is applying the wrong rule — the decisive moment, not any earlier moment in the sequence, is what the law actually cares about. Just as a stumping review cares about the batter's position at the specific decisive instant, not any earlier momentary excursion, this exercise's `pressOut()` must care about whether the finger is in-bounds at the specific instant it lifts, not whether it was ever out of bounds at some earlier point in the same press. Just as a batter safely grounded back inside the crease before the bails come off is not out despite the earlier excursion, a finger that drags out and then drags back in before lifting must still confirm the action, because the earlier excursion is exactly as irrelevant to the final outcome as the batter's earlier airborne foot. The insight is that correctly implementing a rule governed by a decisive final moment means the logic must track the most recent state precisely, and must resist the intuitive but wrong urge to disqualify based on something true only earlier in the sequence.

Starter Code

typescript
// press-state-machine.starter.ts — complete this state machine.
// It currently compiles and runs, but always confirms every press
// regardless of bounds — that's the bug the TODOs below need to fix.

type PressState = "idle" | "pressed-in-bounds" | "pressed-out-of-bounds";

class PressStateMachine {
  private state: PressState = "idle";

  get currentState(): PressState {
    return this.state;
  }

  pressIn(): void {
    // TODO 1: a finger landing should move the state to "pressed-in-bounds".
    this.state = "pressed-in-bounds";
  }

  boundsChange(inBounds: boolean): void {
    // TODO 2: while a finger is down, crossing the component's bounds
    // should move the state between "pressed-in-bounds" and
    // "pressed-out-of-bounds". Currently this does nothing at all.
  }

  pressOut(): boolean {
    // TODO 3: should only return true if the state was
    // "pressed-in-bounds" immediately before this call, and must reset
    // state back to "idle" regardless of the outcome. Currently this
    // always returns true without checking the actual state at all.
    this.state = "idle";
    return true;
  }
}

const button = new PressStateMachine();
button.pressIn();
button.boundsChange(false); // finger drags outside the button
const confirmedWhileOutOfBounds = button.pressOut(); // finger lifts while still outside

console.log("Starter result (should be false — finger lifted out of bounds):", confirmedWhileOutOfBounds);

Running the starter as-is prints `true`, which is exactly the bug this exercise is built around: the finger dragged outside the button and lifted while still outside, which should not count as a confirmed press for a payment-confirmation button, but the placeholder `pressOut()` returns `true` unconditionally without checking `boundsChange` at all, and `boundsChange` itself doesn't update state yet. The three TODOs mark exactly where the real state-tracking logic needs to go.

Hints & Common Pitfalls

`boundsChange(inBounds)` should only actually change state while the finger is down — it should never move `currentState` out of `"idle"`, because a `boundsChange` call arriving when nothing is actually pressed (which shouldn't happen in a correctly wired real component, but is worth guarding against in the state machine itself) shouldn't spuriously start a press. Guard the state transition on the current state already being one of the two "pressed" states before applying a bounds change.

A subtler pitfall is implementing `pressOut()` by checking the `inBounds` argument that was most recently passed to `boundsChange`, stored as a separate boolean flag, rather than checking `this.state` directly — this works for the simple cases but is redundant with `currentState` and creates two sources of truth that can drift out of sync if a future change updates one and not the other; the cleaner, more maintainable implementation checks `this.state === "pressed-in-bounds"` directly, since that's already the single source of truth `boundsChange` should be maintaining.

Analogy🏏Cricket
🏏 Think of it like cricket: A scorer tracking whether a batter is currently "in" or "retired" doesn't maintain two separate, independently updated records of the same fact — one flag for "is batting" and a completely separate flag for "has been dismissed" that has to be manually kept in sync with the first — because two independent records of what should be one single fact will eventually drift apart the moment one gets updated and the other doesn't, producing a scorecard that contradicts itself. A well-run scoring system maintains exactly one authoritative record of a batter's status and derives every other fact — who's currently at the crease, who's next in — from that single source, rather than duplicating the same underlying fact across multiple places that all have to be kept manually consistent. Just as a well-run scoring system maintains one authoritative status record rather than duplicating the same fact across multiple flags, this exercise's `pressOut()` should check `this.state` directly rather than maintaining a separate, redundant `inBounds` flag that has to be kept in sync with it. Just as two independent records of the same fact eventually drift apart and produce a contradictory scorecard, two independent representations of press state eventually drift apart and produce a state machine that gives a different answer depending on which representation happens to be checked. The insight is that a single, authoritative source of truth for a piece of state is not just tidier — it's what prevents an entire class of drift bugs that duplicated state is structurally prone to.

A common mistake is implementing `boundsChange(false)` by immediately setting state all the way to `"idle"` rather than to `"pressed-out-of-bounds"`. The symptom is that a finger dragging out of the button and then dragging back in before lifting incorrectly fails to confirm, because once state hit `"idle"`, the subsequent `boundsChange(true)` had no "pressed" state to return to and the machine effectively treated the interaction as already over. The root cause is conflating "outside the button's bounds" with "no longer pressed at all" — they are genuinely different states, since the finger is still down, just not currently over the button. The fix is a real third state, `"pressed-out-of-bounds"`, distinct from both `"idle"` and `"pressed-in-bounds"`, that a subsequent `boundsChange(true)` can transition back out of, exactly as this exercise's Requirements specify.

Reference Solution

typescript
// press-state-machine.solution.ts — a complete, correctly ordered
// implementation using a single source of truth for the press state.

type PressState = "idle" | "pressed-in-bounds" | "pressed-out-of-bounds";

class PressStateMachine {
  private state: PressState = "idle";

  get currentState(): PressState {
    return this.state;
  }

  pressIn(): void {
    this.state = "pressed-in-bounds";
  }

  boundsChange(inBounds: boolean): void {
    // Only meaningful while a finger is actually down — guards against a
    // stray bounds event when nothing is pressed at all.
    if (this.state === "idle") return;
    this.state = inBounds ? "pressed-in-bounds" : "pressed-out-of-bounds";
  }

  pressOut(): boolean {
    // Single source of truth: check the state itself, not a duplicated flag.
    const confirmed = this.state === "pressed-in-bounds";
    this.state = "idle";
    return confirmed;
  }
}

function runSequence(label: string, actions: (m: PressStateMachine) => boolean): void {
  const machine = new PressStateMachine();
  const result = actions(machine);
  console.log(`${label}: confirmed=${result}`);
}

runSequence("Clean tap, no drag", (m) => {
  m.pressIn();
  return m.pressOut();
});

runSequence("Drag out, lift while out (should NOT confirm)", (m) => {
  m.pressIn();
  m.boundsChange(false);
  return m.pressOut();
});

runSequence("Drag out then back in, lift while back in (SHOULD confirm)", (m) => {
  m.pressIn();
  m.boundsChange(false);
  m.boundsChange(true);
  return m.pressOut();
});

The three logged sequences exercise exactly the cases the Requirements section specified: a clean tap confirms, a drag-out-and-lift-while-out does not confirm, and a drag-out-then-back-in-then-lift does confirm, because `pressOut()` checks the current state at the moment it's actually called, not any earlier state in the sequence. The `boundsChange` guard against `"idle"` state is a defensive measure that doesn't affect the three required scenarios but prevents a subtle bug if a bounds event ever arrives before a press-in event does, which a real gesture responder implementation should never allow but a state machine written defensively shouldn't assume.

Mapped onto a real `Pressable`, `pressIn()` corresponds to the `onPressIn` callback, `pressOut()`'s confirmed/not-confirmed result corresponds to deciding whether to actually fire the app's real action inside `onPressOut`, and `boundsChange` corresponds to the bounds tracking `Pressable` performs internally and exposes indirectly through its press-state styling callback — a production component wouldn't need to hand-roll this exact state machine since `Pressable` already handles the equivalent logic, but understanding the sequence it's handling is what makes its behavior predictable rather than mysterious.

Analogy🏏Cricket
🏏 Think of it like cricket: A DRS system doesn't ask a human ball-tracking operator to redo the trajectory physics from first principles for every single review — the underlying engine already computes it correctly every time, and what actually needs mastering is understanding the engine's inputs and outputs well enough to interpret and trust the result, not re-deriving the physics from scratch. A commentator who deeply understands how the tracking system actually reasons about a delivery can explain a marginal decision confidently and accurately to viewers, while one who doesn't is reduced to vague, unconvincing guesses about why the system decided what it decided. Just as understanding DRS's actual reasoning process is what makes a commentator's explanation trustworthy, understanding `Pressable`'s actual press-in, bounds, and press-out sequence is what makes a developer's use of it deliberate rather than a copy-pasted incantation they don't really understand. Just as no commentator needs to personally re-derive the underlying trajectory physics to explain a decision well, no application developer needs to hand-roll this exact state machine in production, since `Pressable` already implements the equivalent logic — but genuinely understanding what it's doing internally is what turns "it usually works" into "I know exactly why this specific edge case behaves the way it does." The insight is that using a well-built abstraction well doesn't require reimplementing it — it requires understanding it deeply enough to predict its behavior with confidence.

Testing Your Solution

typescript
// press-state-machine.test.ts — a small table-driven test suite covering
// the required cases plus a couple of deliberately awkward edge cases.

type PressState = "idle" | "pressed-in-bounds" | "pressed-out-of-bounds";

class PressStateMachine {
  private state: PressState = "idle";
  get currentState(): PressState {
    return this.state;
  }
  pressIn(): void {
    this.state = "pressed-in-bounds";
  }
  boundsChange(inBounds: boolean): void {
    if (this.state === "idle") return;
    this.state = inBounds ? "pressed-in-bounds" : "pressed-out-of-bounds";
  }
  pressOut(): boolean {
    const confirmed = this.state === "pressed-in-bounds";
    this.state = "idle";
    return confirmed;
  }
}

interface Case {
  label: string;
  run: (m: PressStateMachine) => boolean;
  expected: boolean;
}

const cases: Case[] = [
  { label: "clean tap", run: (m) => { m.pressIn(); return m.pressOut(); }, expected: true },
  { label: "drag out, lift out", run: (m) => { m.pressIn(); m.boundsChange(false); return m.pressOut(); }, expected: false },
  { label: "drag out then back in, lift in", run: (m) => { m.pressIn(); m.boundsChange(false); m.boundsChange(true); return m.pressOut(); }, expected: true },
  { label: "drag out, back in, out again, lift out", run: (m) => { m.pressIn(); m.boundsChange(false); m.boundsChange(true); m.boundsChange(false); return m.pressOut(); }, expected: false },
  { label: "redundant boundsChange(true) while already in bounds", run: (m) => { m.pressIn(); m.boundsChange(true); m.boundsChange(true); return m.pressOut(); }, expected: true },
  { label: "boundsChange arriving with no active press is ignored", run: (m) => { m.boundsChange(false); m.pressIn(); return m.pressOut(); }, expected: true },
];

let failures = 0;
for (const c of cases) {
  const machine = new PressStateMachine();
  const actual = c.run(machine);
  const stateAfter = machine.currentState;
  const pass = actual === c.expected && stateAfter === "idle";
  if (!pass) failures += 1;
  console.log(`[${pass ? "PASS" : "FAIL"}] ${c.label}: confirmed=${actual} (expected ${c.expected}), stateAfter=${stateAfter}`);
}

console.log(`\n${cases.length - failures}/${cases.length} cases passed.`);
if (failures > 0) process.exit(1);

The fifth case — a redundant `boundsChange(true)` while already in bounds — checks that the state machine is idempotent for a no-op bounds update, which a real gesture responder can plausibly fire more than once in a row as it continuously tracks finger position; the state machine should not treat a repeated identical bounds update as anything unusual. The sixth case checks the defensive guard from the Reference Solution directly: a stray `boundsChange` call before any `pressIn()` should be silently ignored rather than corrupting a subsequent, legitimate press sequence.

Every case in this suite also checks `stateAfter === "idle"`, not just the returned boolean, because a `pressOut()` that returns the correct confirmed value but fails to reset state back to `"idle"` would leave the machine in a corrupted condition for the very next interaction — a bug that a test checking only the return value would miss entirely, which is exactly the kind of gap a thorough test suite is supposed to close.

Analogy🏏Cricket
🏏 Think of it like cricket: A ground's equipment check before a match doesn't just confirm that the stumps are currently standing — it confirms the stumps will reset correctly and stand ready after being knocked down and replaced during play, because a stump that returns the wrong reading on a dismissal check but then fails to reset cleanly for the next delivery would cause a completely different problem on the very next ball, one a check that only looked at the current instant would never catch. A groundstaff crew that only ever verifies "is it currently correct" and never verifies "does it correctly reset for what comes next" is missing exactly the class of bug that only shows up on the following delivery, not the current one. Just as a proper equipment check verifies both the current reading and the correct reset for what comes next, this test suite verifies both the returned confirmed value and that `currentState` correctly resets to `"idle"` for the next interaction. Just as a bug that only manifests on the following delivery would be invisible to a check that only looks at the current ball, a state-reset bug that only manifests on the next press would be invisible to a test that only checks the current call's return value. The insight is that a component managing state over multiple interactions has to be tested for correctness across those interactions, not just correctness of a single isolated call, because some bugs only ever surface in the transition between one interaction and the next.
Lesson 6 of 35
0% complete