Repository: mui/material-ui (TypeScript) — Bug Fix

Issue:
[TextField] "Maximum update depth exceeded." in  Internet Explorer 11
App crashes with "Maximum update depth exceeded" triggered by filling a TextField on Internet Explorer 11

- [x] The issue is present in the latest release.
- [x] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Current Behavior 😯

When typing text in a Textfield, the app crashed when all visible space is filled up.
Only observed in IE11

## Expected Behavior 🤔

The initial visible rows of the textField should grow until reaching rowsMax.
Works as expected in Safari and Chrome

## Steps to Reproduce 🕹
https://stackblitz.com/edit/react-ts-6luqab

Steps:

1. The Textfield need the following parameters rows, rowsMax, fullWidth, multiline
2. Type text in textfield until crashing (fill all visible space)

## Your Environment 🌎
| Tech        | Version |
| ----------- | ------- |
| Material-UI | v4.5.0  |
| React       |   16.10.0      |
| React-Dom       |   16.10.0      |
| Browser     |   Internet Explorer 11  (tested in Browserstack)    |



Discussion:
I can confirm this behavior. I will try to look into this 😄 Thank you for pointing this out.
In this context, I would propose that we put protection in place to limit the number of rerenders, we don't want to see the UI crash because of a layout instability:

```diff
diff --git a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
index d7040ddc1..52550922a 100644
--- a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
+++ b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
@@ -33,6 +33,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
   const inputRef = React.useRef(null);
   const handleRef = useForkRef(ref, inputRef);
   const shadowRef = React.useRef(null);
+  const renders = React.useRef(0);
   const [state, setState] = React.useState({});

   const syncHeight = React.useCallback(() => {
@@ -76,10 +77,12 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
       // Need a large enough different to update the height.
       // This prevents infinite rendering loop.
       if (
-        (outerHeightStyle > 0 &&
+        renders.current < 10 &&
+        ((outerHeightStyle > 0 &&
           Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
-        prevState.overflow !== overflow
+          prevState.overflow !== overflow)
       ) {
+        renders.current += 1;
         return {
           overflow,
           outerHeightStyle,
@@ -91,6 +94,8 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
   }, [rows, rowsMax, props.placeholder]);

   React.useEffect(() => {
+    renders.current = 0;
+
     const handleResize = debounce(() => {
       syncHeight();
     });
@@ -107,6 +112,8 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
   });

   const handleChange = event => {
+    renders.current = 0;
+
     if (!isControlled) {
       syncHeight();
     }
```

Maybe, we would want to trigger a warning and a test case too.
@adeelibr If you don't mind I can pick it up
@SerhiiBilyk sure go ahead :)
@adeelibr unfortunately I'm unable to run IE on my laptop :( So feel free to pick it up
I tried version 4.7.2 on Chrome 79.0.3945.130 on Windows 10 and reproduced this issue.
Seems to be better when adding "box-sizing: border-box" to the .inputbar-input class.

Hi, I'll be working on this if that's OK with you guys.

Resolving patch:
diff --git a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
--- a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
+++ b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
@@ -35,6 +35,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
   const inputRef = React.useRef(null);
   const handleRef = useForkRef(ref, inputRef);
   const shadowRef = React.useRef(null);
+  const renders = React.useRef(0);
   const [state, setState] = React.useState({});
 
   const syncHeight = React.useCallback(() => {
@@ -75,25 +76,39 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
     const overflow = Math.abs(outerHeight - innerHeight) <= 1;
 
     setState(prevState => {
-      // Need a large enough different to update the height.
+      // Need a large enough difference to update the height.
       // This prevents infinite rendering loop.
       if (
-        (outerHeightStyle > 0 &&
+        renders.current < 20 &&
+        ((outerHeightStyle > 0 &&
           Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
-        prevState.overflow !== overflow
+          prevState.overflow !== overflow)
       ) {
+        renders.current += 1;
         return {
           overflow,
           outerHeightStyle,
         };
       }
 
+      if (process.env.NODE_ENV !== 'production') {
+        if (renders.current === 20) {
+          console.error(
+            [
+              'Material-UI: too many re-renders. The layout is unstable.',
+              'TextareaAutosize limits the number of renders to prevent an infinite loop.',
+            ].join('\n'),
+          );
+        }
+      }
+
       return prevState;
     });
   }, [rowsMax, rowsMin, props.placeholder]);
 
   React.useEffect(() => {
     const handleResize = debounce(() => {
+      renders.current = 0;
       syncHeight();
     });
 
@@ -108,7 +123,13 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
     syncHeight();
   });
 
+  React.useEffect(() => {
+    renders.current = 0;
+  }, [value]);
+
   const handleChange = event => {
+    renders.current = 0;
+
     if (!isControlled) {
       syncHeight();
     }
@@ -128,7 +149,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
         rows={rowsMin}
         style={{
           height: state.outerHeightStyle,
-          // Need a large enough different to allow scrolling.
+          // Need a large enough difference to allow scrolling.
           // This prevents infinite rendering loop.
           overflow: state.overflow ? 'hidden' : null,
           ...style,


Test patch:
diff --git a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js
--- a/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js
+++ b/packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js
@@ -3,6 +3,7 @@ import { assert } from 'chai';
 import sinon, { spy, stub, useFakeTimers } from 'sinon';
 import { createMount } from '@material-ui/core/test-utils';
 import describeConformance from '@material-ui/core/test-utils/describeConformance';
+import consoleErrorMock from 'test/utils/consoleErrorMock';
 import TextareaAutosize from './TextareaAutosize';
 
 function getStyle(wrapper) {
@@ -38,7 +39,9 @@ describe('<TextareaAutosize />', () => {
 
     const getComputedStyleStub = {};
 
-    function setLayout(wrapper, { getComputedStyle, scrollHeight, lineHeight }) {
+    function setLayout(wrapper, { getComputedStyle, scrollHeight, lineHeight: lineHeightArg }) {
+      const lineHeight = typeof lineHeightArg === 'function' ? lineHeightArg : () => lineHeightArg;
+
       const input = wrapper
         .find('textarea')
         .at(0)
@@ -53,7 +56,7 @@ describe('<TextareaAutosize />', () => {
       let index = 0;
       stub(shadow, 'scrollHeight').get(() => {
         index += 1;
-        return index % 2 === 1 ? scrollHeight : lineHeight;
+        return index % 2 === 1 ? scrollHeight : lineHeight();
       });
     }
 
@@ -237,5 +240,35 @@ describe('<TextareaAutosize />', () => {
       wrapper.update();
       assert.deepEqual(getStyle(wrapper), { height: lineHeight * 2, overflow: null });
     });
+
+    describe('warnings', () => {
+      before(() => {
+        consoleErrorMock.spy();
+      });
+
+      after(() => {
+        consoleErrorMock.reset();
+      });
+
+      it('warns if layout is unstable but not crash', () => {
+        const wrapper = mount(<TextareaAutosize rowsMax={3} />);
+        let index = 0;
+        setLayout(wrapper, {
+          getComputedStyle: {
+            'box-sizing': 'content-box',
+          },
+          scrollHeight: 100,
+          lineHeight: () => {
+            index += 1;
+            return 15 + index;
+          },
+        });
+        wrapper.setProps();
+        wrapper.update();
+
+        assert.strictEqual(consoleErrorMock.callCount(), 3);
+        assert.include(consoleErrorMock.args()[0][0], 'Material-UI: too many re-renders.');
+      });
+    });
   });
 });