From b33f00c09548a89a510fab83c6d7310b868f9dbb Mon Sep 17 00:00:00 2001 From: Archmonger Date: Mon, 21 Sep 2026 08:02:30 +0000 Subject: [PATCH] fix: attach form submit listener before paint to prevent reload flake test_form_no_page_reload could fail with render_count == 2 in CI. The Form component attached its submit listener in React.useEffect, which runs *after* the browser paints. That leaves a window after the form is rendered but before the listener is wired, during which a submit falls through to the browser's native GET submission - a full page reload that remounts the app and re-runs the route component, bumping the render counter. Attach the listener in useLayoutEffect instead, which runs synchronously before paint, so the preventDefault handler is always in place by the time the form is visible and interactive. This mirrors the Link preventDefault race addressed for scroll restoration. --- src/js/src/components.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/js/src/components.ts b/src/js/src/components.ts index f8da299..e4cadec 100644 --- a/src/js/src/components.ts +++ b/src/js/src/components.ts @@ -133,7 +133,13 @@ export function Navigate({ * listener for ReactPy-Router's server-side form component. */ export function Form({ onSubmitCallback, formClass }: FormProps): null { - React.useEffect(() => { + // Attach the submit listener in a layout effect (before paint) rather than a + // passive effect (after paint). A passive effect leaves a window after the form + // is rendered but before the listener is wired, during which a submit would + // fall through to the browser's native GET submission (a full page reload). + // `useLayoutEffect` runs synchronously before paint, so the listener is always + // in place by the time the form is visible and interactive. + React.useLayoutEffect(() => { const handleSubmit = (event: Event) => { event.preventDefault(); const form = event.currentTarget as HTMLFormElement;