From 613cacc0e3e9bb8b7547f26fea4061d54797e690 Mon Sep 17 00:00:00 2001 From: Ryan Christian <33403762+rschristian@users.noreply.github.com> Date: Fri, 26 Apr 2024 01:29:31 -0500 Subject: [PATCH] feat: debug throw on too many rerenders (#4349) * [Compat] Throw an error for too many repeated function component rerenders * refactor: Move limiter to debug * revert: Unrelated changes --------- Co-authored-by: Andre Wiggins --- debug/src/debug.js | 20 ++++++++++++++++++++ debug/test/browser/debug.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/debug/src/debug.js b/debug/src/debug.js index e06c430151..45cc67c3e5 100644 --- a/debug/src/debug.js +++ b/debug/src/debug.js @@ -247,11 +247,31 @@ export function initDebug() { if (oldBeforeDiff) oldBeforeDiff(vnode); }; + let renderCount = 0; + let currentComponent; options._render = vnode => { if (oldRender) { oldRender(vnode); } hooksAllowed = true; + + const nextComponent = vnode._component; + if (nextComponent === currentComponent) { + renderCount++; + } else { + renderCount = 1; + } + + if (renderCount >= 25) { + throw new Error( + `Too many re-renders. This is limited to prevent an infinite loop ` + + `which may lock up your browser. The component causing this is: ${getDisplayName( + vnode + )}` + ); + } + + currentComponent = nextComponent; }; options._hook = (comp, index, type) => { diff --git a/debug/test/browser/debug.test.js b/debug/test/browser/debug.test.js index 54be87578a..cd4b4d9bd8 100644 --- a/debug/test/browser/debug.test.js +++ b/debug/test/browser/debug.test.js @@ -1,4 +1,5 @@ import { createElement, render, createRef, Component, Fragment } from 'preact'; +import { useState } from 'preact/hooks'; import { setupScratch, teardown, @@ -271,6 +272,37 @@ describe('debug', () => { expect(console.error).to.not.be.called; }); + 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); + }); + describe('duplicate keys', () => { const List = props => ; const ListItem = props =>
  • {props.children}
  • ;