{"record":{"id":"87d319a1cb2d1b3c","repo":"necolas/react-native-web","slug":"cannot-record-touch-move-without-a-touch-start-to","errorCode":null,"errorMessage":"Cannot record touch move without a touch start.\nTouch Move: ${printTouch(touch)}\nTouch Bank: ${printTouchBank(touchHistory)}","messagePattern":"Cannot record touch move without a touch start\\.\nTouch Move: (.+?)\nTouch Bank: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/react-native-web/src/modules/useResponderEvents/ResponderTouchHistoryStore.js","lineNumber":120,"sourceCode":"  } else {\n    touchHistory.touchBank[identifier] = createTouchRecord(touch);\n  }\n  touchHistory.mostRecentTimeStamp = timestampForTouch(touch);\n}\n\nfunction recordTouchMove(touch: Touch, touchHistory): void {\n  const touchRecord = touchHistory.touchBank[getTouchIdentifier(touch)];\n  if (touchRecord) {\n    touchRecord.touchActive = true;\n    touchRecord.previousPageX = touchRecord.currentPageX;\n    touchRecord.previousPageY = touchRecord.currentPageY;\n    touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;\n    touchRecord.currentPageX = touch.pageX;\n    touchRecord.currentPageY = touch.pageY;\n    touchRecord.currentTimeStamp = timestampForTouch(touch);\n    touchHistory.mostRecentTimeStamp = timestampForTouch(touch);\n  } else {\n    console.warn(\n      'Cannot record touch move without a touch start.\\n',\n      `Touch Move: ${printTouch(touch)}\\n`,\n      `Touch Bank: ${printTouchBank(touchHistory)}`\n    );\n  }\n}\n\nfunction recordTouchEnd(touch: Touch, touchHistory): void {\n  const touchRecord = touchHistory.touchBank[getTouchIdentifier(touch)];\n  if (touchRecord) {\n    touchRecord.touchActive = false;\n    touchRecord.previousPageX = touchRecord.currentPageX;\n    touchRecord.previousPageY = touchRecord.currentPageY;\n    touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;\n    touchRecord.currentPageX = touch.pageX;\n    touchRecord.currentPageY = touch.pageY;\n    touchRecord.currentTimeStamp = timestampForTouch(touch);\n    touchHistory.mostRecentTimeStamp = timestampForTouch(touch);","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/necolas/react-native-web/blob/a9de220ba9e65bdea540fb5322ffb1da2b0bf442/packages/react-native-web/src/modules/useResponderEvents/ResponderTouchHistoryStore.js#L102-L138","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before (test/synthetic event)\ndispatchTouchEvent('touchmove', { identifier: 1, pageX: 50, pageY: 60 });\n// after\ndispatchTouchEvent('touchstart', { identifier: 1, pageX: 10, pageY: 20 });\ndispatchTouchEvent('touchmove', { identifier: 1, pageX: 50, pageY: 60 });","handlingStrategy":"validation","validationCode":"function hasTouchStart(touchHistory, identifier) {\n  return touchHistory != null && touchHistory.touchBank != null && touchHistory.touchBank[identifier] != null;\n}\nif (!hasTouchStart(touchHistory, touch.identifier)) return; // skip move\nrecordTouchMove(touch, touchHistory);","typeGuard":"function isTrackedTouch(touchHistory, touch) {\n  return Boolean(touch && typeof touch.identifier === 'number' && touchHistory?.touchBank?.[touch.identifier]);\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["touch","responder-events","event-ordering"],"backgroundTag":"touchmove-without-touchstart","analyzedSha":"a9de220ba9e65bdea540fb5322ffb1da2b0bf442","analyzedAt":"2026-09-01T10:01:09.883Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}