Skip to content

Accordion/Collapse: keepMounted=true (Activity) suspends useEffect — breaks react-hook-form field registration in collapsed panels #9047

Description

@sakal-s

Summary

Since 9.3.1, Collapse defaults to keepMounted: true, which uses React 19's <Activity mode=\"hidden\"> for collapsed panels. React's Activity in hidden mode suspends all useEffect and useLayoutEffect calls. Libraries that rely on useEffect for setup — most notably react-hook-form — break silently inside collapsed Accordion panels.

Affected versions

  • 9.3.1 and above (introduced when keepMounted defaulted to true with Activity backing)
  • Not affected: 9.3.0 (before the fix — keepMounted was undefined, hitting the else content = children branch with no Activity)

Related issues

Root cause

Collapse.mjs rendering logic uses strict equality:

if (keepMounted === false)     content = isExited ? null : children;  // unmounts
else if (keepMounted === true) content = <Activity mode="hidden">;     // ← broken path
else                           content = children;                     // always in DOM

When keepMounted === true (the current default), React 19's Activity is used. Activity in mode="hidden" defers/suspends side effects. react-hook-form registers fields via useEffect inside Controller — those effects never fire in a hidden panel, so fields are never registered.

Reproduction

import { Accordion } from '@mantine/core';
import { useForm, FormProvider, useWatch, Controller } from 'react-hook-form';

function WatchedField({ name }) {
  const value = useWatch({ name });
  // With shouldUnregister: true (common default), this returns undefined
  // when the panel is collapsed — even for values present in defaultValues
  console.log(name, value);
  return <input {...useFormContext().register(name)} />;
}

export default function Demo() {
  const form = useForm({
    shouldUnregister: true,
    defaultValues: { panels: [{ type: 'A' }] },
  });

  return (
    <FormProvider {...form}>
      <Accordion>
        <Accordion.Item value="0">
          <Accordion.Control>Panel 0</Accordion.Control>
          <Accordion.Panel>
            {/* useWatch returns undefined here — field never registers via useEffect */}
            <WatchedField name="panels.0.type" />
          </Accordion.Panel>
        </Accordion.Item>
      </Accordion>
    </FormProvider>
  );
}

useWatch({ name: 'panels.0.type' }) returns undefined even though defaultValues has the value, because the Controller's registration useEffect is suspended by Activity.

Observed behaviour

  • useWatch returns undefined for fields inside a collapsed Accordion.Panel
  • Fields are never registered with react-hook-form while the panel is hidden
  • formState.errors for those fields is also inaccessible
  • Affects any library using useEffect for DOM/subscription setup

Expected behaviour

Fields inside a collapsed-but-mounted panel should behave as if they are mounted normally — effects should run, registrations should complete, subscriptions should be active. This is the contract keepMounted implies.

Current workaround

Override Collapse default props in the Mantine theme to a non-boolean truthy value, which bypasses the Activity branch entirely:

// theme.ts
createTheme({
  components: {
    Collapse: {
      defaultProps: { keepMounted: 1 as unknown as boolean }
    }
  }
})

1 === true is false (strict equality), so the else content = children branch runs — content stays in DOM, no Activity, effects work normally. This is obviously a hack and not a real solution.

Suggested fix

The proposed keepMountedMode="display-none" from PR #9021 would solve this. Alternatively, the default for keepMounted could remain false (explicit unmount) and users who want DOM preservation without Activity side-effects could opt in to a CSS-based approach.

A simpler short-term fix would be to document that keepMounted={true} suspends effects and to provide a first-class keepMountedMode or similar escape hatch.

Environment

  • @mantine/core: 9.4.1
  • React: 19.x
  • react-hook-form: 7.81.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    Fixed in patchCompleted issues that will be published with next patch (1.0.X)

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions