From f53733525c3fe31eca4d9ba6c3a9367c1e88de59 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 8 Aug 2024 13:16:38 +0200 Subject: [PATCH] Remove all `useEffect` usages (#12659) --- playground/.gitignore | 3 + playground/README.md | 2 +- playground/src/Editor/Chrome.tsx | 123 +++++++++++++++ playground/src/Editor/Editor.tsx | 206 ++++++++----------------- playground/src/Editor/SourceEditor.tsx | 2 +- playground/src/Editor/theme.ts | 32 ++-- playground/src/main.tsx | 8 +- 7 files changed, 208 insertions(+), 168 deletions(-) create mode 100644 playground/src/Editor/Chrome.tsx diff --git a/playground/.gitignore b/playground/.gitignore index c6bba59138121..e30d2eac55839 100644 --- a/playground/.gitignore +++ b/playground/.gitignore @@ -128,3 +128,6 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# Wrangler +api/.wrangler diff --git a/playground/README.md b/playground/README.md index 082b7a9f15984..3b7c0f394a627 100644 --- a/playground/README.md +++ b/playground/README.md @@ -12,7 +12,7 @@ Finally, install TypeScript dependencies with `npm install`, and run the develop To run the datastore, which is based on [Workers KV](https://developers.cloudflare.com/workers/runtime-apis/kv/), install the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/), -then run `npx wrangler dev --local` from the `./playground/db` directory. Note that the datastore is +then run `npx wrangler dev --local` from the `./playground/api` directory. Note that the datastore is only required to generate shareable URLs for code snippets. The development datastore does not require Cloudflare authentication or login, but in turn only persists data locally. diff --git a/playground/src/Editor/Chrome.tsx b/playground/src/Editor/Chrome.tsx new file mode 100644 index 0000000000000..b97a1b5f008c8 --- /dev/null +++ b/playground/src/Editor/Chrome.tsx @@ -0,0 +1,123 @@ +import { useCallback, useMemo, useRef, useState } from "react"; +import Header from "./Header"; +import { persist, persistLocal, restore, stringify } from "./settings"; +import { useTheme } from "./theme"; +import { default as Editor, Source } from "./Editor"; +import initRuff, { Workspace } from "../pkg/ruff_wasm"; +import { loader } from "@monaco-editor/react"; +import { setupMonaco } from "./setupMonaco"; +import { DEFAULT_PYTHON_SOURCE } from "../constants"; + +export default function Chrome() { + const initPromise = useRef>(null); + const [pythonSource, setPythonSource] = useState(null); + const [settings, setSettings] = useState(null); + const [revision, setRevision] = useState(0); + const [ruffVersion, setRuffVersion] = useState(null); + + const [theme, setTheme] = useTheme(); + + const handleShare = useCallback(() => { + if (settings == null || pythonSource == null) { + return; + } + + persist(settings, pythonSource).catch((error) => + console.error(`Failed to share playground: ${error}`), + ); + }, [pythonSource, settings]); + + if (initPromise.current == null) { + initPromise.current = startPlayground() + .then(({ sourceCode, settings, ruffVersion }) => { + setPythonSource(sourceCode); + setSettings(settings); + setRuffVersion(ruffVersion); + setRevision(1); + }) + .catch((error) => { + console.error("Failed to initialize playground.", error); + }); + } + + const handleSourceChanged = useCallback( + (source: string) => { + setPythonSource(source); + setRevision((revision) => revision + 1); + + if (settings != null) { + persistLocal({ pythonSource: source, settingsSource: settings }); + } + }, + [settings], + ); + + const handleSettingsChanged = useCallback( + (settings: string) => { + setSettings(settings); + setRevision((revision) => revision + 1); + + if (pythonSource != null) { + persistLocal({ pythonSource: pythonSource, settingsSource: settings }); + } + }, + [pythonSource], + ); + + const source: Source | null = useMemo(() => { + if (pythonSource == null || settings == null) { + return null; + } + + return { pythonSource, settingsSource: settings }; + }, [settings, pythonSource]); + + return ( +
+
+ +
+ {source != null && ( + + )} +
+
+ ); +} + +// Run once during startup. Initializes monaco, loads the wasm file, and restores the previous editor state. +async function startPlayground(): Promise<{ + sourceCode: string; + settings: string; + ruffVersion: string; +}> { + await initRuff(); + const monaco = await loader.init(); + + console.log(monaco); + + setupMonaco(monaco); + + const response = await restore(); + const [settingsSource, pythonSource] = response ?? [ + stringify(Workspace.defaultSettings()), + DEFAULT_PYTHON_SOURCE, + ]; + + return { + sourceCode: pythonSource, + settings: settingsSource, + ruffVersion: Workspace.version(), + }; +} diff --git a/playground/src/Editor/Editor.tsx b/playground/src/Editor/Editor.tsx index 5bfc5bc1cf28c..1320fb4b208bb 100644 --- a/playground/src/Editor/Editor.tsx +++ b/playground/src/Editor/Editor.tsx @@ -1,15 +1,7 @@ -import { - useCallback, - useDeferredValue, - useEffect, - useMemo, - useState, -} from "react"; +import { useDeferredValue, useMemo, useState } from "react"; import { Panel, PanelGroup } from "react-resizable-panels"; -import { DEFAULT_PYTHON_SOURCE } from "../constants"; -import init, { Diagnostic, Workspace } from "../pkg/ruff_wasm"; +import { Diagnostic, Workspace } from "../pkg/ruff_wasm"; import { ErrorMessage } from "./ErrorMessage"; -import Header from "./Header"; import PrimarySideBar from "./PrimarySideBar"; import { HorizontalResizeHandle } from "./ResizeHandle"; import SecondaryPanel, { @@ -17,17 +9,15 @@ import SecondaryPanel, { SecondaryTool, } from "./SecondaryPanel"; import SecondarySideBar from "./SecondarySideBar"; -import { persist, persistLocal, restore, stringify } from "./settings"; import SettingsEditor from "./SettingsEditor"; import SourceEditor from "./SourceEditor"; -import { useTheme } from "./theme"; +import { Theme } from "./theme"; type Tab = "Source" | "Settings"; -interface Source { +export interface Source { pythonSource: string; settingsSource: string; - revision: number; } interface CheckResult { @@ -36,15 +26,20 @@ interface CheckResult { secondary: SecondaryPanelResult; } -export default function Editor() { - const [ruffVersion, setRuffVersion] = useState(null); - const [checkResult, setCheckResult] = useState({ - diagnostics: [], - error: null, - secondary: null, - }); - const [source, setSource] = useState(null); +type Props = { + source: Source; + theme: Theme; + onSourceChanged(source: string): void; + onSettingsChanged(settings: string): void; +}; + +export default function Editor({ + source, + theme, + onSourceChanged, + onSettingsChanged, +}: Props) { const [tab, setTab] = useState("Source"); const [secondaryTool, setSecondaryTool] = useState( () => { @@ -58,7 +53,6 @@ export default function Editor() { } }, ); - const [theme, setTheme] = useTheme(); // Ideally this would be retrieved right from the URL... but routing without a proper // router is hard (there's no location changed event) and pulling in a router @@ -81,33 +75,9 @@ export default function Editor() { setSecondaryTool(tool); }; - useEffect(() => { - async function initAsync() { - await init(); - const response = await restore(); - const [settingsSource, pythonSource] = response ?? [ - stringify(Workspace.defaultSettings()), - DEFAULT_PYTHON_SOURCE, - ]; - - setSource({ - revision: 0, - pythonSource, - settingsSource, - }); - setRuffVersion(Workspace.version()); - } - - initAsync().catch(console.error); - }, []); - const deferredSource = useDeferredValue(source); - useEffect(() => { - if (deferredSource == null) { - return; - } - + const checkResult: CheckResult = useMemo(() => { const { pythonSource, settingsSource } = deferredSource; try { @@ -161,116 +131,62 @@ export default function Editor() { }; } - setCheckResult({ + return { diagnostics, error: null, secondary, - }); + }; } catch (e) { - setCheckResult({ + return { diagnostics: [], error: (e as Error).message, secondary: null, - }); + }; } }, [deferredSource, secondaryTool]); - useEffect(() => { - if (source != null) { - persistLocal(source); - } - }, [source]); - - const handleShare = useMemo(() => { - if (source == null) { - return undefined; - } - - return () => { - return persist(source.settingsSource, source.pythonSource); - }; - }, [source]); - - const handlePythonSourceChange = useCallback((pythonSource: string) => { - setSource((state) => - state - ? { - ...state, - pythonSource, - revision: state.revision + 1, - } - : null, - ); - }, []); - - const handleSettingsSourceChange = useCallback((settingsSource: string) => { - setSource((state) => - state - ? { - ...state, - settingsSource, - revision: state.revision + 1, - } - : null, - ); - }, []); - return ( -
-
- -
- {source ? ( - - setTab(tool)} - selected={tab} - /> - - - + + setTab(tool)} selected={tab} /> + + + + + {secondaryTool != null && ( + <> + + + - {secondaryTool != null && ( - <> - - - - - - )} - - - ) : null} -
+ + )} + + + {checkResult.error && tab === "Source" ? (
{checkResult.error}
) : null} -
+ ); } diff --git a/playground/src/Editor/SourceEditor.tsx b/playground/src/Editor/SourceEditor.tsx index 50d14f74475ac..c74946e59bec9 100644 --- a/playground/src/Editor/SourceEditor.tsx +++ b/playground/src/Editor/SourceEditor.tsx @@ -5,7 +5,7 @@ import Editor, { BeforeMount, Monaco } from "@monaco-editor/react"; import { MarkerSeverity, MarkerTag } from "monaco-editor"; import { useCallback, useEffect, useRef } from "react"; -import { Diagnostic } from "../pkg"; +import { Diagnostic } from "../pkg/ruff_wasm"; import { Theme } from "./theme"; export default function SourceEditor({ diff --git a/playground/src/Editor/theme.ts b/playground/src/Editor/theme.ts index 60446a800f94d..7549dae5864a4 100644 --- a/playground/src/Editor/theme.ts +++ b/playground/src/Editor/theme.ts @@ -1,12 +1,14 @@ /** * Light and dark mode theming. */ -import { useEffect, useState } from "react"; +import { useState } from "react"; export type Theme = "dark" | "light"; export function useTheme(): [Theme, (theme: Theme) => void] { - const [localTheme, setLocalTheme] = useState("light"); + const [localTheme, setLocalTheme] = useState(() => + detectInitialTheme(), + ); const setTheme = (mode: Theme) => { if (mode === "dark") { @@ -18,18 +20,18 @@ export function useTheme(): [Theme, (theme: Theme) => void] { setLocalTheme(mode); }; - useEffect(() => { - const initialTheme = localStorage.getItem("theme"); - if (initialTheme === "dark") { - setTheme("dark"); - } else if (initialTheme === "light") { - setTheme("light"); - } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - setTheme("dark"); - } else { - setTheme("light"); - } - }, []); - return [localTheme, setTheme]; } + +function detectInitialTheme(): Theme { + const initialTheme = localStorage.getItem("theme"); + if (initialTheme === "dark") { + return "dark"; + } else if (initialTheme === "light") { + return "light"; + } else if (window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "dark"; + } else { + return "light"; + } +} diff --git a/playground/src/main.tsx b/playground/src/main.tsx index d62cb07f57560..fbe0181a4dabd 100644 --- a/playground/src/main.tsx +++ b/playground/src/main.tsx @@ -1,14 +1,10 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import Editor from "./Editor"; import "./index.css"; -import { loader } from "@monaco-editor/react"; -import { setupMonaco } from "./Editor/setupMonaco"; - -loader.init().then(setupMonaco); +import Chrome from "./Editor/Chrome"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + , );