Skip to content

Commit 55254ef

Browse files
authored
Ensure we reset renderCount (#5017)
* Golf some bytes and ensure we reset renderCount * Apply suggestion from @JoviDeCroock * Add test
1 parent b341bfe commit 55254ef

File tree

2 files changed

+80
-20
lines changed

2 files changed

+80
-20
lines changed

src/component.js

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -217,28 +217,31 @@ const depthSort = (a, b) => a._vnode._depth - b._vnode._depth;
217217

218218
/** Flush the render queue by rerendering all queued components */
219219
function process() {
220-
let c,
221-
l = 1;
222-
223-
// Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary
224-
// process() calls from getting scheduled while `queue` is still being consumed.
225-
while (rerenderQueue.length) {
226-
// Keep the rerender queue sorted by (depth, insertion order). The queue
227-
// will initially be sorted on the first iteration only if it has more than 1 item.
228-
//
229-
// New items can be added to the queue e.g. when rerendering a provider, so we want to
230-
// keep the order from top to bottom with those new items so we can handle them in a
231-
// single pass
232-
if (rerenderQueue.length > l) {
233-
rerenderQueue.sort(depthSort);
234-
}
220+
try {
221+
let c,
222+
l = 1;
223+
224+
// Don't update `renderCount` yet. Keep its value non-zero to prevent unnecessary
225+
// process() calls from getting scheduled while `queue` is still being consumed.
226+
while (rerenderQueue.length) {
227+
// Keep the rerender queue sorted by (depth, insertion order). The queue
228+
// will initially be sorted on the first iteration only if it has more than 1 item.
229+
//
230+
// New items can be added to the queue e.g. when rerendering a provider, so we want to
231+
// keep the order from top to bottom with those new items so we can handle them in a
232+
// single pass
233+
if (rerenderQueue.length > l) {
234+
rerenderQueue.sort(depthSort);
235+
}
235236

236-
c = rerenderQueue.shift();
237-
l = rerenderQueue.length;
237+
c = rerenderQueue.shift();
238+
l = rerenderQueue.length;
238239

239-
renderComponent(c);
240+
renderComponent(c);
241+
}
242+
} finally {
243+
rerenderQueue.length = process._rerenderCount = 0;
240244
}
241-
process._rerenderCount = 0;
242245
}
243246

244247
process._rerenderCount = 0;

test/browser/components.test.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createElement, render, Component, Fragment } from 'preact';
1+
import { createElement, render, Component, Fragment, options } from 'preact';
22
import { setupRerender } from 'preact/test-utils';
33
import {
44
setupScratch,
@@ -2731,4 +2731,61 @@ describe('Components', () => {
27312731
expect(scratch.innerHTML).to.equal('<div>Updated: yes</div>');
27322732
});
27332733
});
2734+
2735+
it('should reset the rerender queue if rendering throws', () => {
2736+
let shouldThrow = false;
2737+
let increment;
2738+
let debounceCount = 0;
2739+
let flush;
2740+
2741+
// Use a custom debounceRendering so we can track whether
2742+
// enqueueRender actually schedules a new process() call.
2743+
const prevDebounce = options.debounceRendering;
2744+
options.debounceRendering = cb => {
2745+
debounceCount++;
2746+
flush = cb;
2747+
};
2748+
2749+
class App extends Component {
2750+
constructor(props) {
2751+
super(props);
2752+
this.state = { count: 0 };
2753+
increment = () => this.setState(s => ({ count: s.count + 1 }));
2754+
}
2755+
render(props, state) {
2756+
if (shouldThrow) throw new Error('test error');
2757+
return <div>{state.count}</div>;
2758+
}
2759+
}
2760+
2761+
render(<App />, scratch);
2762+
expect(scratch.innerHTML).to.equal('<div>0</div>');
2763+
2764+
// First setState to sync prevDebounce inside enqueueRender
2765+
increment();
2766+
expect(debounceCount).to.equal(1);
2767+
flush();
2768+
expect(scratch.innerHTML).to.equal('<div>1</div>');
2769+
2770+
// Trigger a rerender that will throw
2771+
debounceCount = 0;
2772+
shouldThrow = true;
2773+
increment();
2774+
expect(debounceCount).to.equal(1);
2775+
expect(() => flush()).to.throw();
2776+
2777+
// After the error, a subsequent setState must schedule a new
2778+
// process() via debounceRendering. Without the fix,
2779+
// _rerenderCount stays non-zero and enqueueRender won't
2780+
// call debounceRendering, leaving the component stuck.
2781+
debounceCount = 0;
2782+
shouldThrow = false;
2783+
increment();
2784+
expect(debounceCount).to.equal(1);
2785+
2786+
flush();
2787+
expect(scratch.innerHTML).to.equal('<div>3</div>');
2788+
2789+
options.debounceRendering = prevDebounce;
2790+
});
27342791
});

0 commit comments

Comments
 (0)