What you were expecting:
Submitting a TabbedForm with many required fields should validate and show errors — the same way a small form, or a SimpleForm with the identical fields, does.
What happened instead:
Once a TabbedForm has enough required fields spread across several tabs, clicking the default <SaveButton> throws "Maximum update depth exceeded" and the form never submits. The same fields in a SimpleForm submit fine.
Reproduced deterministically (3/3) with a headless browser on react-admin 5.14.5.
Steps to reproduce:
- A
TabbedForm with ~6 tabs × ~8 required TextInputs (validator defined at module scope — not the inline-validator pitfall), default toolbar / SaveButton.
- Open the edit page and click Save with the fields empty.
- →
Maximum update depth exceeded; the form does not submit.
- Move the identical fields into a
SimpleForm → no crash. Or add a form-level validate to the TabbedForm → no crash.
Related code: (runnable — drop into a fork of examples/simple, or any Vite + react-admin app)
import {
Admin, Resource, List, Datagrid, TextField, Edit, TabbedForm,
TextInput, required,
} from "react-admin";
import fakeDataProvider from "ra-data-fakerest";
const validate = required(); // single validator, module scope
const TABS = 6, PER = 8; // ~49 required fields across tabs
const PostList = () => (
<List><Datagrid rowClick="edit"><TextField source="batch" /></Datagrid></List>
);
const PostEdit = () => (
<Edit>
<TabbedForm> {/* crashes; <SimpleForm> does not */}
<TabbedForm.Tab label="Info">
<TextInput source="batch" validate={validate} />
</TabbedForm.Tab>
{Array.from({ length: TABS }, (_, n) => (
<TabbedForm.Tab key={n} label={`Tab ${n + 1}`}>
{Array.from({ length: PER }, (_, i) => (
<TextInput key={`t${n}f${i}`} source={`t${n}f${i}`} validate={validate} />
))}
</TabbedForm.Tab>
))}
</TabbedForm>
</Edit>
);
const dataProvider = fakeDataProvider({ posts: [{ id: 1, batch: "B1" }] });
export const App = () => (
<Admin dataProvider={dataProvider}>
<Resource name="posts" list={PostList} edit={PostEdit} />
</Admin>
);
// Open /posts/1 -> click Save -> "Maximum update depth exceeded".
Minimal isolation — same field count, only the form container changes:
| Container |
Result |
TabbedForm (per-tab FormGroup) |
crash |
SimpleForm (no FormGroup) |
ok |
TabbedForm + form-level validate (resolver) |
ok |
Likely mechanism:
useInput wraps every validate as an async function, so react-hook-form's hasPromiseValidation is always true and it toggles validatingFields per field via _updateIsValidating during validation.
<FormTab> wraps content in <FormGroupContextProvider>; useFormGroup subscribes to validatingFields and calls setState on every change.
- With enough required async fields across the tab FormGroups, a single submit churns
validatingFields field-by-field, and the re-render cascade exceeds React's NESTED_UPDATE_LIMIT (50). SimpleForm has no validatingFields subscriber, so it is immune.
Notes:
- Threshold/timing-sensitive near the limit — small forms don't trip it, and it can be flaky right at the boundary (consistent with hovering at the 50-update ceiling).
- Not specific to array-vs-single validators, nor to
useWatch/helperText — plain TextInputs with a single required() reproduce it.
This looks like a severe, at-scale variant of #5323 ("Async validation leads to form-level re-rendering"), which is closed and described only extra re-renders, not a crash.
Workaround: provide a single form-level validate, which react-admin turns into a resolver (getSimpleValidationResolver). react-hook-form's handleSubmit runs resolver XOR per-field, so it validates the whole form in one pass and skips per-field validation — validatingFields toggles once in bulk instead of per field, and the cascade never happens.
Environment
- React-admin version: 5.14.5
- ra-core version: 5.14.5
- react-hook-form version: 7.72.1
- React version: 19.2.5
- @mui/material version: 7.3.10
- Last version that did not exhibit the issue (if applicable): not version-specific — inherent to async-wrapped validators in a
TabbedForm at scale
- Browser: Chromium (headless, Playwright) and Chrome
What you were expecting:
Submitting a
TabbedFormwith many required fields should validate and show errors — the same way a small form, or aSimpleFormwith the identical fields, does.What happened instead:
Once a
TabbedFormhas enough required fields spread across several tabs, clicking the default<SaveButton>throws "Maximum update depth exceeded" and the form never submits. The same fields in aSimpleFormsubmit fine.Reproduced deterministically (3/3) with a headless browser on react-admin 5.14.5.
Steps to reproduce:
TabbedFormwith ~6 tabs × ~8 requiredTextInputs (validator defined at module scope — not the inline-validator pitfall), default toolbar /SaveButton.Maximum update depth exceeded; the form does not submit.SimpleForm→ no crash. Or add a form-levelvalidateto theTabbedForm→ no crash.Related code: (runnable — drop into a fork of
examples/simple, or any Vite + react-admin app)Minimal isolation — same field count, only the form container changes:
TabbedForm(per-tabFormGroup)SimpleForm(noFormGroup)TabbedForm+ form-levelvalidate(resolver)Likely mechanism:
useInputwraps everyvalidateas anasyncfunction, so react-hook-form'shasPromiseValidationis always true and it togglesvalidatingFieldsper field via_updateIsValidatingduring validation.<FormTab>wraps content in<FormGroupContextProvider>;useFormGroupsubscribes tovalidatingFieldsand callssetStateon every change.validatingFieldsfield-by-field, and the re-render cascade exceeds React'sNESTED_UPDATE_LIMIT(50).SimpleFormhas novalidatingFieldssubscriber, so it is immune.Notes:
useWatch/helperText— plainTextInputs with a singlerequired()reproduce it.This looks like a severe, at-scale variant of #5323 ("Async validation leads to form-level re-rendering"), which is closed and described only extra re-renders, not a crash.
Workaround: provide a single form-level
validate, which react-admin turns into a resolver (getSimpleValidationResolver). react-hook-form'shandleSubmitruns resolver XOR per-field, so it validates the whole form in one pass and skips per-field validation —validatingFieldstoggles once in bulk instead of per field, and the cascade never happens.Environment
TabbedFormat scale