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 = () =>