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
- Ensure every synthetic touch sequence includes a touchstart with the same identifier before any touchmove/touchend for that identifier
- Attach the useResponderEvents listeners (startResponderEvent) before gestures begin — e.g. don't mount content mid-drag or hydrate mid-touch
- If dispatching events programmatically (tests/scripts), build complete sequences: start, move..., end with consistent identifier and pageX/pageY/timestamp
- 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
- Always dispatch a complete touch lifecycle (start -> moves -> end/cancel) with consistent identifiers in tests and synthetic events
- Attach responder event listeners before gestures can begin; avoid mounting/hydrating mid-drag
- Handle touchcancel distinctly and never forward orphan end/move events after cancellation
- Reset touch history between test cases to avoid stale identifier state
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.