Skip to content

SegmentedControl: infinite re-render loop (React error #185) in production builds with React 19 #8809

@marcelolima381

Description

@marcelolima381

Dependencies check up

  • I have verified that I use latest version of all @mantine/* packages

What version of @mantine/* packages do you have in package.json?

9.0.0

What package has an issue?

@mantine/core

What framework do you use?

Vite

In which browsers you can reproduce the issue?

Chrome

Describe the bug

Bug description

SegmentedControl crashes with React error #185 ("Too many re-renders") in production builds when the component is rendered inside a Modal (or any conditionally mounted
container). The crash does not occur in development builds.

Environment

  • @mantine/core: 9.0.0
  • react: 19.2.4
  • Bundler: Vite (ESM output)
  • Mode: production build only

Steps to reproduce

  1. Render a SegmentedControl inside a <Modal> (controlled, opened state)
  2. Open the modal
  3. App crashes immediately with React error Feature/strict types in public api #185
function Example() {
  const [opened, setOpened] = useState(false);
  const [value, setValue] = useState('a');

  return (
    <>
      <Button onClick={() => setOpened(true)}>Open</Button>
      <Modal opened={opened} onClose={() => setOpened(false)}>
        <SegmentedControl
          value={value}
          onChange={setValue}
          data={['a', 'b', 'c']}
        />
      </Modal>
    </>
  );
}

Root cause

The bug is in esm/components/SegmentedControl/SegmentedControl.mjs. The component uses useState to track DOM element refs and a functional updater that always creates a new
object:

// SegmentedControl.mjs (ESM — used by Vite in production)
const [refs, setRefs] = useState({});
const setElementRef = (element, val) => {
    setRefs((prev) => ({ ...prev, [val]: element })); // ← always new object
};

// inline ref callback — new function identity on every render
ref: (node) => setElementRef(node, `${item.value}`),

The infinite loop mechanism:

1. SegmentedControl renders  ref callback attaches  setRefs called with a new object  state update  re-render
2. On re-render, the inline ref callback has a new function identity
3. React sees the ref changed  calls old_ref(null)  setRefs  state update
4. React calls new_ref(element)  setRefs  state update
5. Both updates trigger re-renders  repeat  React hits the 25 re-render limit  error #185

Note: the CJS version (cjs/components/SegmentedControl/SegmentedControl.cjs) does NOT have this bug. It uses direct mutation instead:

// SegmentedControl.cjs (CJS — does NOT crash)
const setElementRef = (element, val) => {
    refs[val] = element;
    setRefs(refs); // ← same object reference → React bails out → no re-render
};

This ESM/CJS inconsistency confirms it's a regression in the ESM build.

### If possible, include a link to a codesandbox with a minimal reproduction

_No response_

### Possible fix

Replace the useState-based ref tracking with useRef + stable cached callbacks, so React never sees a changed ref identity:

import { createElement, useState, useRef } from "react";

// Replace:
// const [refs, setRefs] = useState({});
// const setElementRef = (element, val) => {
//     setRefs((prev) => ({ ...prev, [val]: element }));
// };

// With:
const refsMap = useRef({});
const [, forceUpdate] = useState(0);
const callbackCache = useRef({});
const getStableRef = (val) => {
    if (!callbackCache.current[val]) {
        callbackCache.current[val] = (node) => {
            if (refsMap.current[val] !== node) {
                refsMap.current[val] = node;
                if (node !== null) forceUpdate((n) => n + 1);
            }
        };
    }
    return callbackCache.current[val];
};

// Then in the label element:
// ref: (node) => setElementRef(node, `${item.value}`),
// →
// ref: getStableRef(`${item.value}`),

// And in FloatingIndicator:
// target: refs[`${_value}`],
// →
// target: refsMap.current[`${_value}`],

This ensures:
- The ref callback function is stable (same identity between renders)
- React never triggers the old/new ref cleanup cycle unnecessarily
- forceUpdate only fires when an element actually attaches (node !== null), never on cleanup

### Self-service

- [ ] I would be willing to implement a fix for this issue

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs reproductionIssues without reproduction, closed within a week if the reproduction is not provided

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions