[playground] Fix useEffect on tabify (#34594)
There was a bug in the Compiler Playground related to the "Show Internals" toggle due to a useEffect that was causing the tab names to flicker from a rerender. Rewritten instead with a `<Suspense>` boundary + `use`.
Eugene Choi committed
Sep 25, 2025 at 14:56 UTC
250f1b20e0ac8e6c1ba03f2466ad63ff9b5104de
3 files changed
+40
-11
compiler/apps/playground/components/Editor/Output.tsx
+34
-11
@@ -19,12 +19,13 @@ import {
19
import parserBabel from 'prettier/plugins/babel';
20
import * as prettierPluginEstree from 'prettier/plugins/estree';
21
import * as prettier from 'prettier/standalone';
22
-import {memo, ReactNode, useEffect, useState} from 'react';
22
import {type Store} from '../../lib/stores';
23
+import {memo, ReactNode, use, useState, Suspense} from 'react';
24
import AccordionWindow from '../AccordionWindow';
25
import TabbedWindow from '../TabbedWindow';
26
import {monacoOptions} from './monacoOptions';
27
import {BabelFileResult} from '@babel/core';
28
+import {LRUCache} from 'lru-cache';
29
30
const MemoizedOutput = memo(Output);
31
@@ -32,6 +33,10 @@ export default MemoizedOutput;
33
34
export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
35
36
+const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
37
+ max: 5,
38
+});
39
+
40
export type PrintedCompilerPipelineValue =
41
| {
42
kind: 'hir';
@@ -200,6 +205,25 @@ ${code}
205
return reorderedTabs;
206
}
207
208
+function tabifyCached(
209
+ store: Store,
210
+ compilerOutput: CompilerOutput,
211
+): Promise<Map<string, ReactNode>> {
212
+ const cached = tabifyCache.get(store);
213
+ if (cached) return cached;
214
+ const result = tabify(store.source, compilerOutput, store.showInternals);
215
+ tabifyCache.set(store, result);
216
+ return result;
217
+}
218
+
219
+function Fallback(): JSX.Element {
220
+ return (
221
+ <div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
222
+ Loading...
223
+ </div>
224
+ );
225
+}
226
+
227
function utf16ToUTF8(s: string): string {
228
return unescape(encodeURIComponent(s));
229
}
@@ -213,12 +237,17 @@ function getSourceMapUrl(code: string, map: string): string | null {
237
}
238
239
function Output({store, compilerOutput}: Props): JSX.Element {
240
+ return (
241
+ <Suspense fallback={<Fallback />}>
242
+ <OutputContent store={store} compilerOutput={compilerOutput} />
243
+ </Suspense>
244
+ );
245
+}
246
+
247
+function OutputContent({store, compilerOutput}: Props): JSX.Element {
248
const [tabsOpen, setTabsOpen] = useState<Set<string>>(
249
() => new Set(['Output']),
250
);
219
- const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
220
- () => new Map(),
221
- );
251
const [activeTab, setActiveTab] = useState<string>('Output');
252
253
/*
@@ -233,13 +262,6 @@ function Output({store, compilerOutput}: Props): JSX.Element {
262
setTabsOpen(new Set(['Output']));
263
setActiveTab('Output');
264
}
236
-
237
- useEffect(() => {
238
- tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
239
- setTabs(tabs);
240
- });
241
- }, [store.source, compilerOutput, store.showInternals]);
242
-
265
const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
266
let lastResult: string = '';
267
for (const [passName, results] of compilerOutput.results) {
@@ -254,6 +276,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
276
lastResult = currResult;
277
}
278
}
279
+ const tabs = use(tabifyCached(store, compilerOutput));
280
281
if (!store.showInternals) {
282
return (
compiler/apps/playground/package.json
+1
@@ -32,6 +32,7 @@
32
"hermes-eslint": "^0.25.0",
33
"hermes-parser": "^0.25.0",
34
"invariant": "^2.2.4",
35
+ "lru-cache": "^11.2.2",
36
"lz-string": "^1.5.0",
37
"monaco-editor": "^0.52.0",
38
"next": "15.6.0-canary.7",
compiler/apps/playground/yarn.lock
+5
@@ -3104,6 +3104,11 @@ lru-cache@^10.2.0:
3104
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
3105
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
3106
3107
+lru-cache@^11.2.2:
3108
+ version "11.2.2"
3109
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24"
3110
+ integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==
3111
+
3112
lru-cache@^5.1.1:
3113
version "5.1.1"
3114
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"