diff --git a/compat/src/render.js b/compat/src/render.js index 41db9aa689..bbd1416109 100644 --- a/compat/src/render.js +++ b/compat/src/render.js @@ -261,14 +261,28 @@ options.vnode = vnode => { if (oldVNodeHook) oldVNodeHook(vnode); }; -// Only needed for react-relay +let renderCount = 0; let currentComponent; const oldBeforeRender = options._render; options._render = function (vnode) { if (oldBeforeRender) { oldBeforeRender(vnode); } - currentComponent = vnode._component; + + const nextComponent = vnode._component; + if (nextComponent === currentComponent) { + renderCount++; + } else { + renderCount = 1; + } + + if (renderCount >= 25) { + throw new Error( + `Too many re-renders. Preact/compat limits the number of renders to prevent an infinite loop.` + ); + } + + currentComponent = nextComponent; }; const oldDiffed = options.diffed; diff --git a/compat/test/browser/render.test.js b/compat/test/browser/render.test.js index 0bfb021be7..12c83cb379 100644 --- a/compat/test/browser/render.test.js +++ b/compat/test/browser/render.test.js @@ -3,7 +3,9 @@ import React, { render, Component, hydrate, - createContext + createContext, + useState, + Fragment } from 'preact/compat'; import { setupRerender, act } from 'preact/test-utils'; import { @@ -512,4 +514,35 @@ describe('compat render', () => { expect(scratch.textContent).to.equal('foo'); }); + + it('throws an error if a component rerenders too many times', () => { + let rerenderCount = 0; + function TestComponent({ loop = false }) { + const [count, setCount] = useState(0); + if (loop) { + setCount(count + 1); + } + + if (count > 30) { + expect.fail( + 'Repeated rerenders did not cause the expected error. This test is failing.' + ); + } + + rerenderCount += 1; + return
; + } + + expect(() => { + render( + + + + , + scratch + ); + }).to.throw(/Too many re-renders/); + // 1 for first TestComponent + 24 for second TestComponent + expect(rerenderCount).to.equal(25); + }); });