necolas/react-native-web · error · Error

Touchable cannot transition from `${curState}` to `${signal}

Error message

Touchable cannot transition from `${curState}` to `${signal}` for responder `${responderID}`

What it means

Companion to the unrecognized-signal throw: the transition lookup succeeded but maps to States.ERROR, meaning the state machine explicitly forbids moving from curState via this signal. Touchable throws to surface the illegal transition with the responder ID. Indicates a broken responder lifecycle or library bug.

Source

Thrown at packages/react-native-web/src/exports/Touchable/index.js:778

    const responderID = this.state.touchable.responderID;
    const curState = this.state.touchable.touchState;
    const nextState = Transitions[curState] && Transitions[curState][signal];
    if (!responderID && signal === Signals.RESPONDER_RELEASE) {
      return;
    }
    if (!nextState) {
      throw new Error(
        'Unrecognized signal `' +
          signal +
          '` or state `' +
          curState +
          '` for Touchable responder `' +
          responderID +
          '`'
      );
    }
    if (nextState === States.ERROR) {
      throw new Error(
        'Touchable cannot transition from `' +
          curState +
          '` to `' +
          signal +
          '` for responder `' +
          responderID +
          '`'
      );
    }
    if (curState !== nextState) {
      this._performSideEffectsForTransition(curState, nextState, signal, e);
      this.state.touchable.touchState = nextState;
    }
  },

  _cancelLongPressDelayTimeout: function () {
    this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
    this.longPressDelayTimeout = null;

View on GitHub (pinned to a9de220ba9)

Solutions

  1. Upgrade react-native-web to pick up state-machine race fixes.
  2. Reproduce and file an issue with the reported curState → signal pair.
  3. Avoid calling Touchable responder internals directly from app code.
  4. Simplify nesting of touchable/gesture components around the failing responder.

Example fix

// before (app code forcing internal state)
ref._receiveSignal(Signals.ENTER_PRESS_PRESSIN, id);
// after
// rely on native pointer events; upgrade react-native-web instead
Defensive patterns

Strategy: try-catch

Validate before calling

// Only legal transitions should ever be dispatched:
const nextState = Transitions[curState] && Transitions[curState][signal];
if (nextState === States.ERROR) throw new Error(`Illegal transition ${curState} -> ${signal}`);

Type guard

const isLegalTransition = (state, signal) => Transitions[state]?.[signal] !== States.ERROR;

Try / catch

try { pressHandlers(); } catch (e) { if (/cannot transition from/.test(e.message)) reportStateRace(e.message); else throw e; }

Prevention

When it happens

Trigger: A signal arrives that the transition table deliberately maps to ERROR for the current state — e.g. a second RESPONDER_GRANT while already active, or a termination/enter signal in a terminal state.

Common situations: Fuzzy/overlapping pointer events hitting the same Touchable twice; touch cancel sequences racing with release; custom long-press/press-in logic forcing state changes; known library race conditions fixed in newer versions.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of necolas/react-native-web@a9de220ba9 (2026-09-01). Data as JSON: /api/errors/bcf918cc7d641473. Report an issue: GitHub.