Repository: mui/material-ui (TypeScript)
Date: 2024-03-25 20:06:28+00:00
Task type: Feature

Issue:
restore component state in data grid when switching tabs
### Order ID or Support key 💳 (optional)

66023

### Duplicates

- [X] I have searched the existing issues

### Latest version

- [X] I have tested the latest version

### The problem in depth 🔍

If I use a TabPanel component that has a DataGridPro component as a child, then the state is not preserved when I switch tabs.

If the user changes a column width, this tweak is lost when they switch tabs.  I believe that I can do save and restore state using the API, but I would expect a simpler way of managing this, perhaps by keeping the TabPanel mounted even if it is not showing ?

### Your environment 🌎

<details>
  <summary>`npx @mui/envinfo`</summary>

```
  System:
    OS: Linux 6.2 Fedora Linux 36 (Thirty Six)
  Binaries:
    Node: 18.14.0 - /run/user/1001/fnm_multishells/173521_1689808956027/bin/node
    Yarn: 1.22.19 - /run/user/1001/fnm_multishells/173521_1689808956027/bin/yarn
    npm: 9.3.1 - /run/user/1001/fnm_multishells/173521_1689808956027/bin/npm
  Browsers:
    Chrome: Version 114.0.5735.199 (Official Build) (64-bit)
```
</details>



Issue discussion:
You need to change the `display` style to `none` of the TabPanel. In that way, it will kept mounted but not visible and the DataGrid won't lose its state.
Changing `display` to `none` would require me to manage the tab switching behaviour myself, obviating the usage of the TabPanel component.  I am suggesting that the behaviour of the TabPanel should be what keeps my components mounted using whatever means necessary.

@romgrk could you let me know what more information you need from me ?
Isn't this a duplicate of https://github.com/mui/material-ui/issues/37398?
Running into this problem as well. I have data grid autosizing options turned on but switching tabs removes all the formatting. So switching back to a previously opened tab shrinks all the columns. I'm also not quite sure about the use of `display: "none"`. Doing so seems to hide the entire panel content. 

Solution patch:
diff --git a/docs/pages/material-ui/api/tab-panel.json b/docs/pages/material-ui/api/tab-panel.json
--- a/docs/pages/material-ui/api/tab-panel.json
+++ b/docs/pages/material-ui/api/tab-panel.json
@@ -3,6 +3,7 @@
     "value": { "type": { "name": "string" }, "required": true },
     "children": { "type": { "name": "node" } },
     "classes": { "type": { "name": "object" }, "additionalInfo": { "cssApi": true } },
+    "keepMounted": { "type": { "name": "bool" }, "default": "false" },
     "sx": {
       "type": {
         "name": "union",
@@ -14,6 +15,12 @@
   "name": "TabPanel",
   "imports": ["import TabPanel from '@mui/lab/TabPanel';", "import { TabPanel } from '@mui/lab';"],
   "classes": [
+    {
+      "key": "hidden",
+      "className": "MuiTabPanel-hidden",
+      "description": "State class applied to the root `div` element if `hidden={true}`.",
+      "isGlobal": false
+    },
     {
       "key": "root",
       "className": "MuiTabPanel-root",
diff --git a/docs/translations/api-docs/tab-panel/tab-panel.json b/docs/translations/api-docs/tab-panel/tab-panel.json
--- a/docs/translations/api-docs/tab-panel/tab-panel.json
+++ b/docs/translations/api-docs/tab-panel/tab-panel.json
@@ -3,6 +3,7 @@
   "propDescriptions": {
     "children": { "description": "The content of the component." },
     "classes": { "description": "Override or extend the styles applied to the component." },
+    "keepMounted": { "description": "Always keep the children in the DOM." },
     "sx": {
       "description": "The system prop that allows defining system overrides as well as additional CSS styles."
     },
@@ -10,5 +11,12 @@
       "description": "The <code>value</code> of the corresponding <code>Tab</code>. Must use the index of the <code>Tab</code> when no <code>value</code> was passed to <code>Tab</code>."
     }
   },
-  "classDescriptions": { "root": { "description": "Styles applied to the root element." } }
+  "classDescriptions": {
+    "hidden": {
+      "description": "State class applied to {{nodeName}} if {{conditions}}.",
+      "nodeName": "the root <code>div</code> element",
+      "conditions": "<code>hidden={true}</code>"
+    },
+    "root": { "description": "Styles applied to the root element." }
+  }
 }
diff --git a/packages/mui-lab/src/TabPanel/TabPanel.d.ts b/packages/mui-lab/src/TabPanel/TabPanel.d.ts
--- a/packages/mui-lab/src/TabPanel/TabPanel.d.ts
+++ b/packages/mui-lab/src/TabPanel/TabPanel.d.ts
@@ -22,6 +22,11 @@ export interface TabPanelProps extends StandardProps<React.HTMLAttributes<HTMLDi
    * no `value` was passed to `Tab`.
    */
   value: string;
+  /**
+   * Always keep the children in the DOM.
+   * @default false
+   */
+  keepMounted?: boolean;
 }
 
 /**
diff --git a/packages/mui-lab/src/TabPanel/TabPanel.js b/packages/mui-lab/src/TabPanel/TabPanel.js
--- a/packages/mui-lab/src/TabPanel/TabPanel.js
+++ b/packages/mui-lab/src/TabPanel/TabPanel.js
@@ -8,10 +8,10 @@ import { getTabPanelUtilityClass } from './tabPanelClasses';
 import { getPanelId, getTabId, useTabContext } from '../TabContext';
 
 const useUtilityClasses = (ownerState) => {
-  const { classes } = ownerState;
+  const { classes, hidden } = ownerState;
 
   const slots = {
-    root: ['root'],
+    root: ['root', hidden && 'hidden'],
   };
 
   return composeClasses(slots, getTabPanelUtilityClass, classes);
@@ -28,7 +28,7 @@ const TabPanelRoot = styled('div', {
 const TabPanel = React.forwardRef(function TabPanel(inProps, ref) {
   const props = useThemeProps({ props: inProps, name: 'MuiTabPanel' });
 
-  const { children, className, value, ...other } = props;
+  const { children, className, value, keepMounted = false, ...other } = props;
 
   const ownerState = {
     ...props,
@@ -54,7 +54,7 @@ const TabPanel = React.forwardRef(function TabPanel(inProps, ref) {
       ownerState={ownerState}
       {...other}
     >
-      {value === context.value && children}
+      {(keepMounted || value === context.value) && children}
     </TabPanelRoot>
   );
 });
@@ -76,6 +76,11 @@ TabPanel.propTypes /* remove-proptypes */ = {
    * @ignore
    */
   className: PropTypes.string,
+  /**
+   * Always keep the children in the DOM.
+   * @default false
+   */
+  keepMounted: PropTypes.bool,
   /**
    * The system prop that allows defining system overrides as well as additional CSS styles.
    */
diff --git a/packages/mui-lab/src/TabPanel/tabPanelClasses.ts b/packages/mui-lab/src/TabPanel/tabPanelClasses.ts
--- a/packages/mui-lab/src/TabPanel/tabPanelClasses.ts
+++ b/packages/mui-lab/src/TabPanel/tabPanelClasses.ts
@@ -4,6 +4,8 @@ import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
 export interface TabPanelClasses {
   /** Styles applied to the root element. */
   root: string;
+  /** State class applied to the root `div` element if `hidden={true}`. */
+  hidden: string;
 }
 
 export type TabPanelClassKey = keyof TabPanelClasses;
@@ -12,6 +14,6 @@ export function getTabPanelUtilityClass(slot: string): string {
   return generateUtilityClass('MuiTabPanel', slot);
 }
 
-const tabPanelClasses: TabPanelClasses = generateUtilityClasses('MuiTabPanel', ['root']);
+const tabPanelClasses: TabPanelClasses = generateUtilityClasses('MuiTabPanel', ['root', 'hidden']);
 
 export default tabPanelClasses;


Test patch:
diff --git a/packages/mui-lab/src/TabPanel/TabPanel.test.tsx b/packages/mui-lab/src/TabPanel/TabPanel.test.tsx
--- a/packages/mui-lab/src/TabPanel/TabPanel.test.tsx
+++ b/packages/mui-lab/src/TabPanel/TabPanel.test.tsx
@@ -24,14 +24,17 @@ describe('<TabPanel />', () => {
     ],
   }));
 
-  it('renders a [role="tabpanel"]', () => {
-    const { getByTestId } = render(
+  it('renders a [role="tabpanel"] and mounts children', () => {
+    const { getByTestId, queryByTestId } = render(
       <TabContext value="0">
-        <TabPanel data-testid="tabpanel" value="0" />
+        <TabPanel data-testid="tabpanel" value="0">
+          <div data-testid="child" />
+        </TabPanel>
       </TabContext>,
     );
 
     expect(getByTestId('tabpanel')).to.have.attribute('role', 'tabpanel');
+    expect(queryByTestId('child')).to.not.equal(null);
   });
 
   it('is [hidden] when TabPanel#value !== TabContext#value and does not mount children', () => {
@@ -47,6 +50,19 @@ describe('<TabPanel />', () => {
     expect(queryByTestId('child')).to.equal(null);
   });
 
+  it('is [hidden] when TabPanel#value !== TabContext#value but does mount children when keepMounted', () => {
+    const { getByTestId, queryByTestId } = render(
+      <TabContext value="1">
+        <TabPanel data-testid="tabpanel" value="0" keepMounted>
+          <div data-testid="child" />
+        </TabPanel>
+      </TabContext>,
+    );
+
+    expect(getByTestId('tabpanel')).to.have.property('hidden', true);
+    expect(queryByTestId('child')).to.not.equal(null);
+  });
+
   it('is accessible when TabPanel#value === TabContext#value', () => {
     const { getByTestId } = render(
       <TabContext value="0">