Skip to content

fix(x): product tour stuck on first step in dev (StrictMode double-mount) - #842

Open
YarnMeister wants to merge 1 commit into
rowboatlabs:mainfrom
YarnMeister:fix/tour-strictmode
Open

fix(x): product tour stuck on first step in dev (StrictMode double-mount)#842
YarnMeister wants to merge 1 commit into
rowboatlabs:mainfrom
YarnMeister:fix/tour-strictmode

Conversation

@YarnMeister

Copy link
Copy Markdown

Problem

Running npm run dev and clicking Take a tour dims the screen and shows the water/spotlight, but the boat never rows anywhere and the speech bubble (with the Next button) never appears — the tour dead-ends on step 1 of 12. Packaged builds are unaffected.

Root cause

The renderer mounts the app in <StrictMode> (main.tsx), and React 19 double-invokes effects in dev (mount → cleanup → mount). The tour's step effect doesn't survive that round trip:

  1. First run: entering = truesettle() sets enteredStepRef.current = stepIndex before startTravel() begins the rAF travel loop.
  2. StrictMode cleanup: cancelled = true; cancelTravel() kills the loop.
  3. Second run: enteredStepRef.current already matches, so entering = false → the effect takes the resize branch ("jump, keep the bubble up"), which moves the mascot to its destination and returns — setArrived(true) never fires.

Since the bubble only renders when arrived is true, the tour is permanently stuck. Traced via CDP: mascot frozen at its computed destination transform, wake strokeDashoffset never written, zero exceptions.

Fix

Roll the entered marker back in the effect cleanups when the run that set it was torn down before arrival:

const unmarkEntered = () => {
  if (entering && enteredStepRef.current === stepIndex) enteredStepRef.current = -1
}

Resize-triggered re-runs execute with entering === false, so the existing jump-without-re-rowing behavior is preserved.

Verification

  • Before: reproduced 100% in dev — tour mounts, no bubble, stuck on step 1 indefinitely.
  • After (hard reload, dev): boat rows in from off-screen drawing its wake, bubble appears, and all 12 stops advance cleanly through Done, which tears the tour down fully.
  • npm run typecheck:renderer and npm run lint pass.

🤖 Generated with Claude Code

In dev, StrictMode's double-invoked effects (mount -> cleanup -> mount)
break the tour's step effect: settle() marks the step as entered via
enteredStepRef before startTravel() begins, the StrictMode cleanup then
cancels the travel rAF loop, and the second run sees the step already
entered so it takes the resize branch — jumping the mascot to its
destination without ever flipping `arrived`. The speech bubble (and its
Next button) only render once `arrived` is true, so the tour dims the
screen and dead-ends on step 1 with a motionless boat.

Fix: roll the entered marker back in the effect cleanup when the run it
belongs to started the entry but was torn down before arrival. Resize
re-runs enter with `entering === false`, so the existing jump-and-keep-
the-bubble behavior is unchanged.

Production builds are unaffected (StrictMode double-invocation is
dev-only), which is why packaged releases never showed this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant