bug: react - `form.Field mode=array` rerender the render props after first change, even if array size is identical - only first time
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
childrenis invoked (render)- you update
todos[0].text childrenis invoked again- you update
todos[0].textagain childrenis 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
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;
}
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.
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
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
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