Skip to content

Commit 41bd38c

Browse files
committed
Debounce state updates in the test viewer
The test viewer would update 100s of times per second sometimes, which would make the editor hang. We now debounce renders. I also fixed handling when there is an error in jest, if you fix the error the editor doesn't clear the red squiggles.
1 parent 45346e4 commit 41bd38c

File tree

3 files changed

+106
-42
lines changed

3 files changed

+106
-42
lines changed

packages/app/src/app/components/Preview/DevTools/Tests/TestDetails/index.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,22 @@ export const TestDetails = ({ file, status, openFile, runTests }: Props) => {
6262
)}
6363
<Action>
6464
<Tooltip content="Open File">
65-
<FileIcon onClick={() => openFile(file.fileName)} />
65+
<FileIcon
66+
onClick={e => {
67+
e.stopPropagation();
68+
openFile(file.fileName);
69+
}}
70+
/>
6671
</Tooltip>
6772
</Action>
6873
<Action>
6974
<Tooltip content="Run Tests">
70-
<PlayIcon onClick={() => runTests(file)} />
75+
<PlayIcon
76+
onClick={e => {
77+
e.stopPropagation();
78+
runTests(file);
79+
}}
80+
/>
7181
</Tooltip>
7282
</Action>
7383
</TestTitle>

packages/app/src/app/components/Preview/DevTools/Tests/index.tsx

+91-40
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ const INITIAL_STATE = {
161161

162162
class Tests extends React.Component<DevToolProps, State> {
163163
state = INITIAL_STATE;
164+
draftState: State | null = null;
164165

165166
listener: () => void;
166167

@@ -176,13 +177,54 @@ class Tests extends React.Component<DevToolProps, State> {
176177
}
177178
}
178179

180+
setStateTimer = null;
181+
/**
182+
* We can call setState 100s of times per second, which puts great strain
183+
* on rendering from React. We debounce the rendering so that we flush changes
184+
* after a while. This prevents the editor from getting stuck.
185+
*
186+
* Every setState call will have to go through this, otherwise we get race conditions
187+
* where the underlying state has changed, but the draftState didn't change.
188+
*/
189+
setStateDebounced = (setStateFunc, time = 200) => {
190+
const draftState = this.draftState || this.state;
191+
192+
const newState =
193+
typeof setStateFunc === 'function'
194+
? setStateFunc(draftState, this.props)
195+
: setStateFunc;
196+
this.draftState = { ...draftState, ...newState };
197+
198+
if (this.setStateTimer) {
199+
clearTimeout(this.setStateTimer);
200+
}
201+
202+
const updateFunc = () => {
203+
if (this.draftState) {
204+
this.setState(this.draftState);
205+
}
206+
207+
this.draftState = null;
208+
this.setStateTimer = null;
209+
};
210+
211+
if (time === 0) {
212+
updateFunc();
213+
} else {
214+
this.setStateTimer = window.setTimeout(updateFunc, time);
215+
}
216+
};
217+
179218
UNSAFE_componentWillReceiveProps(nextProps: DevToolProps) {
180219
if (nextProps.sandboxId !== this.props.sandboxId) {
181-
this.setState({
182-
files: {},
183-
selectedFilePath: null,
184-
running: true,
185-
});
220+
this.setStateDebounced(
221+
{
222+
files: {},
223+
selectedFilePath: null,
224+
running: true,
225+
},
226+
0
227+
);
186228
}
187229

188230
if (this.props.hidden && !nextProps.hidden) {
@@ -191,19 +233,24 @@ class Tests extends React.Component<DevToolProps, State> {
191233
}
192234

193235
selectFile = (file: File) => {
194-
this.setState(state => ({
195-
selectedFilePath:
196-
file.fileName === state.selectedFilePath ? null : file.fileName,
197-
}));
236+
this.setStateDebounced(
237+
state => ({
238+
selectedFilePath:
239+
file.fileName === state.selectedFilePath ? null : file.fileName,
240+
}),
241+
0
242+
);
198243
};
199244

200245
toggleFileExpansion = (file: File) => {
201-
this.setState(oldState =>
202-
immer(oldState, state => {
203-
state.fileExpansionState[file.fileName] = !state.fileExpansionState[
204-
file.fileName
205-
];
206-
})
246+
this.setStateDebounced(
247+
oldState =>
248+
immer(oldState, state => {
249+
state.fileExpansionState[file.fileName] = !state.fileExpansionState[
250+
file.fileName
251+
];
252+
}),
253+
0
207254
);
208255
};
209256

@@ -231,7 +278,7 @@ class Tests extends React.Component<DevToolProps, State> {
231278
if (this.props.updateStatus) {
232279
this.props.updateStatus('clear');
233280
}
234-
this.setState(INITIAL_STATE);
281+
this.setStateDebounced(INITIAL_STATE, 0);
235282
break;
236283
}
237284
case 'test_count': {
@@ -247,15 +294,21 @@ class Tests extends React.Component<DevToolProps, State> {
247294
if (this.props.updateStatus) {
248295
this.props.updateStatus('clear');
249296
}
250-
this.setState({
251-
running: true,
252-
});
297+
this.setStateDebounced(
298+
{
299+
running: true,
300+
},
301+
0
302+
);
253303
break;
254304
}
255305
case messages.TOTAL_TEST_END: {
256-
this.setState({
257-
running: false,
258-
});
306+
this.setStateDebounced(
307+
{
308+
running: false,
309+
},
310+
0
311+
);
259312

260313
const files = Object.keys(this.state.files);
261314
const failingTests = files.filter(
@@ -280,7 +333,7 @@ class Tests extends React.Component<DevToolProps, State> {
280333
}
281334

282335
case messages.ADD_FILE: {
283-
this.setState(oldState =>
336+
this.setStateDebounced(oldState =>
284337
immer(oldState, state => {
285338
state.files[data.path] = {
286339
tests: {},
@@ -293,7 +346,7 @@ class Tests extends React.Component<DevToolProps, State> {
293346
break;
294347
}
295348
case 'remove_file': {
296-
this.setState(oldState =>
349+
this.setStateDebounced(oldState =>
297350
immer(oldState, state => {
298351
if (state.files[data.path]) {
299352
delete state.files[data.path];
@@ -305,7 +358,7 @@ class Tests extends React.Component<DevToolProps, State> {
305358
break;
306359
}
307360
case messages.FILE_ERROR: {
308-
this.setState(oldState =>
361+
this.setStateDebounced(oldState =>
309362
immer(oldState, state => {
310363
if (state.files[data.path]) {
311364
state.files[data.path].fileError = data.error;
@@ -325,7 +378,7 @@ class Tests extends React.Component<DevToolProps, State> {
325378
case messages.ADD_TEST: {
326379
const testName = [...this.currentDescribeBlocks, data.testName];
327380

328-
this.setState(oldState =>
381+
this.setStateDebounced(oldState =>
329382
immer(oldState, state => {
330383
if (!state.files[data.path]) {
331384
state.files[data.path] = {
@@ -351,7 +404,7 @@ class Tests extends React.Component<DevToolProps, State> {
351404
const { test } = data;
352405
const testName = [...test.blocks, test.name];
353406

354-
this.setState(oldState =>
407+
this.setStateDebounced(oldState =>
355408
immer(oldState, state => {
356409
if (!state.files[test.path]) {
357410
state.files[test.path] = {
@@ -382,7 +435,7 @@ class Tests extends React.Component<DevToolProps, State> {
382435
const { test } = data;
383436
const testName = [...test.blocks, test.name];
384437

385-
this.setState(oldState =>
438+
this.setStateDebounced(oldState =>
386439
immer(oldState, state => {
387440
if (!state.files[test.path]) {
388441
return;
@@ -471,36 +524,34 @@ class Tests extends React.Component<DevToolProps, State> {
471524
};
472525

473526
toggleWatching = () => {
527+
this.setStateDebounced(state => ({ watching: !state.watching }), 0);
474528
dispatch({
475529
type: 'set-test-watching',
476530
watching: !this.state.watching,
477531
});
478-
this.setState(state => ({ watching: !state.watching }));
479532
};
480533

481534
runAllTests = () => {
482-
this.setState({ files: {} }, () => {
483-
dispatch({
484-
type: 'run-all-tests',
485-
});
535+
this.setStateDebounced({ files: {} }, 0);
536+
dispatch({
537+
type: 'run-all-tests',
486538
});
487539
};
488540

489541
runTests = (file: File) => {
490-
this.setState(
542+
this.setStateDebounced(
491543
oldState =>
492544
immer(oldState, state => {
493545
if (state.files[file.fileName]) {
494546
state.files[file.fileName].tests = {};
495547
}
496548
}),
497-
() => {
498-
dispatch({
499-
type: 'run-tests',
500-
path: file.fileName,
501-
});
502-
}
549+
0
503550
);
551+
dispatch({
552+
type: 'run-tests',
553+
path: file.fileName,
554+
});
504555
};
505556

506557
openFile = (path: string) => {

packages/app/src/app/overmind/namespaces/editor/actions.ts

+3
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,8 @@ export const prettifyClicked: AsyncAction = async ({
583583
});
584584
};
585585

586+
// TODO(@CompuIves): Look into whether we even want to call this function.
587+
// We can probably call the dispatch from the bundler itself instead.
586588
export const errorsCleared: Action = ({ state, effects }) => {
587589
const sandbox = state.editor.currentSandbox;
588590
if (!sandbox) {
@@ -604,6 +606,7 @@ export const errorsCleared: Action = ({ state, effects }) => {
604606
}
605607
});
606608
state.editor.errors = [];
609+
effects.vscode.setErrors(state.editor.errors);
607610
}
608611
};
609612

0 commit comments

Comments
 (0)