Performance: `FieldPathId` objects break memoization — stabilize identity instead of deep-comparing

Author: jimmycallinCreated Aug 27, 2026Updated Sep 12, 2026
Labelsbreaking change

Every field receives a fieldPathId prop: { path: (string|number)[], $id: string, name? }. It is rebuilt by toFieldPathId() on every render, at every level of the field tree. React's memoization (React.memo, shouldComponentUpdate) compares props by reference, so a freshly-built-but-equal object looks like a change, and the memo boundary silently stops working.

Today we compensate downstream of the problem:

  • useDeepCompareMemo(toFieldPathId(...)) at six call sites in core (ArrayField ×3, ObjectField, LayoutGridField, FallbackField) to launder the unstable reference;
  • deepEquals in SchemaField's memo comparator and shouldRender.

This works, but it's the pattern the React docs explicitly warn against: custom comparators doing deep equality "can become incredibly slow and can freeze your app" (react.dev/reference/react/memo), and the cost is paid on every render at every boundary. It also can't be fixed with useMemo: the instability cascades (each field's id depends on the parent's unstable id object), Form is a class component, and LayoutGridField creates ids inside loops where hooks can't go. useMemo is also documented as a discardable cache, "not a semantic guarantee."

The root cause is a data-modeling choice: field identity is represented as an object recreated during render, while consumers rely on reference identity.

How other libraries solved this

Every major React form library identifies a field by a primitive string path'friends[0].firstName' — not an object. react-hook-form (_fields keyed by name string, get(_fields, name)), Formik (getIn/setIn over dot paths), TanStack Form (name string into an external store), React Final Form (registerField(name: string, ...)). A string's identity is its value, so memo boundaries, map keys, and subscription keys are stable for free. This is also react.dev's first-listed advice for memo: "prefer accepting individual values as props."

The performant ones go one step further and invert data flow: state lives outside React and fields subscribe by their name string (react-hook-form's pub-sub subjects + uncontrolled inputs, Final Form's subscription model, TanStack's store selectors), so one keystroke re-renders one field instead of the tree. The lone library architected like RJSF — Formik, prop/context-driven with deep-compare compensation (FastField) — is the ecosystem's documented performance cautionary tale (formik#1974, formik#2335).

Where stable object identity is genuinely needed, the established pattern is interning: a keyed cache returning the same instance per logical key. Jotai's atomFamily and Reselect v5's weakMapMemoize are exactly this. The known hard part is eviction — Recoil's unbounded global family caches leaked for years (Recoil#366, #954). The discipline: key the cache by an object reference in a WeakMap so GC handles invalidation, or scope it to an owning instance (Apollo per-InMemoryCache, Relay per-Environment).

What this means for RJSF

Short term (v6, non-breaking): Deprecate useDeepCompareMemo in @rjsf/utils docs (keep exported for compatibility)

Long term (v7, breaking): make field identity a primitive. Replace the fieldPathId object prop with a canonical string path, resolved lazily where the segments are needed. This dissolves the problem instead of caching around it and matches the entire modern form ecosystem. It requires a library-owned path syntax with escaping (the current idSeparator join is ambiguous when field names contain the separator) — solved the same way everyone else solved it. It would also unlock the bigger prize: subscription-based field reactivity instead of re-rendering the tree through props.

Source: rjsf-team/react-jsonschema-form