necolas/react-native-web · warning

Cannot record touch move without a touch start. Touch Move:

Error message

Cannot record touch move without a touch start.
Touch Move: ${printTouch(touch)}
Touch Bank: ${printTouchBank(touchHistory)}

What it means

This warning is emitted by ResponderTouchHistoryStore when recordTouchMove receives a touch event whose identifier has no existing entry in the touch bank — i.e. a 'touchmove' arrives for a touch that never had a matching 'touchstart' recorded. The library tracks touches by identifier, so a move without a start means its history is inconsistent (the start was missed, filtered out, or the touch state was reset mid-gesture). It warns via console.warn rather than throwing, so gesture tracking for that touch is silently skipped.

Source

Thrown at packages/react-native-web/src/modules/useResponderEvents/ResponderTouchHistoryStore.js:120

  } else {
    touchHistory.touchBank[identifier] = createTouchRecord(touch);
  }
  touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
}

function recordTouchMove(touch: Touch, touchHistory): void {
  const touchRecord = touchHistory.touchBank[getTouchIdentifier(touch)];
  if (touchRecord) {
    touchRecord.touchActive = true;
    touchRecord.previousPageX = touchRecord.currentPageX;
    touchRecord.previousPageY = touchRecord.currentPageY;
    touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
    touchRecord.currentPageX = touch.pageX;
    touchRecord.currentPageY = touch.pageY;
    touchRecord.currentTimeStamp = timestampForTouch(touch);
    touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
  } else {
    console.warn(
      'Cannot record touch move without a touch start.\n',
      `Touch Move: ${printTouch(touch)}\n`,
      `Touch Bank: ${printTouchBank(touchHistory)}`
    );
  }
}

function recordTouchEnd(touch: Touch, touchHistory): void {
  const touchRecord = touchHistory.touchBank[getTouchIdentifier(touch)];
  if (touchRecord) {
    touchRecord.touchActive = false;
    touchRecord.previousPageX = touchRecord.currentPageX;
    touchRecord.previousPageY = touchRecord.currentPageY;
    touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
    touchRecord.currentPageX = touch.pageX;
    touchRecord.currentPageY = touch.pageY;
    touchRecord.currentTimeStamp = timestampForTouch(touch);
    touchHistory.mostRecentTimeStamp = timestampForTouch(touch);

View on GitHub (pinned to a9de220ba9)

Solutions

  1. Ensure every synthetic touch sequence includes a touchstart with the same identifier before any touchmove/touchend for that identifier
  2. Attach the useResponderEvents listeners (startResponderEvent) before gestures begin — e.g. don't mount content mid-drag or hydrate mid-touch
  3. If dispatching events programmatically (tests/scripts), build complete sequences: start, move..., end with consistent identifier and pageX/pageY/timestamp
  4. Check for duplicate listener setup or custom event interception that swallows touchstart before the store sees it

Example fix

// before (test/synthetic event)
dispatchTouchEvent('touchmove', { identifier: 1, pageX: 50, pageY: 60 });
// after
dispatchTouchEvent('touchstart', { identifier: 1, pageX: 10, pageY: 20 });
dispatchTouchEvent('touchmove', { identifier: 1, pageX: 50, pageY: 60 });
Defensive patterns

Strategy: validation

Validate before calling

function hasTouchStart(touchHistory, identifier) {
  return touchHistory != null && touchHistory.touchBank != null && touchHistory.touchBank[identifier] != null;
}
if (!hasTouchStart(touchHistory, touch.identifier)) return; // skip move
recordTouchMove(touch, touchHistory);

Type guard

function isTrackedTouch(touchHistory, touch) {
  return Boolean(touch && typeof touch.identifier === 'number' && touchHistory?.touchBank?.[touch.identifier]);
}

Prevention

When it happens

Trigger: A touchmove/touchchange event with a touch.identifier that is not present in touchHistory.touchBank; typically caused by synthetic dispatch of move events without a start, event listeners attached mid-gesture so the initial touchstart is missed, or custom event emulation (e.g. mouse-to-touch shims) sending move without start.

Common situations: Automated tests or E2E tools (puppeteer/selenium, react-native testing libraries) synthesizing touch sequences incompletely; browser extensions or embedded webviews intercepting the original touchstart; calling responder event handlers manually; dragging a finger/pointer started before JS hydrated and began listening.

Related errors


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