-
-
Notifications
You must be signed in to change notification settings - Fork 604
Description
Describe the bug
form.Subscribe triggers unnecessary re-renders when the selector returns an object or array, even when the underlying values have not changed. This is because LocalSubscribe in packages/react-form/src/useForm.tsx calls useStore(form.store, selector) without an equality function, so it falls back to Object.is reference equality. Since selectors that return objects/arrays create a new reference on every store update, the component re-renders every time any field in the form changes — not just the fields the selector cares about.
Screen recording
Your minimal, reproducible example
https://codesandbox.io/p/github/kevalmiistry/form-test/main?import=true
Steps to reproduce
- Open React DevTools and enable "Highlight updates when components render"
- Set Account Type to "Business"
- Set Country to "US"
- The Tax ID field appears (rendered inside the form.Subscribe block)
- Now type in any other input (e.g. the Name field)
- Observe that the form.Subscribe block (and the Tax ID field inside it) flashes/re-renders on every keystroke, even though accountType and country have not changed
Expected behavior
form.Subscribe should only re-render when the content of the selected value actually changes, not when the reference changes. A shallow equality check would solve this for the common patterns above (objects and arrays with primitive values).
How often does this bug happen?
Every time
Screenshots or Videos
No response
Platform
- OS: macOS Sequoia (Darwin 25.3.0)
- Browser: Chrome (latest)
@tanstack/react-form— react adapter- Tested on latest
mainbranch (commite21cc011)
TanStack Form adapter
react-form
TanStack Form version
1.28.5
TypeScript version
No response
Additional context
Root cause
In packages/react-form/src/useForm.tsx, the LocalSubscribe component:
function LocalSubscribe({ form, selector = (state) => state, children }) {
const data = useStore(form.store, selector) // ← no equality function
return <>{functionalUpdate(children, data)}</>
}useStore from @tanstack/react-store accepts an optional third argument — an equality function. Without it, Object.is is used, which fails for objects and arrays.
Proposed fix
- Import
shallowfrom@tanstack/react-store(already a dependency, zero cost) - Pass
shallowas the third argument touseStore - Extract the default selector to a module-level constant to fix the
@eslint-react/no-unstable-default-propslint warning
import { shallow, useStore } from '@tanstack/react-store'
const defaultSelector = (state: AnyFormState) => state
function LocalSubscribe({
form,
selector = defaultSelector,
children,
}: PropsWithChildren<{
form: AnyFormApi
selector?: (state: AnyFormState) => AnyFormState
}>) {
const data = useStore(form.store, selector, shallow)
return <>{functionalUpdate(children, data)}</>
}This is a non-breaking change. shallow correctly handles primitives (falls back to Object.is), plain objects, arrays, Maps, Sets, and Dates. All existing tests pass.
I have already implemented this in my local setup and can confirm it is working as expected and all tests are passing post changes. I can open a PR for this if the approach looks good. In that case please assign the issue to me.
