Skip to content

Tabs: registerTab should update in-place instead of unregister+register #329

Description

@Quis90

Problem

When TabPanel's title prop changes (new reference), the useEffect with [props.id, props.title] dependencies fires:

  1. Cleanup runs unregisterTab(id) — removes the tab from state
  2. Effect runs registerTab({id, title}) — appends to end via concat

The reducer's registerTab checks tabWasAlreadyRegistered by ID, but since cleanup already removed it, the tab gets appended to the end — causing visible tab reordering.

This means passing JSX as title (which creates a new reference on every render) causes the tab to jump to the end on every render.

Expected Behavior

When a tab with the same ID re-registers (e.g. due to title change), it should retain its position in the tab list.

Suggested Fix

In the reducer's registerTab case, if a tab with the same ID already exists, update its title in-place instead of skipping:

case 'registerTab': {
  const existingIndex = state.tabs.findIndex((tab) => tab.id === action.newTab.id);
  if (existingIndex !== -1) {
    const updatedTabs = [...state.tabs];
    updatedTabs[existingIndex] = action.newTab;
    return { ...state, tabs: updatedTabs };
  }
  return {
    tabs: state.tabs.concat(action.newTab),
    activeTabID: state.tabs.length === 0 ? action.newTab.id : state.activeTabID,
  };
}

Reproduction

Pass a JSX element as title that depends on changing state:

<TabPanel title={<MyTitle hasIcon={someChangingState} />} id="my-tab">

Every time someChangingState changes, the tab moves to the end of the tab list.

Workaround

Use a stable string as title instead of JSX.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions