Skip to content

Commit

Permalink
[TextareaAutosize] Prevent "Maximum update depth exceeded" (#19743)
Browse files Browse the repository at this point in the history
  • Loading branch information
SofianeDjellouli authored Feb 20, 2020
1 parent a5a3785 commit 4d1bf80
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 6 deletions.
29 changes: 25 additions & 4 deletions packages/material-ui/src/TextareaAutosize/TextareaAutosize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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();
});

Expand All @@ -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();
}
Expand All @@ -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,
Expand Down
37 changes: 35 additions & 2 deletions packages/material-ui/src/TextareaAutosize/TextareaAutosize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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();
});
}

Expand Down Expand Up @@ -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.');
});
});
});
});

0 comments on commit 4d1bf80

Please sign in to comment.