Repository: facebook/react
Pull request: [playground] Fix useEffect on tabify

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`.



Changed files:
filename: compiler/apps/playground/components/Editor/Output.tsx
patch: @@ -19,19 +19,24 @@ import {
 import parserBabel from 'prettier/plugins/babel';
 import * as prettierPluginEstree from 'prettier/plugins/estree';
 import * as prettier from 'prettier/standalone';
-import {memo, ReactNode, useEffect, useState} from 'react';
 import {type Store} from '../../lib/stores';
+import {memo, ReactNode, use, useState, Suspense} from 'react';
 import AccordionWindow from '../AccordionWindow';
 import TabbedWindow from '../TabbedWindow';
 import {monacoOptions} from './monacoOptions';
 import {BabelFileResult} from '@babel/core';
+import {LRUCache} from 'lru-cache';
 
 const MemoizedOutput = memo(Output);
 
 export default MemoizedOutput;
 
 export const BASIC_OUTPUT_TAB_NAMES = ['Output', 'SourceMap'];
 
+const tabifyCache = new LRUCache<Store, Promise<Map<string, ReactNode>>>({
+  max: 5,
+});
+
 export type PrintedCompilerPipelineValue =
   | {
       kind: 'hir';
@@ -200,6 +205,25 @@ ${code}
   return reorderedTabs;
 }
 
+function tabifyCached(
+  store: Store,
+  compilerOutput: CompilerOutput,
+): Promise<Map<string, ReactNode>> {
+  const cached = tabifyCache.get(store);
+  if (cached) return cached;
+  const result = tabify(store.source, compilerOutput, store.showInternals);
+  tabifyCache.set(store, result);
+  return result;
+}
+
+function Fallback(): JSX.Element {
+  return (
+    <div className="w-full h-monaco_small sm:h-monaco flex items-center justify-center">
+      Loading...
+    </div>
+  );
+}
+
 function utf16ToUTF8(s: string): string {
   return unescape(encodeURIComponent(s));
 }
@@ -213,12 +237,17 @@ function getSourceMapUrl(code: string, map: string): string | null {
 }
 
 function Output({store, compilerOutput}: Props): JSX.Element {
+  return (
+    <Suspense fallback={<Fallback />}>
+      <OutputContent store={store} compilerOutput={compilerOutput} />
+    </Suspense>
+  );
+}
+
+function OutputContent({store, compilerOutput}: Props): JSX.Element {
   const [tabsOpen, setTabsOpen] = useState<Set<string>>(
     () => new Set(['Output']),
   );
-  const [tabs, setTabs] = useState<Map<string, React.ReactNode>>(
-    () => new Map(),
-  );
   const [activeTab, setActiveTab] = useState<string>('Output');
 
   /*
@@ -233,13 +262,6 @@ function Output({store, compilerOutput}: Props): JSX.Element {
     setTabsOpen(new Set(['Output']));
     setActiveTab('Output');
   }
-
-  useEffect(() => {
-    tabify(store.source, compilerOutput, store.showInternals).then(tabs => {
-      setTabs(tabs);
-    });
-  }, [store.source, compilerOutput, store.showInternals]);
-
   const changedPasses: Set<string> = new Set(['Output', 'HIR']); // Initial and final passes should always be bold
   let lastResult: string = '';
   for (const [passName, results] of compilerOutput.results) {
@@ -254,6 +276,7 @@ function Output({store, compilerOutput}: Props): JSX.Element {
       lastResult = currResult;
     }
   }
+  const tabs = use(tabifyCached(store, compilerOutput));
 
   if (!store.showInternals) {
     return (

filename: compiler/apps/playground/package.json
patch: @@ -32,6 +32,7 @@
     "hermes-eslint": "^0.25.0",
     "hermes-parser": "^0.25.0",
     "invariant": "^2.2.4",
+    "lru-cache": "^11.2.2",
     "lz-string": "^1.5.0",
     "monaco-editor": "^0.52.0",
     "next": "15.6.0-canary.7",

filename: compiler/apps/playground/yarn.lock
patch: @@ -3104,6 +3104,11 @@ lru-cache@^10.2.0:
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
   integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
 
+lru-cache@^11.2.2:
+  version "11.2.2"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24"
+  integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==
+
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"