TanStack / TanStack/form

Array item removal binds a surviving sibling's sub-Field to a stale, out-of-range index for one render (stable/non-index keys)

Open
#2,249 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

v1 v2: needs investigation
Dominant language
TypeScript
Stars
6.7k
Forks
682
Avg merge
5d 18h
Merged PRs (30d)
7

Description

[Opus-written, can add human replies if something's not clear or helpful!]

First off, thanks for all the work on TanStack Form. It's been a joy to build with, and this is a pretty narrow edge case rather than anything blocking us. Wanted to write it up carefully in case it helps.

Summary

When you remove a non-last item from an array field, a surviving sibling's sub-Fields re-render for one frame with their name still pointing at the old, now out-of-range index. During that single frame both field.state.value and form.getFieldValue(field.name) come back undefined, even though the field's value type is non-optional (for example a plain string). On the very next render everything reconciles to the correct index and is fine.

This shows up when the array item containers use stable (non-index) React keys. We need stable keys for drag-to-reorder and view transitions, so switching to index keys isn't really an option for us.

I can confirm it still reproduces on @tanstack/react-form@1.28.3, which is a good number of releases after the earliest related report.

How this relates to existing issues

I dug through the tracker first, and two closed issues are in the neighborhood:

  • #1363 ("Non-index keys in React causing inputs to be uncontrolled when removing array fields") looks like the same root cause. With non-index keys, removing an item leaves a sibling's field name/path pointing at a stale index, the value reads as undefined for a render, and React flips the input to uncontrolled. It was last reproduced on 1.2.3 and closed without a fix landing, so I figured a fresh writeup on a current version might be useful.
  • #1518 is close but subtly different. There, form.getFieldValue(field.name) still returns the correct value while state.value is transiently undefined. In our case that workaround doesn't hold, because the field's name itself is stale, so the store lookup comes back empty too. The lag is in the field's path, not just its cached value.

Happy to be pointed at whichever of these you'd rather consolidate into if you feel it's a duplicate.

Versions

  • @tanstack/react-form 1.28.3, @tanstack/form-core 1.28.x
  • React 19, Next.js 16 (App Router)

Reproduction

  • An array field options, where each item is { name: string, values: {...}[] }.
  • The item containers are keyed by a stable key (key={opt.name}), because rows are drag-reorderable (dnd-kit) and animated with <ViewTransition>, both of which want stable identity.
  • Each row renders its sub-fields via form.AppField name={options[${idx}].name} and similar, where idx is the .map() index passed down as a prop.

Steps:

  1. Start with two options, say Tamaño (index 0) and Color (index 1).
  2. Remove the first one (Tamaño, index 0). The array becomes length 1, and Color should shift to index 0.
  3. The surviving Color row's sub-fields re-render once.

What I see

On that re-render, some temporary tracing logged (formatted for readability):

OptionHeader.name  transient { fieldName: "options[1].name",   stateValue: null, storeValue: null }
OptionBody.values  transient { fieldName: "options[1].values", stateValue: null, storeValue: null }
  • fieldName is still options[1], the old index, even though the array now only has options[0].
  • field.state.value is undefined.
  • form.getFieldValue(field.name) is also undefined, because options[1] no longer exists.

The next render reconciles to options[0] and it's all correct.

What I'd expect

A sub-Field of an array item shouldn't render bound to an index that no longer exists after a sibling is removed, and a field whose value type is non-optional shouldn't transiently expose undefined.

What we're doing for now

We early-return when the value is missing, since the field is about to reconcile or unmount anyway:

<form.AppField name={`options[${optionIdx}].name`}>
  {(nameField) => {
    // Transient undefined guard, TanStack Form bug
    // oxlint-disable-next-line typescript/no-unnecessary-condition
    if (nameField.state.value === undefined) return null
    // ...
  }}
</form.AppField>

That avoids the crash and the final render is correct. The tradeoff is that the row renders nothing for one frame, and the typed value is briefly a small lie.

So

Would it be feasible for the array re-index to update a surviving item's sub-Field name/value in step with the array mutation (or to skip rendering removed or shifted paths), so that stable-keyed array items stay type-correct without forcing index keys? I know that's easier said than done given how derivation works, so no pressure. I'm glad to put together a minimal StackBlitz on 1.28.x if that would help you dig in.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the array mutation and field-path derivation in @tanstack/form-core, using the React AppField reproduction with stable keys and the form.getFieldValue API described here. Reproduce removal of the first item from options, then trace why the surviving sub-Fields retain options[1]; done means they use the shifted path without a transient undefined value.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.