Skip to content

fix(react-form): remove no-deps layout effect in useField to prevent infinite loops#2019

Closed
yannisgu wants to merge 2 commits intoTanStack:mainfrom
yannisgu:fix/use-field-update-no-deps
Closed

fix(react-form): remove no-deps layout effect in useField to prevent infinite loops#2019
yannisgu wants to merge 2 commits intoTanStack:mainfrom
yannisgu:fix/use-field-update-no-deps

Conversation

@yannisgu
Copy link

@yannisgu yannisgu commented Feb 5, 2026

Note

This PR was generated by an AI agent (Claude Code). Human verification of the fix and its implications is still outstanding.

Summary

  • Replace the no-dependency-array useIsomorphicLayoutEffect(() => { fieldApi.update(opts) }) in useField with direct property assignment (fieldApi.options = opts; fieldApi.name = opts.name)
  • This prevents infinite synchronous re-render loops when components trigger state updates or ref changes during React's commit phase

Reproduction

Minimal repro repo: https://github.com/yannisgu/radix-tanstack-form-react19-repro

git clone https://github.com/yannisgu/radix-tanstack-form-react19-repro
cd radix-tanstack-form-react19-repro/radix-select-tanstack-form
pnpm install && pnpm dev
# Open http://localhost:5173 — tab freezes

Problem

useField runs fieldApi.update(opts) inside a layout effect with no dependency array — it fires during every commit phase. While the comment says fieldApi.update "should not have any side effects", the implementation conditionally calls setFieldValue and setFieldMeta, which trigger store notifications.

This becomes an infinite loop when combined with any component that triggers a state update during commit:

  1. Layout effect fires → fieldApi.update(opts)setFieldMeta() → store notification → re-render
  2. Component's ref or effect triggers state update during commit → re-render
  3. Layout effect fires again → step 1 → infinite loop

The loop is synchronous (happens during React's commit phase), so the browser tab freezes rather than throwing "Maximum update depth exceeded".

Affected libraries

This has been confirmed to cause infinite loops with:

  • @radix-ui/react-selectSelectTrigger passes a state setter as a ref via composeRefs, which creates a new ref identity every render. In React 19, this causes ref unmount/remount during commit.
  • @radix-ui/react-checkbox — Uses useSize(element) which calls setSize() in useLayoutEffect
  • @dnd-kit/coreuseDroppable dispatches actions in useEffect

Fix

The comment already says this should be "like a useRef". This PR makes that literally true by directly assigning options during render instead of calling the full update() method in an effect.

The initialization side effects in fieldApi.update() (default value registration and meta initialization) are already handled by fieldApi.mount(), which calls this.update(this.options) during the mount layout effect (which has [fieldApi] deps).

Test plan

  • Existing tests should pass (the mount effect still calls fieldApi.update() for initialization)
  • Components using form.Field with Radix UI components no longer freeze
  • Default values still work (handled by fieldApi.mount())

…infinite loops

useField previously called fieldApi.update(opts) inside a layout effect
with no dependency array, causing it to run during every commit phase.
fieldApi.update() conditionally calls setFieldValue and setFieldMeta,
which trigger store notifications and re-renders.

Any component that triggers a state update or ref change during commit
(e.g. Radix UI's composeRefs creating a new ref identity in React 19,
or any component using useLayoutEffect with setState) would cause the
layout effect to fire again, creating an infinite synchronous re-render
loop that freezes the browser tab.

The comment already noted that fieldApi.update "should not have any side
effects" and should be "like a useRef". This change makes that literally
true by directly assigning options and name during render, rather than
calling the full update() method (which includes initialization side
effects) in an effect.

The initialization side effects (default value and meta registration)
are already handled by fieldApi.mount(), which calls this.update()
during the mount layout effect.
@changeset-bot
Copy link

changeset-bot bot commented Feb 5, 2026

🦋 Changeset detected

Latest commit: 94f80f3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@tanstack/react-form Patch
@tanstack/react-form-nextjs Patch
@tanstack/react-form-remix Patch
@tanstack/react-form-start Patch
@tanstack/form-core Patch
@tanstack/angular-form Patch
@tanstack/vue-form Patch
@tanstack/solid-form Patch
@tanstack/svelte-form Patch
@tanstack/form-devtools Patch
@tanstack/lit-form Patch
@tanstack/react-form-devtools Patch
@tanstack/solid-form-devtools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@crutchcorn
Copy link
Member

This fundamentally breaks how TanStack Form works. Do not open PRs to open-source projects with AI without reviewing manually, or else you will be banned from organizations for creating spam.

@crutchcorn crutchcorn closed this Feb 5, 2026
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.

2 participants