Repository: mui/mui-x
Pull request #16357: [Tree View] Add React Compiler linting rules
Author: login: flaviendelangle
Labels: name: type: enhancement
description: It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.
name: scope: tree view
description: Changes related to the tree view. This includes TreeView, TreeItem.
Created: 2025-01-27T16:48:25Z | Merged: 2025-04-16T06:46:44Z

- [x] Remove the concept of models in favor of storing the value directly in the state (similar to what the grid is doing)
- [x] Other small fixes to make ESLint happy


Diff:
diff --git a/.eslintrc.js b/.eslintrc.js
index 3507ec728001e..c4cdc01526887 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -24,7 +24,7 @@ const ENABLE_REACT_COMPILER_PLUGIN_DATA_GRID =
 const ENABLE_REACT_COMPILER_PLUGIN_DATE_PICKERS =
   process.env.ENABLE_REACT_COMPILER_PLUGIN_DATE_PICKERS ?? false;
 const ENABLE_REACT_COMPILER_PLUGIN_TREE_VIEW =
-  process.env.ENABLE_REACT_COMPILER_PLUGIN_TREE_VIEW ?? false;
+  process.env.ENABLE_REACT_COMPILER_PLUGIN_TREE_VIEW ?? true;
 
 const isAnyReactCompilerPluginEnabled =
   ENABLE_REACT_COMPILER_PLUGIN ||
diff --git a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js
index d8ee5d6799a64..7e2aa1da6bda0 100644
--- a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js
+++ b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.js
@@ -13,8 +13,8 @@ import {
   RichTreeViewItems,
 } from '@mui/x-tree-view/internals';
 
-const useTreeViewLogExpanded = ({ params, models }) => {
-  const expandedStr = JSON.stringify(models.expandedItems.value);
+const useTreeViewLogExpanded = ({ params, store }) => {
+  const expandedStr = JSON.stringify(store.value.expansion.expandedItems);
 
   React.useEffect(() => {
     if (params.areLogsEnabled && params.logMessage) {
diff --git a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx
index 6bdec09f20949..dbd9bddb4035d 100644
--- a/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx
+++ b/docs/data/tree-view/rich-tree-view/headless/LogExpandedItems.tsx
@@ -40,9 +40,9 @@ type TreeViewLogExpandedSignature = TreeViewPluginSignature<{
 
 const useTreeViewLogExpanded: TreeViewPlugin<TreeViewLogExpandedSignature> = ({
   params,
-  models,
+  store,
 }) => {
-  const expandedStr = JSON.stringify(models.expandedItems.value);
+  const expandedStr = JSON.stringify(store.value.expansion.expandedItems);
 
   React.useEffect(() => {
     if (params.areLogsEnabled && params.logMessage) {
diff --git a/docs/data/tree-view/rich-tree-view/headless/headless.md b/docs/data/tree-view/rich-tree-view/headless/headless.md
index 697f5621d8777..47801d9c82c5b 100644
--- a/docs/data/tree-view/rich-tree-view/headless/headless.md
+++ b/docs/data/tree-view/rich-tree-view/headless/headless.md
@@ -61,65 +61,15 @@ useCustomPlugin.getDefaultizedParams = ({ params }) => ({
 });
 ```
 
-### Controllable models
-
-A model is a value that can either be controlled or initialized using a prop.
-The Tree View contains several models like the `expandedItems` model which contains the ids of the items currently expanded.
-
-You can create your own models using the `models` property of your plugin:
-
-```ts
-useCustomPlugin.params = {
-  // ...other params
-  defaultCustomModel: true,
-  customModel: true,
-};
-
-useCustomPlugin.getDefaultizedParams = ({ params }) => ({
-  ...params,
-  // ... other defaultized params
-  defaultCustomModel: params.defaultCustomModel ?? false,
-});
-
-useCustomPlugin.models = {
-  customModel: {
-    getDefaultValue: (params) => defaultCustomModel,
-  },
-};
-```
-
-:::info
-When creating a model, you should always set a default value to your `defaultCustomModel` prop. This will initialize the component's internal state when the model is uncontrolled.
-See the [Params default value](/x/react-tree-view/rich-tree-view/headless/#params-default-value) section for more information.
-:::
-
-You can then use this model in your plugin (or in any other plugin) using the `models` parameter:
-
-```ts
-const useCustomPlugin = ({ models }) => {
-  React.useEffect(() => {
-    console.log(models.customModel.value);
-  });
-
-  const updateCustomModel = (newValue) =>
-    models.customModel.setControlledValue(newValue);
-
-  return {};
-};
-```
-
 ### Use elements from another plugin
 
-Your plugin can access the instance methods, the params and the models of any other plugin.
+Your plugin can access the instance methods, the params and the state of any other plugin.
 
 ```ts
-const useCustomPlugin = ({ models }) => {
+const useCustomPlugin = ({ store }) => {
   const handleSomeAction = () => {
     // Log the id of the items currently expanded
-    console.log(models.expandedItems.value);
-
-    // Collapse all the items
-    models.expandedItems.setValue([]);
+    console.log(store.value.expansion.expandedItems);
 
     // Check if an item is expanded
     const isExpanded = useSelector(selectorIsItemExpanded, 'some-item-id');
@@ -127,11 +77,6 @@ const useCustomPlugin = ({ models }) => {
 };
 ```
 
-:::warning
-Setting a model value in another plugin is wrong because it won't publish the associated callback.
-We probably need a new abstraction here so that a plugin is always responsible for its model updates.
-:::
-
 ### Define a plugin state
 
 TODO
@@ -146,9 +91,12 @@ The Tree View instance is an object accessible in all the plugins and in the Tre
 It is the main way a plugin can provide features to the rest of the component.
 
 ```ts
-const useCustomPlugin = ({ models }) => {
+const useCustomPlugin = ({ store }) => {
   const toggleCustomModel = () =>
-    models.customModel.setValue(!models.customModel.value);
+    store.update((prevState) => ({
+      ...prevState,
+      customModel: !prevState.customModel,
+    }));
 
   return {
     instance: {
@@ -161,7 +109,7 @@ const useCustomPlugin = ({ models }) => {
 You can then use this instance method in any other plugin:
 
 ```ts
-const useOtherCustomPlugin = ({ models, instance }) => {
+const useOtherCustomPlugin = ({ instance }) => {
   const handleSomeAction = () => {
     instance.toggleCustomModel();
   };
@@ -171,10 +119,13 @@ const useOtherCustomPlugin = ({ models, instance }) => {
 ### Emit and receive events
 
 ```ts
-const useCustomPlugin = () => {
+const useCustomPlugin = ({ store }) => {
   const toggleCustomModel = () => {
-    const newValue = !models.customModel.value;
-    models.customModel.setValue(newValue);
+    const newValue = !selectorCustomModel(store.value);
+    store.update((prevState) => ({
+      ...prevState,
+      customModel: newValue,
+    }));
     publishTreeViewEvent(instance, 'toggleCustomModel', { value: newValue });
   };
 
@@ -273,8 +224,6 @@ type UseCustomPluginSignature = TreeViewPluginSignature<{
   events: UseCustomPluginEvents;
   // The states defined by your plugin
   state: UseCustomPluginState;
-  // The name of the models defined by your plugin
-  modelNames: UseCustomPluginModelNames;
   // The plugins this plugin needs to work correctly
   dependencies: UseCustomPluginDependantPlugins;
 }>;
@@ -308,8 +257,6 @@ type UseCustomPluginSignature = TreeViewPluginSignature<{
     };
   };
   contextValue: { customPlugin: { enabled: boolean } };
-  modelNames: 'customModel';
-  // We want to have access to the expansion models and methods of the expansion plugin.
   dependencies: [UseTreeViewExpansionSignature];
 }>;
 ```
diff --git a/packages/x-tree-view/src/internals/models/plugin.ts b/packages/x-tree-view/src/internals/models/plugin.ts
index 25ebf01ad2590..1fc487c8abbf2 100644
--- a/packages/x-tree-view/src/internals/models/plugin.ts
+++ b/packages/x-tree-view/src/internals/models/plugin.ts
@@ -1,6 +1,6 @@
 import * as React from 'react';
 import { EventHandlers } from '@mui/utils/types';
-import { TreeViewExperimentalFeatures, TreeViewInstance, TreeViewModel } from './treeView';
+import { TreeViewExperimentalFeatures, TreeViewInstance } from './treeView';
 import type { MergeSignaturesProperty, OptionalIfEmpty } from './helpers';
 import { TreeViewEventLookupElement } from './events';
 import type { TreeViewCorePluginSignatures } from '../corePlugins';
@@ -18,11 +18,6 @@ export interface TreeViewPluginOptions<TSignature extends TreeViewAnyPluginSigna
    */
   params: TreeViewUsedDefaultizedParams<TSignature>;
   experimentalFeatures: TreeViewUsedExperimentalFeatures<TSignature>;
-  /**
-   * The store of controlled properties.
-   * If they are not controlled by the user, they will be initialized by the plugin.
-   */
-  models: TreeViewUsedModels<TSignature>;
   /**
    * The store that can be used to access the state of other plugins.
    */
@@ -37,14 +32,6 @@ export interface TreeViewPluginOptions<TSignature extends TreeViewAnyPluginSigna
   plugins: TreeViewPlugin<TreeViewAnyPluginSignature>[];
 }
 
-type TreeViewModelsInitializer<TSignature extends TreeViewAnyPluginSignature> = {
-  [TControlled in keyof TSignature['models']]: {
-    getDefaultValue: (
-      params: TSignature['defaultizedParams'],
-    ) => Exclude<TSignature['defaultizedParams'][TControlled], undefined>;
-  };
-};
-
 type TreeViewResponse<TSignature extends TreeViewAnyPluginSignature> = {
   getRootProps?: <TOther extends EventHandlers = {}>(
     otherHandlers: TOther,
@@ -60,7 +47,6 @@ export type TreeViewPluginSignature<
     publicAPI?: {};
     events?: { [key in keyof T['events']]: TreeViewEventLookupElement };
     state?: {};
-    modelNames?: keyof T['defaultizedParams'];
     experimentalFeatures?: string;
     dependencies?: readonly TreeViewAnyPluginSignature[];
     optionalDependencies?: readonly TreeViewAnyPluginSignature[];
@@ -88,17 +74,6 @@ export type TreeViewPluginSignature<
    * The state is the mutable data that will actually be stored in the plugin state and can be accessed by other plugins.
    */
   state: T extends { state: {} } ? T['state'] : {};
-  /**
-   * A helper for controlled properties.
-   * Properties defined here can be controlled by the user. If they are not controlled, they will be initialized by the plugin.
-   */
-  models: T extends { defaultizedParams: {}; modelNames: keyof T['defaultizedParams'] }
-    ? {
-        [TControlled in T['modelNames']]-?: TreeViewModel<
-          Exclude<T['defaultizedParams'][TControlled], undefined>
-        >;
-      }
-    : {};
   experimentalFeatures: T extends { experimentalFeatures: string }
     ? { [key in T['experimentalFeatures']]?: boolean }
     : {};
@@ -122,7 +97,6 @@ export type TreeViewAnyPluginSignature = {
   dependencies: any;
   optionalDependencies: any;
   events: any;
-  models: any;
   experimentalFeatures: any;
   publicAPI: any;
 };
@@ -163,14 +137,6 @@ type TreeViewUsedExperimentalFeatures<TSignature extends TreeViewAnyPluginSignat
     TSignature['optionalDependencies']
   >;
 
-type RemoveSetValue<Models extends Record<string, TreeViewModel<any>>> = {
-  [K in keyof Models]: Omit<Models[K], 'setValue'>;
-};
-
-export type TreeViewUsedModels<TSignature extends TreeViewAnyPluginSignature> =
-  TSignature['models'] &
-    RemoveSetValue<MergeSignaturesProperty<TreeViewRequiredPlugins<TSignature>, 'models'>>;
-
 export type TreeViewUsedEvents<TSignature extends TreeViewAnyPluginSignature> =
   TSignature['events'] & MergeSignaturesProperty<TreeViewRequiredPlugins<TSignature>, 'events'>;
 
@@ -211,11 +177,6 @@ export type TreeViewPlugin<TSignature extends TreeViewAnyPluginSignature> = {
    * @returns {TSignature['state']} The initial state of the plugin.
    */
   getInitialState?: (params: TreeViewUsedDefaultizedParams<TSignature>) => TSignature['state'];
-  /**
-   * The configuration of properties that can be controlled by the user.
-   * If they are not controlled, they will be initialized by the plugin.
-   */
-  models?: TreeViewModelsInitializer<TSignature>;
   /**
    * An object where each property used by the plugin is set to `true`.
    */
diff --git a/packages/x-tree-view/src/internals/models/treeView.ts b/packages/x-tree-view/src/internals/models/treeView.ts
index b2c13afe8da23..f505a56ec408f 100644
--- a/packages/x-tree-view/src/internals/models/treeView.ts
+++ b/packages/x-tree-view/src/internals/models/treeView.ts
@@ -18,12 +18,6 @@ export interface TreeViewItemMeta {
   label?: string;
 }
 
-export interface TreeViewModel<TValue> {
-  name: string;
-  value: TValue;
-  setControlledValue: (value: TValue | ((prevValue: TValue) => TValue)) => void;
-}
-
 export type TreeViewInstance<
   TSignatures extends readonly TreeViewAnyPluginSignature[],
   TOptionalSignatures extends readonly TreeViewAnyPluginSignature[] = [],
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts
index 1871b09fd45fb..8d666a9bc52ca 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.selectors.ts
@@ -1,3 +1,4 @@
+import { TreeViewItemId } from '../../../models';
 import { createSelector, TreeViewRootSelector } from '../../utils/selectors';
 import { selectorItemMeta } from '../useTreeViewItems/useTreeViewItems.selectors';
 import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types';
@@ -5,14 +6,38 @@ import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types';
 const selectorExpansion: TreeViewRootSelector<UseTreeViewExpansionSignature> = (state) =>
   state.expansion;
 
+/**
+ * Get the expanded items.
+ * @param {TreeViewState<[UseTreeViewExpansionSignature]>} state The state of the tree view.
+ * @returns {TreeViewItemId[]} The expanded items.
+ */
+export const selectorExpandedItems = createSelector(
+  [selectorExpansion],
+  (expansionState) => expansionState.expandedItems,
+);
+
+/**
+ * Get the expanded items as a Map.
+ * @param {TreeViewState<[UseTreeViewExpansionSignature]>} state The state of the tree view.
+ * @returns {TreeViewExpansionValue} The expanded items as a Map.
+ */
+export const selectorExpandedItemsMap = createSelector([selectorExpandedItems], (expandedItems) => {
+  const expandedItemsMap = new Map<TreeViewItemId, true>();
+  expandedItems.forEach((id) => {
+    expandedItemsMap.set(id, true);
+  });
+
+  return expandedItemsMap;
+});
+
 /**
  * Check if an item is expanded.
  * @param {TreeViewState<[UseTreeViewExpansionSignature]>} state The state of the tree view.
  * @returns {boolean} `true` if the item is expanded, `false` otherwise.
  */
 export const selectorIsItemExpanded = createSelector(
-  [selectorExpansion, (_, itemId: string) => itemId],
-  (expansionState, itemId) => expansionState.expandedItemsMap.has(itemId),
+  [selectorExpandedItemsMap, (_, itemId: string) => itemId],
+  (expandedItemsMap, itemId) => expandedItemsMap.has(itemId),
 );
 
 /**
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx
index d5fa5c51c36d8..a5e54a8063fef 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.test.tsx
@@ -122,7 +122,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>('useTreeViewExpansion plugin',
       expect(() => {
         view.setProps({ expandedItems: undefined });
       }).toErrorDev(
-        'MUI X: A component is changing the controlled expandedItems state of TreeView to be uncontrolled.',
+        'MUI X: A component is changing the controlled expandedItems state of Tree View to be uncontrolled.',
       );
     });
 
@@ -137,7 +137,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>('useTreeViewExpansion plugin',
         expect(view.isItemExpanded('1')).to.equal(true);
         expect(view.isItemExpanded('2')).to.equal(false);
       }).toErrorDev(
-        'MUI X: A component is changing the default expandedItems state of an uncontrolled TreeView after being initialized. To suppress this warning opt to use a controlled TreeView.',
+        'MUI X: A component is changing the default expandedItems state of an uncontrolled Tree View after being initialized. To suppress this warning opt to use a controlled Tree View.',
       );
     });
   });
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
index a29d28f7bd0e1..cabe75ea89728 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
@@ -7,29 +7,29 @@ import {
   UseTreeViewExpansionSignature,
 } from './useTreeViewExpansion.types';
 import { TreeViewItemId } from '../../../models';
-import { selectorIsItemExpandable, selectorIsItemExpanded } from './useTreeViewExpansion.selectors';
-import { createExpandedItemsMap, getExpansionTrigger } from './useTreeViewExpansion.utils';
+import {
+  selectorExpandedItems,
+  selectorIsItemExpandable,
+  selectorIsItemExpanded,
+} from './useTreeViewExpansion.selectors';
+import { getExpansionTrigger } from './useTreeViewExpansion.utils';
 import {
   selectorItemMeta,
   selectorItemOrderedChildrenIds,
 } from '../useTreeViewItems/useTreeViewItems.selectors';
 import { publishTreeViewEvent } from '../../utils/publishTreeViewEvent';
+import { useAssertModelConsistency } from '../../utils/models';
 
 export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature> = ({
   instance,
   store,
   params,
-  models,
 }) => {
-  useEnhancedEffect(() => {
-    store.update((prevState) => ({
-      ...prevState,
-      expansion: {
-        ...prevState.expansion,
-        expandedItemsMap: createExpandedItemsMap(models.expandedItems.value),
-      },
-    }));
-  }, [store, models.expandedItems.value]);
+  useAssertModelConsistency({
+    state: 'expandedItems',
+    controlled: params.expandedItems,
+    defaultValue: params.defaultExpandedItems,
+  });
 
   useEnhancedEffect(() => {
     store.update((prevState) => {
@@ -52,17 +52,23 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
   }, [store, params.isItemEditable, params.expansionTrigger]);
 
   const setExpandedItems = (event: React.SyntheticEvent | null, value: TreeViewItemId[]) => {
+    if (params.expandedItems === undefined) {
+      store.update((prevState) => ({
+        ...prevState,
+        expansion: { ...prevState.expansion, expandedItems: value },
+      }));
+    }
     params.onExpandedItemsChange?.(event, value);
-    models.expandedItems.setControlledValue(value);
   };
 
   const applyItemExpansion: UseTreeViewExpansionInstance['applyItemExpansion'] = useEventCallback(
     ({ itemId, event, shouldBeExpanded }) => {
+      const oldExpanded = selectorExpandedItems(store.value);
       let newExpanded: string[];
       if (shouldBeExpanded) {
-        newExpanded = [itemId].concat(models.expandedItems.value);
+        newExpanded = [itemId].concat(oldExpanded);
       } else {
-        newExpanded = models.expandedItems.value.filter((id) => id !== itemId);
+        newExpanded = oldExpanded.filter((id) => id !== itemId);
       }
 
       if (params.onItemExpansionToggle) {
@@ -109,7 +115,7 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
         selectorIsItemExpandable(store.value, child) && !selectorIsItemExpanded(store.value, child),
     );
 
-    const newExpanded = models.expandedItems.value.concat(diff);
+    const newExpanded = selectorExpandedItems(store.value).concat(diff);
 
     if (diff.length > 0) {
       if (params.onItemExpansionToggle) {
@@ -122,6 +128,19 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
     }
   };
 
+  /**
+   * Update the controlled model when the `expandedItems` prop changes.
+   */
+  useEnhancedEffect(() => {
+    const expandedItems = params.expandedItems;
+    if (expandedItems !== undefined) {
+      store.update((prevState) => ({
+        ...prevState,
+        expansion: { ...prevState.expansion, expandedItems },
+      }));
+    }
+  }, [store, params.expandedItems]);
+
   return {
     publicAPI: {
       setItemExpansion,
@@ -134,12 +153,6 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
   };
 };
 
-useTreeViewExpansion.models = {
-  expandedItems: {
-    getDefaultValue: (params) => params.defaultExpandedItems,
-  },
-};
-
 const DEFAULT_EXPANDED_ITEMS: string[] = [];
 
 useTreeViewExpansion.getDefaultizedParams = ({ params }) => ({
@@ -149,9 +162,8 @@ useTreeViewExpansion.getDefaultizedParams = ({ params }) => ({
 
 useTreeViewExpansion.getInitialState = (params) => ({
   expansion: {
-    expandedItemsMap: createExpandedItemsMap(
+    expandedItems:
       params.expandedItems === undefined ? params.defaultExpandedItems : params.expandedItems,
-    ),
     expansionTrigger: getExpansionTrigger(params),
   },
 });
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts
index 3fa0579d721c8..656c82a769241 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.types.ts
@@ -86,7 +86,7 @@ export type UseTreeViewExpansionDefaultizedParameters = DefaultizedProps<
 
 export interface UseTreeViewExpansionState {
   expansion: {
-    expandedItemsMap: Map<string, true>;
+    expandedItems: string[];
     expansionTrigger: 'content' | 'iconContainer';
   };
 }
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts
index 45ae73e5a6f82..8d120b501adbd 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.utils.ts
@@ -1,16 +1,6 @@
-import { TreeViewItemId } from '../../../models';
 import { TreeViewUsedDefaultizedParams } from '../../models';
 import { UseTreeViewExpansionSignature } from './useTreeViewExpansion.types';
 
-export const createExpandedItemsMap = (expandedItems: string[]) => {
-  const expandedItemsMap = new Map<TreeViewItemId, true>();
-  expandedItems.forEach((id) => {
-    expandedItemsMap.set(id, true);
-  });
-
-  return expandedItemsMap;
-};
-
 export const getExpansionTrigger = ({
   isItemEditable,
   expansionTrigger,
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts
index 4681cdc793d46..0a4718826811f 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.selectors.ts
@@ -1,5 +1,13 @@
 import { UseTreeViewFocusSignature } from './useTreeViewFocus.types';
 import { createSelector, TreeViewRootSelector } from '../../utils/selectors';
+import { selectorSelectionModelArray } from '../useTreeViewSelection/useTreeViewSelection.selectors';
+import {
+  selectorDisabledItemFocusable,
+  selectorItemMetaLookup,
+  selectorItemOrderedChildrenIds,
+} from '../useTreeViewItems/useTreeViewItems.selectors';
+import { isItemDisabled } from '../useTreeViewItems/useTreeViewItems.utils';
+import { selectorExpandedItemsMap } from '../useTreeViewExpansion/useTreeViewExpansion.selectors';
 
 const selectorTreeViewFocusState: TreeViewRootSelector<UseTreeViewFocusSignature> = (state) =>
   state.focus;
@@ -12,8 +20,37 @@ const selectorTreeViewFocusState: TreeViewRootSelector<UseTreeViewFocusSignature
  * @returns {TreeViewItemId | null} The id of the item that should be sequentially focusable.
  */
 export const selectorDefaultFocusableItemId = createSelector(
-  selectorTreeViewFocusState,
-  (focus) => focus.defaultFocusableItemId,
+  [
+    selectorSelectionModelArray,
+    selectorExpandedItemsMap,
+    selectorItemMetaLookup,
+    selectorDisabledItemFocusable,
+    (state) => selectorItemOrderedChildrenIds(state, null),
+  ],
+  (selectedItems, expandedItemsMap, itemMetaLookup, disabledItemsFocusable, orderedRootItemIds) => {
+    const firstSelectedItem = selectedItems.find((itemId) => {
+      if (!disabledItemsFocusable && isItemDisabled(itemMetaLookup, itemId)) {
+        return false;
+      }
+
+      const itemMeta = itemMetaLookup[itemId];
+      return itemMeta && (itemMeta.parentId == null || expandedItemsMap.has(itemMeta.parentId));
+    });
+
+    if (firstSelectedItem != null) {
+      return firstSelectedItem;
+    }
+
+    const firstNavigableItem = orderedRootItemIds.find(
+      (itemId) => disabledItemsFocusable || !isItemDisabled(itemMetaLookup, itemId),
+    );
+
+    if (firstNavigableItem != null) {
+      return firstNavigableItem;
+    }
+
+    return null;
+  },
 );
 
 /**
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
index 0ed28f4a63b2f..5d1e0a95267d9 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.ts
@@ -4,59 +4,19 @@ import { EventHandlers } from '@mui/utils/types';
 import { TreeViewPlugin } from '../../models';
 import { UseTreeViewFocusSignature } from './useTreeViewFocus.types';
 import { useInstanceEventHandler } from '../../hooks/useInstanceEventHandler';
-import { getFirstNavigableItem } from '../../utils/tree';
 import { TreeViewCancellableEvent } from '../../../models';
-import { convertSelectedItemsToArray } from '../useTreeViewSelection/useTreeViewSelection.utils';
 import {
   selectorDefaultFocusableItemId,
   selectorFocusedItemId,
 } from './useTreeViewFocus.selectors';
 import { selectorIsItemExpanded } from '../useTreeViewExpansion/useTreeViewExpansion.selectors';
-import {
-  selectorCanItemBeFocused,
-  selectorItemMeta,
-} from '../useTreeViewItems/useTreeViewItems.selectors';
+import { selectorItemMeta } from '../useTreeViewItems/useTreeViewItems.selectors';
 
 export const useTreeViewFocus: TreeViewPlugin<UseTreeViewFocusSignature> = ({
   instance,
   params,
   store,
-  models,
 }) => {
-  React.useEffect(() => {
-    let defaultFocusableItemId = convertSelectedItemsToArray(models.selectedItems.value).find(
-      (itemId) => {
-        if (!selectorCanItemBeFocused(store.value, itemId)) {
-          return false;
-        }
-
-        const itemMeta = selectorItemMeta(store.value, itemId);
-        return (
-          itemMeta &&
-          (itemMeta.parentId == null || selectorIsItemExpanded(store.value, itemMeta.parentId))
-        );
-      },
-    );
-
-    if (defaultFocusableItemId == null) {
-      defaultFocusableItemId = getFirstNavigableItem(store.value) ?? null;
-    }
-
-    store.update((prevState) => {
-      if (defaultFocusableItemId === prevState.focus.defaultFocusableItemId) {
-        return prevState;
-      }
-
-      return {
-        ...prevState,
-        focus: {
-          ...prevState.focus,
-          defaultFocusableItemId,
-        },
-      };
-    });
-  }, [store, models.selectedItems.value]);
-
   const setFocusedItemId = useEventCallback((itemId: string | null) => {
     store.update((prevState) => {
       const focusedItemId = selectorFocusedItemId(prevState);
@@ -166,7 +126,7 @@ export const useTreeViewFocus: TreeViewPlugin<UseTreeViewFocusSignature> = ({
 };
 
 useTreeViewFocus.getInitialState = () => ({
-  focus: { focusedItemId: null, defaultFocusableItemId: null },
+  focus: { focusedItemId: null },
 });
 
 useTreeViewFocus.params = {
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts
index 105ad6a364691..7958b86992494 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewFocus/useTreeViewFocus.types.ts
@@ -37,7 +37,6 @@ export type UseTreeViewFocusDefaultizedParameters = UseTreeViewFocusParameters;
 export interface UseTreeViewFocusState {
   focus: {
     focusedItemId: string | null;
-    defaultFocusableItemId: string | null;
   };
 }
 
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts
index 8a952932e4af9..0e3f6807c587f 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.selectors.ts
@@ -2,7 +2,7 @@ import { TreeViewItemId } from '../../../models';
 import { TreeViewItemMeta } from '../../models';
 import { createSelector, TreeViewRootSelector } from '../../utils/selectors';
 import { UseTreeViewItemsSignature } from './useTreeViewItems.types';
-import { TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils';
+import { isItemDisabled, TREE_VIEW_ROOT_PARENT_ID } from './useTreeViewItems.utils';
 
 const selectorTreeViewItemsState: TreeViewRootSelector<UseTreeViewItemsSignature> = (state) =>
   state.items;
@@ -82,31 +82,7 @@ export const selectorItemMeta = createSelector(
  */
 export const selectorIsItemDisabled = createSelector(
   [selectorItemMetaLookup, (_, itemId: string) => itemId],
-  (itemMetaLookup, itemId) => {
-    if (itemId == null) {
-      return false;
-    }
-
-    let itemMeta = itemMetaLookup[itemId];
-
-    // This can be called before the item has been added to the item map.
-    if (!itemMeta) {
-      return false;
-    }
-
-    if (itemMeta.disabled) {
-      return true;
-    }
-
-    while (itemMeta.parentId != null) {
-      itemMeta = itemMetaLookup[itemMeta.parentId];
-      if (itemMeta.disabled) {
-        return true;
-      }
-    }
-
-    return false;
-  },
+  isItemDisabled,
 );
 
 /**
@@ -150,13 +126,23 @@ export const selectorItemDepth = createSelector(
   (itemMeta) => itemMeta?.depth ?? 0,
 );
 
+/**
+ * Check if the disabled items are focusable.
+ * @param {TreeViewState<[UseTreeViewItemsSignature]>} state The state of the tree view.
+ * @returns {boolean} Whether the disabled items are focusable.
+ */
+export const selectorDisabledItemFocusable = createSelector(
+  [selectorTreeViewItemsState],
+  (itemsState) => itemsState.disabledItemsFocusable,
+);
+
 export const selectorCanItemBeFocused = createSelector(
-  [selectorTreeViewItemsState, selectorIsItemDisabled],
-  (itemsState, isItemDisabled) => {
-    if (itemsState.disabledItemsFocusable) {
+  [selectorDisabledItemFocusable, selectorIsItemDisabled],
+  (disabledItemsFocusable, isDisabled) => {
+    if (disabledItemsFocusable) {
       return true;
     }
 
-    return !isItemDisabled;
+    return !isDisabled;
   },
 );
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.utils.ts
index 176d1b5eb37d5..f55a175a6d02e 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.utils.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewItems/useTreeViewItems.utils.ts
@@ -1,3 +1,6 @@
+import { TreeViewItemId } from '../../../models';
+import { TreeViewItemMeta } from '../../models';
+
 export const TREE_VIEW_ROOT_PARENT_ID = '__TREE_VIEW_ROOT_PARENT_ID__';
 
 export const buildSiblingIndexes = (siblings: string[]) => {
@@ -8,3 +11,42 @@ export const buildSiblingIndexes = (siblings: string[]) => {
 
   return siblingsIndexLookup;
 };
+
+/**
+ * Check if an item is disabled.
+ * This method should only be used in selectors that are checking if several items are disabled.
+ * Otherwise, use the `selectorIsItemDisabled` selector.
+ * @returns
+ */
+export const isItemDisabled = (
+  itemMetaLookup: { [itemId: string]: TreeViewItemMeta },
+  itemId: TreeViewItemId,
+) => {
+  if (itemId == null) {
+    return false;
+  }
+
+  let itemMeta = itemMetaLookup[itemId];
+
+  // This can be called before the item has been added to the item map.
+  if (!itemMeta) {
+    return false;
+  }
+
+  if (itemMeta.disabled) {
+    return true;
+  }
+
+  while (itemMeta.parentId != null) {
+    itemMeta = itemMetaLookup[itemMeta.parentId];
+    if (!itemMeta) {
+      return false;
+    }
+
+    if (itemMeta.disabled) {
+      return true;
+    }
+  }
+
+  return false;
+};
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx
index 6e7424c88b596..78db72554a167 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewKeyboardNavigation/useTreeViewKeyboardNavigation.test.tsx
@@ -914,7 +914,7 @@ describeTreeView<
       });
 
       describe('key: Home', () => {
-        it('should select select the focused item and all the items above when Home is pressed while holding Shift + Ctrl', () => {
+        it('should select the focused item and all the items above when Home is pressed while holding Shift + Ctrl', () => {
           const view = render({
             items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }] }, { id: '3' }, { id: '4' }],
             multiSelect: true,
@@ -975,7 +975,7 @@ describeTreeView<
       });
 
       describe('key: End', () => {
-        it('should select select the focused item and all the items below when End is pressed while holding Shift + Ctrl', () => {
+        it('should select the focused item and all the items below when End is pressed while holding Shift + Ctrl', () => {
           const view = render({
             items: [{ id: '1' }, { id: '2', children: [{ id: '2.1' }] }, { id: '3' }, { id: '4' }],
             multiSelect: true,
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts
index 38b7bfd6927bb..fa5c688b204bd 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.selectors.ts
@@ -1,3 +1,4 @@
+import { TreeViewItemId } from '../../../models';
 import { createSelector, TreeViewRootSelector } from '../../utils/selectors';
 import { selectorIsItemDisabled } from '../useTreeViewItems/useTreeViewItems.selectors';
 import { UseTreeViewSelectionSignature } from './useTreeViewSelection.types';
@@ -6,14 +7,57 @@ const selectorTreeViewSelectionState: TreeViewRootSelector<UseTreeViewSelectionS
   state,
 ) => state.selection;
 
+/**
+ * Get the selected items.
+ * @param {TreeViewState<[UseTreeViewSelectionSignature]>} state The state of the tree view.
+ * @returns {TreeViewSelectionValue<boolean>} The selected items.
+ */
+export const selectorSelectionModel = createSelector(
+  [selectorTreeViewSelectionState],
+  (selectionState) => selectionState.selectedItems,
+);
+
+/**
+ * Get the selected items as an array.
+ * @param {TreeViewState<[UseTreeViewSelectionSignature]>} state The state of the tree view.
+ * @returns {TreeViewItemId[]} The selected items as an array.
+ */
+export const selectorSelectionModelArray = createSelector(
+  [selectorSelectionModel],
+  (selectedItems) => {
+    if (Array.isArray(selectedItems)) {
+      return selectedItems;
+    }
+
+    if (selectedItems != null) {
+      return [selectedItems];
+    }
+
+    return [];
+  },
+);
+
+/**
+ * Get the selected items as a map.
+ * @param {TreeViewState<[UseTreeViewSelectionSignature]>} state The state of the tree view.
+ * @returns {Map<TreeViewItemId, true>} The selected items as a Map.
+ */
+const selectorSelectionModelMap = createSelector([selectorSelectionModelArray], (selectedItems) => {
+  const selectedItemsMap = new Map<TreeViewItemId, true>();
+  selectedItems.forEach((id) => {
+    selectedItemsMap.set(id, true);
+  });
+  return selectedItemsMap;
+});
+
 /**
  * Check if an item is selected.
  * @param {TreeViewState<[UseTreeViewSelectionSignature]>} state The state of the tree view.
  * @returns {boolean} `true` if the item is selected, `false` otherwise.
  */
 export const selectorIsItemSelected = createSelector(
-  [selectorTreeViewSelectionState, (_, itemId: string) => itemId],
-  (selectionState, itemId) => selectionState.selectedItemsMap.has(itemId),
+  [selectorSelectionModelMap, (_, itemId: string) => itemId],
+  (selectedItemsMap, itemId) => selectedItemsMap.has(itemId),
 );
 
 /**
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx
index 9b458ebb3bfed..9bf2012fb2bf5 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.test.tsx
@@ -148,7 +148,7 @@ describeTreeView<[UseTreeViewSelectionSignature, UseTreeViewExpansionSignature]>
         expect(() => {
           view.setProps({ selectedItems: undefined });
         }).toErrorDev(
-          'MUI X: A component is changing the controlled selectedItems state of TreeView to be uncontrolled.',
+          'MUI X: A component is changing the controlled selectedItems state of Tree View to be uncontrolled.',
         );
       });
 
@@ -162,7 +162,7 @@ describeTreeView<[UseTreeViewSelectionSignature, UseTreeViewExpansionSignature]>
           view.setProps({ defaultSelectedItems: ['2'] });
           expect(view.getSelectedTreeItems()).to.deep.equal(['1']);
         }).toErrorDev(
-          'MUI X: A component is changing the default selectedItems state of an uncontrolled TreeView after being initialized. To suppress this warning opt to use a controlled TreeView.',
+          'MUI X: A component is changing the default selectedItems state of an uncontrolled Tree View after being initialized. To suppress this warning opt to use a controlled Tree View.',
         );
       });
     });
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
index 88c790e2d537e..57e661c2c921d 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.ts
@@ -10,29 +10,36 @@ import {
   getNonDisabledItemsInRange,
 } from '../../utils/tree';
 import {
+  TreeViewSelectionValue,
   UseTreeViewSelectionInstance,
   UseTreeViewSelectionParameters,
   UseTreeViewSelectionSignature,
 } from './useTreeViewSelection.types';
 import {
-  convertSelectedItemsToArray,
   propagateSelection,
   getAddedAndRemovedItems,
   getLookupFromArray,
-  createSelectedItemsMap,
 } from './useTreeViewSelection.utils';
 import {
   selectorIsItemSelected,
   selectorIsMultiSelectEnabled,
   selectorIsSelectionEnabled,
+  selectorSelectionModel,
+  selectorSelectionModelArray,
 } from './useTreeViewSelection.selectors';
 import { useTreeViewSelectionItemPlugin } from './useTreeViewSelection.itemPlugin';
+import { useAssertModelConsistency } from '../../utils/models';
 
 export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature> = ({
   store,
   params,
-  models,
 }) => {
+  useAssertModelConsistency({
+    state: 'selectedItems',
+    controlled: params.selectedItems,
+    defaultValue: params.defaultSelectedItems,
+  });
+
   const lastSelectedItem = React.useRef<string | null>(null);
   const lastSelectedRange = React.useRef<{ [itemId: string]: boolean }>({});
 
@@ -41,6 +48,7 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
     newModel: typeof params.defaultSelectedItems,
     additionalItemsToPropagate?: TreeViewItemId[],
   ) => {
+    const oldModel = selectorSelectionModel(store.value);
     let cleanModel: typeof newModel;
     const isMultiSelectEnabled = selectorIsMultiSelectEnabled(store.value);
 
@@ -52,7 +60,7 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
         store,
         selectionPropagation: params.selectionPropagation,
         newModel: newModel as string[],
-        oldModel: models.selectedItems.value as string[],
+        oldModel: oldModel as string[],
         additionalItemsToPropagate,
       });
     } else {
@@ -64,7 +72,7 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
         const changes = getAddedAndRemovedItems({
           store,
           newModel: cleanModel as string[],
-          oldModel: models.selectedItems.value as string[],
+          oldModel: oldModel as string[],
         });
 
         if (params.onItemSelectionToggle) {
@@ -76,9 +84,9 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
             params.onItemSelectionToggle!(event, itemId, false);
           });
         }
-      } else if (params.onItemSelectionToggle && cleanModel !== models.selectedItems.value) {
-        if (models.selectedItems.value != null) {
-          params.onItemSelectionToggle(event, models.selectedItems.value as string, false);
+      } else if (params.onItemSelectionToggle && cleanModel !== oldModel) {
+        if (oldModel != null) {
+          params.onItemSelectionToggle(event, oldModel as string, false);
         }
         if (cleanModel != null) {
           params.onItemSelectionToggle(event, cleanModel as string, true);
@@ -86,11 +94,14 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
       }
     }
 
-    if (params.onSelectedItemsChange) {
-      params.onSelectedItemsChange(event, cleanModel);
+    if (params.selectedItems === undefined) {
+      store.update((prevState) => ({
+        ...prevState,
+        selection: { ...prevState.selection, selectedItems: cleanModel },
+      }));
     }
 
-    models.selectedItems.setControlledValue(cleanModel);
+    params.onSelectedItemsChange?.(event, cleanModel);
   };
 
   const setItemSelection: UseTreeViewSelectionInstance['setItemSelection'] = ({
@@ -103,17 +114,17 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
       return;
     }
 
-    let newSelected: typeof models.selectedItems.value;
+    let newSelected: TreeViewSelectionValue<boolean>;
     const isMultiSelectEnabled = selectorIsMultiSelectEnabled(store.value);
     if (keepExistingSelection) {
-      const cleanSelectedItems = convertSelectedItemsToArray(models.selectedItems.value);
+      const oldSelected = selectorSelectionModelArray(store.value);
       const isSelectedBefore = selectorIsItemSelected(store.value, itemId);
       if (isSelectedBefore && (shouldBeSelected === false || shouldBeSelected == null)) {
-        newSelected = cleanSelectedItems.filter((id) => id !== itemId);
+        newSelected = oldSelected.filter((id) => id !== itemId);
       } else if (!isSelectedBefore && (shouldBeSelected === true || shouldBeSelected == null)) {
-        newSelected = [itemId].concat(cleanSelectedItems);
+        newSelected = [itemId].concat(oldSelected);
       } else {
-        newSelected = cleanSelectedItems;
+        newSelected = oldSelected;
       }
     } else {
       // eslint-disable-next-line no-lonely-if
@@ -144,7 +155,7 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
       return;
     }
 
-    let newSelectedItems = convertSelectedItemsToArray(models.selectedItems.value).slice();
+    let newSelectedItems = selectorSelectionModelArray(store.value).slice();
 
     // If the last selection was a range selection,
     // remove the items that were part of the last range from the model
@@ -199,7 +210,7 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
       return;
     }
 
-    let newSelectedItems = convertSelectedItemsToArray(models.selectedItems.value).slice();
+    let newSelectedItems = selectorSelectionModelArray(store.value).slice();
 
     if (Object.keys(lastSelectedRange.current).length === 0) {
       newSelectedItems.push(nextItem);
@@ -225,12 +236,10 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
     store.update((prevState) => ({
       ...prevState,
       selection: {
-        rawSelectedItems: models.selectedItems.value,
-        // Only re-compute the map when the model changes.
-        selectedItemsMap:
-          prevState.selection.rawSelectedItems === models.selectedItems.value
-            ? prevState.selection.selectedItemsMap
-            : createSelectedItemsMap(models.selectedItems.value),
+        selectedItems:
+          params.selectedItems === undefined
+            ? prevState.selection.selectedItems
+            : params.selectedItems,
         isEnabled: !params.disableSelection,
         isMultiSelectEnabled: params.multiSelect,
         isCheckboxSelectionEnabled: params.checkboxSelection,
@@ -242,7 +251,7 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
     }));
   }, [
     store,
-    models.selectedItems.value,
+    params.selectedItems,
     params.multiSelect,
     params.checkboxSelection,
     params.disableSelection,
@@ -270,12 +279,6 @@ export const useTreeViewSelection: TreeViewPlugin<UseTreeViewSelectionSignature>
 
 useTreeViewSelection.itemPlugin = useTreeViewSelectionItemPlugin;
 
-useTreeViewSelection.models = {
-  selectedItems: {
-    getDefaultValue: (params) => params.defaultSelectedItems,
-  },
-};
-
 const DEFAULT_SELECTED_ITEMS: string[] = [];
 
 const EMPTY_SELECTION_PROPAGATION: UseTreeViewSelectionParameters<true>['selectionPropagation'] =
@@ -291,21 +294,16 @@ useTreeViewSelection.getDefaultizedParams = ({ params }) => ({
   selectionPropagation: params.selectionPropagation ?? EMPTY_SELECTION_PROPAGATION,
 });
 
-useTreeViewSelection.getInitialState = (params) => {
-  const rawSelectedItems =
-    params.selectedItems === undefined ? params.defaultSelectedItems : params.selectedItems;
-
-  return {
-    selection: {
-      rawSelectedItems,
-      selectedItemsMap: createSelectedItemsMap(rawSelectedItems),
-      isEnabled: !params.disableSelection,
-      isMultiSelectEnabled: params.multiSelect,
-      isCheckboxSelectionEnabled: params.checkboxSelection,
-      selectionPropagation: params.selectionPropagation,
-    },
-  };
-};
+useTreeViewSelection.getInitialState = (params) => ({
+  selection: {
+    selectedItems:
+      params.selectedItems === undefined ? params.defaultSelectedItems : params.selectedItems,
+    isEnabled: !params.disableSelection,
+    isMultiSelectEnabled: params.multiSelect,
+    isCheckboxSelectionEnabled: params.checkboxSelection,
+    selectionPropagation: params.selectionPropagation,
+  },
+});
 
 useTreeViewSelection.params = {
   disableSelection: true,
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
index b93c14d2a16b8..509c00a0da4f9 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.types.ts
@@ -59,7 +59,7 @@ export interface UseTreeViewSelectionInstance extends UseTreeViewSelectionPublic
   ) => void;
 }
 
-type TreeViewSelectionValue<Multiple extends boolean | undefined> = Multiple extends true
+export type TreeViewSelectionValue<Multiple extends boolean | undefined> = Multiple extends true
   ? string[]
   : string | null;
 
@@ -141,8 +141,7 @@ export type UseTreeViewSelectionDefaultizedParameters<Multiple extends boolean>
 
 export interface UseTreeViewSelectionState {
   selection: {
-    rawSelectedItems: TreeViewSelectionValue<boolean>;
-    selectedItemsMap: Map<string, true>;
+    selectedItems: TreeViewSelectionValue<boolean>;
     isEnabled: boolean;
     isMultiSelectEnabled: boolean;
     isCheckboxSelectionEnabled: boolean;
@@ -155,7 +154,6 @@ export type UseTreeViewSelectionSignature = TreeViewPluginSignature<{
   defaultizedParams: UseTreeViewSelectionDefaultizedParameters<any>;
   instance: UseTreeViewSelectionInstance;
   publicAPI: UseTreeViewSelectionPublicAPI;
-  modelNames: 'selectedItems';
   state: UseTreeViewSelectionState;
   dependencies: [
     UseTreeViewItemsSignature,
diff --git a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts
index ed99d6f9fa75e..7b31486ae278e 100644
--- a/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts
+++ b/packages/x-tree-view/src/internals/plugins/useTreeViewSelection/useTreeViewSelection.utils.ts
@@ -7,31 +7,6 @@ import {
   selectorItemParentId,
 } from '../useTreeViewItems/useTreeViewItems.selectors';
 
-/**
- * Transform the `selectedItems` model to be an array if it was a string or null.
- * @param {string[] | string | null} model The raw model.
- * @returns {string[]} The converted model.
- */
-export const convertSelectedItemsToArray = (model: string[] | string | null): string[] => {
-  if (Array.isArray(model)) {
-    return model;
-  }
-
-  if (model != null) {
-    return [model];
-  }
-
-  return [];
-};
-
-export const createSelectedItemsMap = (selectedItems: string | string[] | null) => {
-  const selectedItemsMap = new Map<TreeViewItemId, true>();
-  convertSelectedItemsToArray(selectedItems).forEach((id) => {
-    selectedItemsMap.set(id, true);
-  });
-  return selectedItemsMap;
-};
-
 export const getLookupFromArray = (array: string[]) => {
   const lookup: { [itemId: string]: true } = {};
   array.forEach((itemId) => {
@@ -49,11 +24,14 @@ export const getAddedAndRemovedItems = ({
   oldModel: TreeViewItemId[];
   newModel: TreeViewItemId[];
 }) => {
-  const newModelLookup = createSelectedItemsMap(newModel);
+  const newModelMap = new Map<TreeViewItemId, true>();
+  newModel.forEach((id) => {
+    newModelMap.set(id, true);
+  });
 
   return {
     added: newModel.filter((itemId) => !selectorIsItemSelected(store.value, itemId)),
-    removed: oldModel.filter((itemId) => !newModelLookup.has(itemId)),
+    removed: oldModel.filter((itemId) => !newModelMap.has(itemId)),
   };
 };
 
diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
index 595fc028e8591..16dd2367b0e3d 100644
--- a/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
+++ b/packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
@@ -15,25 +15,28 @@ import {
   UseTreeViewReturnValue,
   UseTreeViewRootSlotProps,
 } from './useTreeView.types';
-import { useTreeViewModels } from './useTreeViewModels';
 import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins';
 import { extractPluginParamsFromProps } from './extractPluginParamsFromProps';
 import { useTreeViewBuildContext } from './useTreeViewBuildContext';
 import { TreeViewStore } from '../utils/TreeViewStore';
 
+function initializeInputApiRef<T>(inputApiRef: React.RefObject<T | undefined>) {
+  if (inputApiRef.current == null) {
+    inputApiRef.current = {} as T;
+  }
+  return inputApiRef as React.RefObject<T>;
+}
+
 export function useTreeViewApiInitialization<T>(
   inputApiRef: React.RefObject<T | undefined> | undefined,
-): T {
+): React.RefObject<T> {
   const fallbackPublicApiRef = React.useRef({}) as React.RefObject<T>;
 
   if (inputApiRef) {
-    if (inputApiRef.current == null) {
-      inputApiRef.current = {} as T;
-    }
-    return inputApiRef.current;
+    return initializeInputApiRef(inputApiRef);
   }
 
-  return fallbackPublicApiRef.current;
+  return fallbackPublicApiRef;
 }
 
 let globalId: number = 0;
@@ -74,7 +77,6 @@ export const useTreeView = <
       props,
     });
 
-  const models = useTreeViewModels<TSignatures>(plugins, pluginParams);
   const instanceRef = React.useRef({} as TreeViewInstance<TSignatures>);
   const instance = instanceRef.current as TreeViewInstance<TSignatures>;
   const publicAPI = useTreeViewApiInitialization<TreeViewPublicAPI<TSignatures>>(apiRef);
@@ -100,7 +102,7 @@ export const useTreeView = <
   const contextValue = useTreeViewBuildContext<TSignatures>({
     plugins,
     instance,
-    publicAPI,
+    publicAPI: publicAPI.current,
     store: storeRef.current as TreeViewStore<any>,
     rootRef: innerRootRef,
   });
@@ -115,7 +117,6 @@ export const useTreeView = <
       params: pluginParams,
       experimentalFeatures,
       rootRef: innerRootRef,
-      models,
       plugins,
       store: storeRef.current as TreeViewStore<any>,
     });
@@ -125,7 +126,7 @@ export const useTreeView = <
     }
 
     if (pluginResponse.publicAPI) {
-      Object.assign(publicAPI, pluginResponse.publicAPI);
+      Object.assign(publicAPI.current, pluginResponse.publicAPI);
     }
 
     if (pluginResponse.instance) {
diff --git a/packages/x-tree-view/src/internals/useTreeView/useTreeViewModels.ts b/packages/x-tree-view/src/internals/useTreeView/useTreeViewModels.ts
deleted file mode 100644
index 7c30b4d90cbf4..0000000000000
--- a/packages/x-tree-view/src/internals/useTreeView/useTreeViewModels.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import * as React from 'react';
-import {
-  TreeViewAnyPluginSignature,
-  ConvertSignaturesIntoPlugins,
-  MergeSignaturesProperty,
-  TreeViewPlugin,
-} from '../models';
-import { TreeViewCorePluginSignatures } from '../corePlugins';
-
-/**
- * Implements the same behavior as `useControlled` but for several models.
- * The controlled models are never stored in the state, and the state is only updated if the model is not controlled.
- */
-export const useTreeViewModels = <TSignatures extends readonly TreeViewAnyPluginSignature[]>(
-  plugins: ConvertSignaturesIntoPlugins<readonly [...TreeViewCorePluginSignatures, ...TSignatures]>,
-  props: MergeSignaturesProperty<TSignatures, 'defaultizedParams'>,
-) => {
-  type DefaultizedParams = MergeSignaturesProperty<TSignatures, 'defaultizedParams'>;
-
-  const modelsRef = React.useRef<{
-    [modelName: string]: {
-      getDefaultValue: (params: DefaultizedParams) => any;
-      isControlled: boolean;
-    };
-  }>({});
-
-  const [modelsState, setModelsState] = React.useState<{ [modelName: string]: any }>(() => {
-    const initialState: { [modelName: string]: any } = {};
-
-    plugins.forEach((plugin: TreeViewPlugin<TreeViewAnyPluginSignature>) => {
-      if (plugin.models) {
-        Object.entries(plugin.models).forEach(([modelName, modelInitializer]) => {
-          modelsRef.current[modelName] = {
-            isControlled: props[modelName as keyof DefaultizedParams] !== undefined,
-            getDefaultValue: modelInitializer.getDefaultValue,
-          };
-          initialState[modelName] = modelInitializer.getDefaultValue(props);
-        });
-      }
-    });
-
-    return initialState;
-  });
-
-  const models = Object.fromEntries(
-    Object.entries(modelsRef.current).map(([modelName, model]) => {
-      const value = props[modelName as keyof DefaultizedParams] ?? modelsState[modelName];
-
-      return [
-        modelName,
-        {
-          value,
-          setControlledValue: (newValue: any) => {
-            if (!model.isControlled) {
-              setModelsState((prevState) => ({
-                ...prevState,
-                [modelName]: newValue,
-              }));
-            }
-          },
-        },
-      ];
-    }),
-  ) as MergeSignaturesProperty<TSignatures, 'models'>;
-
-  // We know that `modelsRef` do not vary across renders.
-  /* eslint-disable react-hooks/rules-of-hooks, react-hooks/exhaustive-deps */
-  if (process.env.NODE_ENV !== 'production') {
-    Object.entries(modelsRef.current).forEach(([modelName, model]) => {
-      const controlled = props[modelName as keyof DefaultizedParams];
-      const newDefaultValue = model.getDefaultValue(props);
-
-      React.useEffect(() => {
-        if (model.isControlled !== (controlled !== undefined)) {
-          console.error(
-            [
-              `MUI X: A component is changing the ${
-                model.isControlled ? '' : 'un'
-              }controlled ${modelName} state of TreeView to be ${
-                model.isControlled ? 'un' : ''
-              }controlled.`,
-              'Elements should not switch from uncontrolled to controlled (or vice versa).',
-              `Decide between using a controlled or uncontrolled ${modelName} ` +
-                'element for the lifetime of the component.',
-              "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.",
-              'More info: https://fb.me/react-controlled-components',
-            ].join('\n'),
-          );
-        }
-      }, [controlled]);
-
-      const { current: defaultValue } = React.useRef(newDefaultValue);
-
-      React.useEffect(() => {
-        if (!model.isControlled && defaultValue !== newDefaultValue) {
-          console.error(
-            [
-              `MUI X: A component is changing the default ${modelName} state of an uncontrolled TreeView after being initialized. ` +
-                `To suppress this warning opt to use a controlled TreeView.`,
-            ].join('\n'),
-          );
-        }
-      }, [JSON.stringify(newDefaultValue)]);
-    });
-  }
-  /* eslint-enable react-hooks/rules-of-hooks, react-hooks/exhaustive-deps */
-
-  return models;
-};
diff --git a/packages/x-tree-view/src/internals/utils/models.ts b/packages/x-tree-view/src/internals/utils/models.ts
new file mode 100644
index 0000000000000..34677d10a38b3
--- /dev/null
+++ b/packages/x-tree-view/src/internals/utils/models.ts
@@ -0,0 +1,43 @@
+import { warnOnce } from '@mui/x-internals/warning';
+import * as React from 'react';
+
+function useAssertModelConsistencyOutsideOfProduction<T>(parameters: {
+  state: string;
+  controlled: T | undefined;
+  defaultValue: T;
+}) {
+  const { state, controlled, defaultValue } = parameters;
+  const [{ initialDefaultValue, isControlled }] = React.useState({
+    initialDefaultValue: defaultValue,
+    isControlled: controlled !== undefined,
+  });
+
+  if (isControlled !== (controlled !== undefined)) {
+    warnOnce(
+      [
+        `MUI X: A component is changing the ${
+          isControlled ? '' : 'un'
+        }controlled ${state} state of Tree View to be ${isControlled ? 'un' : ''}controlled.`,
+        'Elements should not switch from uncontrolled to controlled (or vice versa).',
+        `Decide between using a controlled or uncontrolled ${state} ` +
+          'element for the lifetime of the component.',
+        "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.",
+        'More info: https://fb.me/react-controlled-components',
+      ],
+      'error',
+    );
+  }
+
+  if (JSON.stringify(initialDefaultValue) !== JSON.stringify(defaultValue)) {
+    warnOnce(
+      [
+        `MUI X: A component is changing the default ${state} state of an uncontrolled Tree View after being initialized. ` +
+          `To suppress this warning opt to use a controlled Tree View.`,
+      ],
+      'error',
+    );
+  }
+}
+
+export const useAssertModelConsistency =
+  process.env.NODE_ENV === 'production' ? () => {} : useAssertModelConsistencyOutsideOfProduction;
diff --git a/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx b/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx
index 8009dd6e0a8cb..8bd9f4f3a089b 100644
--- a/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx
+++ b/packages/x-tree-view/src/useTreeItem/useTreeItem.test.tsx
@@ -98,6 +98,7 @@ describeTreeView<[UseTreeViewExpansionSignature]>(
       function ConditionallyMountedItem(props) {
         const [mounted, setMounted] = React.useState(true);
         if (props.itemId === '2') {
+          // eslint-disable-next-line react-compiler/react-compiler
           setActiveItemMounted = setMounted;
         }
 
diff --git a/test/utils/tree-view/fakeContextValue.ts b/test/utils/tree-view/fakeContextValue.ts
index b2ae9b9c37d4f..92c43afecb836 100644
--- a/test/utils/tree-view/fakeContextValue.ts
+++ b/test/utils/tree-view/fakeContextValue.ts
@@ -29,15 +29,14 @@ export const getFakeContextValue = (
       loading: false,
       error: null,
     },
-    expansion: { expandedItemsMap: new Map(), expansionTrigger: 'content' },
+    expansion: { expandedItems: [], expansionTrigger: 'content' },
     selection: {
-      selectedItemsMap: new Map(),
-      rawSelectedItems: null,
+      selectedItems: null,
       isEnabled: true,
       isMultiSelectEnabled: false,
       isCheckboxSelectionEnabled: features.checkboxSelection ?? false,
       selectionPropagation: { parents: false, descendants: false },
     },
-    focus: { focusedItemId: null, defaultFocusableItemId: null },
+    focus: { focusedItemId: null },
   }),
 });


Reviews:
user: login: alexfauquette
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-14T07:29:00Z
user: login: flaviendelangle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T08:14:34Z
user: login: flaviendelangle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T08:17:20Z
user: login: flaviendelangle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T08:18:09Z
user: login: flaviendelangle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T08:21:10Z
user: login: alexfauquette
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T12:55:30Z
user: login: flaviendelangle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T06:56:53Z
user: login: flaviendelangle
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T06:14:31Z

Review comments:
diff_hunk: @@ -146,9 +91,12 @@ The Tree View instance is an object accessible in all the plugins and in the Tre
 It is the main way a plugin can provide features to the rest of the component.
path: docs/data/tree-view/rich-tree-view/headless/headless.md
user: login: alexfauquette
body: On top of those lines you have a message that will  make no sens after removing the notion of model

> Once `focusedItemId` becomes a model
created_at: 2025-04-14T06:53:37Z
author_association: MEMBER
diff_hunk: @@ -171,10 +119,13 @@ const useOtherCustomPlugin = ({ models, instance }) => {
 ### Emit and receive events
 
 ```ts
-const useCustomPlugin = () => {
+const useCustomPlugin = ({ store }) => {
   const toggleCustomModel = () => {
-    const newValue = !models.customModel.value;
-    models.customModel.setValue(newValue);
+    const newValue = !selectorCustomModel(store.value);
+    store.update((prevState) => ({
+      ...prevState,
+      customModel: newValue,
path: docs/data/tree-view/rich-tree-view/headless/headless.md
user: login: alexfauquette
body: Personally, I woudl do it like that for simplidify

```suggestion
    store.update((prevState) => ({
      ...prevState,
      customModel: !prevState.customModel,
```

By the ways, you're mentioning "model' in you variable names
created_at: 2025-04-14T06:56:02Z
author_association: MEMBER
diff_hunk: @@ -122,6 +128,19 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
     }
   };
 
+  /**
+   * Update the controlled model when the `expandedItems` prop changes.
+   */
+  useEnhancedEffect(() => {
+    const expandedItems = params.expandedItems;
+    if (expandedItems !== undefined) {
+      store.update((prevState) => ({
+        ...prevState,
+        expansion: { ...prevState.expansion, expandedItems },
+      }));
+    }
+  }, [store, params.expandedItems]);
path: packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
user: login: alexfauquette
body: I find it more straightforward to have a file organisation like in the data grid with all the effects together and all the callback together. BUt that's just a matter of taste
created_at: 2025-04-14T07:01:35Z
author_association: MEMBER
diff_hunk: @@ -15,25 +15,28 @@ import {
   UseTreeViewReturnValue,
   UseTreeViewRootSlotProps,
 } from './useTreeView.types';
-import { useTreeViewModels } from './useTreeViewModels';
 import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins';
 import { extractPluginParamsFromProps } from './extractPluginParamsFromProps';
 import { useTreeViewBuildContext } from './useTreeViewBuildContext';
 import { TreeViewStore } from '../utils/TreeViewStore';
 
+function initializeInputApiRef<T>(inputApiRef: React.RefObject<T | undefined>) {
+  if (inputApiRef.current == null) {
+    inputApiRef.current = {} as T;
+  }
+  return inputApiRef.current;
+}
+
 export function useTreeViewApiInitialization<T>(
   inputApiRef: React.RefObject<T | undefined> | undefined,
 ): T {
-  const fallbackPublicApiRef = React.useRef({}) as React.RefObject<T>;
+  const [fallbackPublicApi] = React.useState({});
path: packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
user: login: alexfauquette
body: Why moving from a ref to a state?
created_at: 2025-04-14T07:23:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import { warnOnce } from '@mui/x-internals/warning';
+import * as React from 'react';
+
+function useAssertModelConsistencyOutsideOfProduction<T>(parameters: {
+  state: string;
+  controlled: T | undefined;
+  defaultValue: T;
+}) {
+  const { state, controlled, defaultValue } = parameters;
+  const [{ initialDefaultValue, isControlled }] = React.useState({
+    initialDefaultValue: defaultValue,
+    isControlled: controlled !== undefined,
+  });
+
+  if (isControlled !== (controlled !== undefined)) {
+    warnOnce(
+      [
+        `MUI X: A component is changing the ${
+          isControlled ? '' : 'un'
+        }controlled ${state} state of Tree View to be ${isControlled ? 'un' : ''}controlled.`,
path: packages/x-tree-view/src/internals/utils/models.ts
user: login: alexfauquette
body: If you remove `Tree view` form the message we could use them too in charts
created_at: 2025-04-14T07:24:32Z
author_association: MEMBER
diff_hunk: @@ -15,25 +15,28 @@ import {
   UseTreeViewReturnValue,
   UseTreeViewRootSlotProps,
 } from './useTreeView.types';
-import { useTreeViewModels } from './useTreeViewModels';
 import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins';
 import { extractPluginParamsFromProps } from './extractPluginParamsFromProps';
 import { useTreeViewBuildContext } from './useTreeViewBuildContext';
 import { TreeViewStore } from '../utils/TreeViewStore';
 
+function initializeInputApiRef<T>(inputApiRef: React.RefObject<T | undefined>) {
+  if (inputApiRef.current == null) {
+    inputApiRef.current = {} as T;
+  }
+  return inputApiRef.current;
+}
+
 export function useTreeViewApiInitialization<T>(
   inputApiRef: React.RefObject<T | undefined> | undefined,
 ): T {
-  const fallbackPublicApiRef = React.useRef({}) as React.RefObject<T>;
+  const [fallbackPublicApi] = React.useState({});
path: packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
user: login: flaviendelangle
body: ![image](https://github.com/user-attachments/assets/c040dbf7-f802-4a8d-9f4f-2dc5bea24387)

You skipped the lint here instead, I tried to find a way not to skip it
created_at: 2025-04-14T08:14:34Z
author_association: MEMBER
diff_hunk: @@ -171,10 +119,13 @@ const useOtherCustomPlugin = ({ models, instance }) => {
 ### Emit and receive events
 
 ```ts
-const useCustomPlugin = () => {
+const useCustomPlugin = ({ store }) => {
   const toggleCustomModel = () => {
-    const newValue = !models.customModel.value;
-    models.customModel.setValue(newValue);
+    const newValue = !selectorCustomModel(store.value);
+    store.update((prevState) => ({
+      ...prevState,
+      customModel: newValue,
path: docs/data/tree-view/rich-tree-view/headless/headless.md
user: login: flaviendelangle
body: The section emits an event, this is why I'm computing `newValue` outside of `store.update`, which makes the update heavier to read for sure
created_at: 2025-04-14T08:17:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import { warnOnce } from '@mui/x-internals/warning';
+import * as React from 'react';
+
+function useAssertModelConsistencyOutsideOfProduction<T>(parameters: {
+  state: string;
+  controlled: T | undefined;
+  defaultValue: T;
+}) {
+  const { state, controlled, defaultValue } = parameters;
+  const [{ initialDefaultValue, isControlled }] = React.useState({
+    initialDefaultValue: defaultValue,
+    isControlled: controlled !== undefined,
+  });
+
+  if (isControlled !== (controlled !== undefined)) {
+    warnOnce(
+      [
+        `MUI X: A component is changing the ${
+          isControlled ? '' : 'un'
+        }controlled ${state} state of Tree View to be ${isControlled ? 'un' : ''}controlled.`,
path: packages/x-tree-view/src/internals/utils/models.ts
user: login: flaviendelangle
body: I would add a `componentName` param
Having the name of the component in the error makes the debugging a lot easier imo
created_at: 2025-04-14T08:18:08Z
author_association: MEMBER
diff_hunk: @@ -122,6 +128,19 @@ export const useTreeViewExpansion: TreeViewPlugin<UseTreeViewExpansionSignature>
     }
   };
 
+  /**
+   * Update the controlled model when the `expandedItems` prop changes.
+   */
+  useEnhancedEffect(() => {
+    const expandedItems = params.expandedItems;
+    if (expandedItems !== undefined) {
+      store.update((prevState) => ({
+        ...prevState,
+        expansion: { ...prevState.expansion, expandedItems },
+      }));
+    }
+  }, [store, params.expandedItems]);
path: packages/x-tree-view/src/internals/plugins/useTreeViewExpansion/useTreeViewExpansion.ts
user: login: flaviendelangle
body: The plugins of the tree view are not that big for now, but maybe it's big enough the justify the split :ok_hand: 
created_at: 2025-04-14T08:21:10Z
author_association: MEMBER
diff_hunk: @@ -15,25 +15,28 @@ import {
   UseTreeViewReturnValue,
   UseTreeViewRootSlotProps,
 } from './useTreeView.types';
-import { useTreeViewModels } from './useTreeViewModels';
 import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins';
 import { extractPluginParamsFromProps } from './extractPluginParamsFromProps';
 import { useTreeViewBuildContext } from './useTreeViewBuildContext';
 import { TreeViewStore } from '../utils/TreeViewStore';
 
+function initializeInputApiRef<T>(inputApiRef: React.RefObject<T | undefined>) {
+  if (inputApiRef.current == null) {
+    inputApiRef.current = {} as T;
+  }
+  return inputApiRef.current;
+}
+
 export function useTreeViewApiInitialization<T>(
   inputApiRef: React.RefObject<T | undefined> | undefined,
 ): T {
-  const fallbackPublicApiRef = React.useRef({}) as React.RefObject<T>;
+  const [fallbackPublicApi] = React.useState({});
path: packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
user: login: alexfauquette
body: Not sure that removing a complier comment is a good reason to mutate a react state.

From what I understand, it complains because the compiler tedects that a custom hook returns a ref content, which can create non-pure functions.

A solution could be to have `useChartApiInitialization` return the ref and not the `ref.current`. Such that react compiler does not complain, and do the same stuff internally.

Here is my trial on the charts part

```diff
--- a/packages/x-charts/src/internals/store/useCharts.ts
+++ b/packages/x-charts/src/internals/store/useCharts.ts
@@ -96,7 +96,7 @@ export function useCharts<
     });
 
     if (pluginResponse.publicAPI) {
-      Object.assign(publicAPI, pluginResponse.publicAPI);
+      Object.assign(publicAPI.current, pluginResponse.publicAPI);
     }
 
     if (pluginResponse.instance) {
@@ -110,7 +110,7 @@ export function useCharts<
     () => ({
       store: storeRef.current as ChartStore<TSignaturesWithCorePluginSignatures> &
         UseChartInteractionState,
-      publicAPI,
+      publicAPI: publicAPI.current,
       instance,
       svgRef: innerSvgRef,
     }),
@@ -120,18 +120,21 @@ export function useCharts<
   return { contextValue };
 }
 
+function initializeInputApiRef<T>(inputApiRef: React.RefObject<T | undefined>) {
+  if (inputApiRef.current == null) {
+    inputApiRef.current = {} as T;
+  }
+  return inputApiRef as React.RefObject<T>;
+}
+
 export function useChartApiInitialization<T>(
   inputApiRef: React.RefObject<T | undefined> | undefined,
-): T {
+): React.RefObject<T> {
   const fallbackPublicApiRef = React.useRef({}) as React.RefObject<T>;
 
   if (inputApiRef) {
-    if (inputApiRef.current == null) {
-      // eslint-disable-next-line react-compiler/react-compiler
-      inputApiRef.current = {} as T;
-    }
-    return inputApiRef.current;
+    return initializeInputApiRef(inputApiRef);
   }
 
-  return fallbackPublicApiRef.current;
+  return fallbackPublicApiRef;
 }
```
created_at: 2025-04-14T12:55:30Z
author_association: MEMBER
diff_hunk: @@ -15,25 +15,28 @@ import {
   UseTreeViewReturnValue,
   UseTreeViewRootSlotProps,
 } from './useTreeView.types';
-import { useTreeViewModels } from './useTreeViewModels';
 import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins';
 import { extractPluginParamsFromProps } from './extractPluginParamsFromProps';
 import { useTreeViewBuildContext } from './useTreeViewBuildContext';
 import { TreeViewStore } from '../utils/TreeViewStore';
 
+function initializeInputApiRef<T>(inputApiRef: React.RefObject<T | undefined>) {
+  if (inputApiRef.current == null) {
+    inputApiRef.current = {} as T;
+  }
+  return inputApiRef.current;
+}
+
 export function useTreeViewApiInitialization<T>(
   inputApiRef: React.RefObject<T | undefined> | undefined,
 ): T {
-  const fallbackPublicApiRef = React.useRef({}) as React.RefObject<T>;
+  const [fallbackPublicApi] = React.useState({});
path: packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
user: login: flaviendelangle
body: The approach looks a lot better :ok_hand: 
created_at: 2025-04-15T06:56:53Z
author_association: MEMBER
diff_hunk: @@ -15,25 +15,28 @@ import {
   UseTreeViewReturnValue,
   UseTreeViewRootSlotProps,
 } from './useTreeView.types';
-import { useTreeViewModels } from './useTreeViewModels';
 import { TREE_VIEW_CORE_PLUGINS, TreeViewCorePluginSignatures } from '../corePlugins';
 import { extractPluginParamsFromProps } from './extractPluginParamsFromProps';
 import { useTreeViewBuildContext } from './useTreeViewBuildContext';
 import { TreeViewStore } from '../utils/TreeViewStore';
 
+function initializeInputApiRef<T>(inputApiRef: React.RefObject<T | undefined>) {
+  if (inputApiRef.current == null) {
+    inputApiRef.current = {} as T;
+  }
+  return inputApiRef.current;
+}
+
 export function useTreeViewApiInitialization<T>(
   inputApiRef: React.RefObject<T | undefined> | undefined,
 ): T {
-  const fallbackPublicApiRef = React.useRef({}) as React.RefObject<T>;
+  const [fallbackPublicApi] = React.useState({});
path: packages/x-tree-view/src/internals/useTreeView/useTreeView.ts
user: login: flaviendelangle
body: Done
created_at: 2025-04-16T06:14:31Z
author_association: MEMBER

PR comments:
user: login: mui-bot
created_at: 2025-01-27T16:53:04Z
body: 
<!--
  0 failure: 
  0 warning: 
  
  2 markdown notices
  DangerID: danger-id-Danger;
-->



**Deploy preview:** <a href="https://deploy-preview-16357--material-ui-x.netlify.app/">https://deploy-preview-16357--material-ui-x.netlify.app/</a>



**Updated pages:**

- [docs/data/tree-view/rich-tree-view/headless/headless.md](https://deploy-preview-16357--material-ui-x.netlify.app/x/react-tree-view/rich-tree-view/headless/)

<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against b68a12288ee301827bedcc49afdccf43d5efd254
</p>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-03T10:58:33Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-20T09:23:14Z
body: Thanks for adding a type label to the PR! 👍
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T14:34:39Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-03T15:04:45Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-14T05:58:20Z
body: This pull request has conflicts, please resolve those before we can evaluate the pull request.
author_association: CONTRIBUTOR