Skip to content

perf(react-form): form.Subscribe causes unnecessary re-renders when selector returns objects or arrays with same content #2090

@kevalmiistry

Description

@kevalmiistry

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

Image

Your minimal, reproducible example

https://codesandbox.io/p/github/kevalmiistry/form-test/main?import=true

Steps to reproduce

CodeSandBox

  1. Open React DevTools and enable "Highlight updates when components render"
  2. Set Account Type to "Business"
  3. Set Country to "US"
  4. The Tax ID field appears (rendered inside the form.Subscribe block)
  5. Now type in any other input (e.g. the Name field)
  6. 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 main branch (commit e21cc011)

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

  1. Import shallow from @tanstack/react-store (already a dependency, zero cost)
  2. Pass shallow as the third argument to useStore
  3. Extract the default selector to a module-level constant to fix the @eslint-react/no-unstable-default-props lint 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions