fix(mobile): prevent pushState → popstate infinite loop in MobileApp URL sync
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
E2E API Smoke Test / detect-changes (pull_request) Successful in 44s
gate-check-v3 / gate-check (pull_request) Successful in 25s
CI / Detect changes (pull_request) Successful in 50s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 46s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 36s
qa-review / approved (pull_request) Failing after 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 49s
security-review / approved (pull_request) Failing after 11s
sop-tier-check / tier-check (pull_request) Successful in 10s
Harness Replays / Harness Replays (pull_request) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 18s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7s
audit-force-merge / audit (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Failing after 6m33s
CI / Python Lint & Test (pull_request) Successful in 6m59s
CI / Canvas (Next.js) (pull_request) Successful in 7m32s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Failing after 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7m44s
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 13s
Lint curl status-code capture / Scan workflows for curl status-capture pollution (pull_request) Successful in 13s
Harness Replays / detect-changes (pull_request) Successful in 16s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
E2E API Smoke Test / detect-changes (pull_request) Successful in 44s
gate-check-v3 / gate-check (pull_request) Successful in 25s
CI / Detect changes (pull_request) Successful in 50s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 46s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 36s
qa-review / approved (pull_request) Failing after 17s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 49s
security-review / approved (pull_request) Failing after 11s
sop-tier-check / tier-check (pull_request) Successful in 10s
Harness Replays / Harness Replays (pull_request) Successful in 4s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 18s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 6s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 7s
audit-force-merge / audit (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Failing after 6m33s
CI / Python Lint & Test (pull_request) Successful in 6m59s
CI / Canvas (Next.js) (pull_request) Successful in 7m32s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Failing after 3s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7m44s
React error #185 (Maximum update depth exceeded) on mobile when navigating to ?m=chat&a=... was caused by a render loop in the URL sync effect. Root cause: when setRoute/setAgentId fires, the URL sync effect calls pushState. Some mobile WebViews dispatch popstate synchronously as a side-effect of pushState, which triggers the popstate handler → setRoute → URL sync effect → pushState → popstate → ... infinite loop. Fix: track what we just pushed in a prevPushedRef. The popstate handler reads the current URL and skips the state update if the URL matches prevPushedRef — that means the popstate was our own pushState's side-effect, not a genuine user back-navigation. Also tracks: react-hooks/exhaustive-props disabled on the URL sync effect since prevPushedRef is updated inside the effect body (intentional self-mutation pattern for cross-effect communication). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
c71de18ac1
commit
7cf4e74dc6
@ -6,7 +6,7 @@
|
||||
// instantiated when MobileApp is active, so no React Flow + heavy
|
||||
// chrome cost on phones.
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useTheme } from "@/lib/theme-provider";
|
||||
|
||||
@ -90,20 +90,30 @@ export function MobileApp() {
|
||||
// the URL is already what we'd produce — that handles the initial
|
||||
// mount (we read FROM the URL) and prevents duplicate history entries
|
||||
// when popstate restores state we just pushed.
|
||||
// Track what we pushed so the popstate handler can distinguish
|
||||
// "browser back to our own push" (skip) from "browser back to
|
||||
// user navigation" (update state).
|
||||
const prevPushedRef = useRef<{ route: Route; agentId: string | null }>({ route: route as Route, agentId });
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const current = readRouteFromUrl();
|
||||
if (current.route === route && current.agentId === agentId) return;
|
||||
const url = buildRouteUrl(route, agentId);
|
||||
prevPushedRef.current = { route, agentId };
|
||||
window.history.pushState({ route, agentId }, "", url);
|
||||
}, [route, agentId]);
|
||||
}, [route, agentId]); // eslint-disable-line react-hooks/exhaustive-props
|
||||
|
||||
// Sync URL → route state on browser back/forward. The popstate event
|
||||
// fires AFTER the URL has changed, so re-reading is correct.
|
||||
// Skip: if the URL now reflects the state we just pushed (popstate was
|
||||
// fired synchronously by our own pushState on some mobile browsers).
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const onPop = () => {
|
||||
const next = readRouteFromUrl();
|
||||
// If the URL reflects what we just pushed, skip — this popstate
|
||||
// was a side-effect of our own pushState, not user navigation.
|
||||
if (next.route === prevPushedRef.current.route && next.agentId === prevPushedRef.current.agentId) return;
|
||||
setRoute(next.route);
|
||||
setAgentId(next.agentId);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user