From 8d8c99f330dab042a6537fc6f4d0a0d1e9b0141e Mon Sep 17 00:00:00 2001 From: Segun Adebayo Date: Thu, 13 Aug 2026 11:24:41 +0200 Subject: [PATCH] fix(react): prevent Fieldset.Root render loop on subtree mutations --- .changeset/fix-fieldset-rerender-loop.md | 6 +++++ .../src/components/fieldset/fieldset.test.tsx | 26 +++++++++++++++++++ .../src/components/fieldset/use-fieldset.ts | 12 ++++----- 3 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 .changeset/fix-fieldset-rerender-loop.md diff --git a/.changeset/fix-fieldset-rerender-loop.md b/.changeset/fix-fieldset-rerender-loop.md new file mode 100644 index 0000000000..96d55f0c2d --- /dev/null +++ b/.changeset/fix-fieldset-rerender-loop.md @@ -0,0 +1,6 @@ +--- +'@ark-ui/react': patch +--- + +Fix `Fieldset.Root` re-rendering whenever its subtree mutated, even if helper and error text were unchanged. A nested +`Field.Textarea` with `defaultValue` could turn that into a loop that froze the tab. diff --git a/packages/react/src/components/fieldset/fieldset.test.tsx b/packages/react/src/components/fieldset/fieldset.test.tsx index 36747de116..2feca59e75 100644 --- a/packages/react/src/components/fieldset/fieldset.test.tsx +++ b/packages/react/src/components/fieldset/fieldset.test.tsx @@ -75,4 +75,30 @@ describe('Fieldset', () => { expect(describedBy).toContain('helper-text') }) }) + + it('should not re-render in a loop when a textarea with defaultValue is nested', () => { + let renders = 0 + + render( + + Legend + + Summary + + + + {() => { + renders += 1 + if (renders > 50) { + throw new Error('Fieldset entered an update loop') + } + return null + }} + + , + ) + + expect(screen.getByRole('textbox')).toHaveValue('hello') + expect(renders).toBeLessThan(10) + }) }) diff --git a/packages/react/src/components/fieldset/use-fieldset.ts b/packages/react/src/components/fieldset/use-fieldset.ts index 647cae4694..f36e5536b1 100644 --- a/packages/react/src/components/fieldset/use-fieldset.ts +++ b/packages/react/src/components/fieldset/use-fieldset.ts @@ -29,7 +29,8 @@ export const useFieldset = (props: UseFieldsetProps = {}) => { const env = useEnvironmentContext() - const [textElements, setTextElements] = useState({ hasErrorText: false, hasHelperText: false }) + const [hasErrorText, setHasErrorText] = useState(false) + const [hasHelperText, setHasHelperText] = useState(false) const uid = useId() const id = props.id ?? uid @@ -45,9 +46,8 @@ export const useFieldset = (props: UseFieldsetProps = {}) => { const checkTextElements = () => { const docOrShadowRoot = env.getRootNode() as ShadowRoot | Document - const hasErrorText = !!docOrShadowRoot.getElementById(errorTextId) - const hasHelperText = !!docOrShadowRoot.getElementById(helperTextId) - setTextElements({ hasErrorText, hasHelperText }) + setHasErrorText(!!docOrShadowRoot.getElementById(errorTextId)) + setHasHelperText(!!docOrShadowRoot.getElementById(helperTextId)) } checkTextElements() @@ -60,8 +60,8 @@ export const useFieldset = (props: UseFieldsetProps = {}) => { }, [env, errorTextId, helperTextId]) const ids: string[] = [] - if (textElements.hasErrorText && invalid) ids.push(errorTextId) - if (textElements.hasHelperText) ids.push(helperTextId) + if (hasErrorText && invalid) ids.push(errorTextId) + if (hasHelperText) ids.push(helperTextId) const labelIds = ids.length > 0 ? ids.join(' ') : undefined const getRootProps = () =>