form icon indicating copy to clipboard operation
form copied to clipboard

bug: react - `form.Field mode=array` rerender the render props after first change, even if array size is identical - only first time

Open tresorama opened this issue 2 months ago • 3 comments

Describe the bug

Given...

const form = useForm({
  todos: [
    {text: 'Buy X'}
  ]
});

...as far as I know, this

<form.Field
  name="todos"
  mode="array"
  children={(arrayFieldApi) => {
    const items = arrayFieldApi.state.value;
    return (
      //jsx
    )
  }}
/>

should re-render the children only when the size f the array changes (after add/remove item), and should not re-render if the changes is on todos[0].text (items count is still 1).

This can be seen an equivalent to

<form.Subscribe 
  selector={state => state.values["todos"].length}
  children={(_) => {
  /// ...
  }}
/>

Problem

I noticed that the children is called twice for the same items count, but only on first render. For example:

  • the form load
  • children is invoked (render)
  • you update todos[0].text
  • children is invoked again
  • you update todos[0].text again
  • children is NOT invoked again (true for follow up changes as well)

Your minimal, reproducible example

https://stackblitz.com/edit/tanstack-form-hapw5etq?file=src%2Findex.tsx

Steps to reproduce

Expected behavior

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

TanStack Form adapter

react-form

TanStack Form version

1.23.7

TypeScript version

No response

Additional context

No response

tresorama avatar Oct 16 '25 23:10 tresorama

In an educational test I'm doing for an internal state library similar to zustand, that use an API identical to form.Subscribe, i noticed a similar bug.

I leave it here the rationale. I don't know if the implementation is the same but maybe can help

Bugged

 function useStoreState<R>(
    selector: Selector<TState, R>,
  ) {
    // get the store
    const { store } = useStoreContext();

    // save the first version of the selector
    const firstSelector = useRef(selector).current;

    // create a react state that when changes will trigger rerender
    const [state, setState] = useState(
      () => firstSelector(store.getState())
    );

    useEffect(() => {
      const unsubscribe = store.subscribe({
        selector: firstSelector,
        cb: (newState: R) => setState(newState),
      });
      return unsubscribe;
    }, []);

    return state;
  }

Fixed

 function useStoreState<R>(
    selector: Selector<TState, R>,
  ) {
    // get the store
    const { store } = useStoreContext();

    // save the first version of the selector
    const firstSelector = useRef(selector).current;

    // create a react state that when changes will trigger rerender
    const [state, setState] = useState(
+    // here we call the selector manually before we subscribed to the store.
+    // the store will not save the result of this selector as "prevValue" for this selector (because is not subscribed yet)
+    // so we need to 
+    // - save the state of this first "state` (selecir result) in a ref
+    // - then, inside useEffect, we subscribe to the store (for the same selector) with it as initial state
      () => firstSelector(store.getState())
    );
+    const firstState = useRef(state).current;

    useEffect(() => {
      const unsubscribe = store.subscribe({
        selector: firstSelector,
        cb: (newState: R) => setState(newState),
+       initialValue: firstState,
      });
      return unsubscribe;
    }, []);

    return state;
  }

tresorama avatar Oct 16 '25 23:10 tresorama

Thanks for the report!

To add onto the assessment, here is the line adding in the Subscribe, and is likely tied to the bug you're reporting.

LeCarbonator avatar Oct 20 '25 06:10 LeCarbonator

Repro: https://stackblitz.com/edit/tanstack-form-hapw5etq?file=src%2Findex.tsx

How to reproduce:

  • edit the input of the default array item
  • note that whole array field (not the array item field) rerenders (blue border pulsing)
  • edit again
  • note it doesn't rerender

The extra rerender happens on the first update.

You note the bug only if the form array field is not empty at the start. If the array is empty, the bugged rerender is used to create the first item , hiding the bug

tresorama avatar Oct 21 '25 22:10 tresorama

I don't think this happens anymore since some fixes to arrays were made. Closing unless @tresorama is able to reproduce this still

(Let me know either way ✨)

crutchcorn avatar Dec 15 '25 12:12 crutchcorn

@crutchcorn

v1.27.4 fixes it.

https://stackblitz.com/edit/tanstack-form-vubrj638?file=src%2Findex.tsx,src%2Fbugged.tsx,src%2Ffixed-1-27-4.tsx

Thanks

tresorama avatar Dec 15 '25 18:12 tresorama