Remove all useEffect usages (#12659)

This commit is contained in:
Micha Reiser 2024-08-08 13:16:38 +02:00 committed by GitHub
parent 2daa914334
commit f53733525c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 208 additions and 168 deletions

View file

@ -128,3 +128,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Wrangler
api/.wrangler

View file

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

View file

@ -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 | Promise<void>>(null);
const [pythonSource, setPythonSource] = useState<null | string>(null);
const [settings, setSettings] = useState<null | string>(null);
const [revision, setRevision] = useState(0);
const [ruffVersion, setRuffVersion] = useState<string | null>(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 (
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
<Header
edit={revision}
theme={theme}
version={ruffVersion}
onChangeTheme={setTheme}
onShare={handleShare}
/>
<div className="flex flex-grow">
{source != null && (
<Editor
theme={theme}
source={source}
onSettingsChanged={handleSettingsChanged}
onSourceChanged={handleSourceChanged}
/>
)}
</div>
</main>
);
}
// 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(),
};
}

View file

@ -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<string | null>(null);
const [checkResult, setCheckResult] = useState<CheckResult>({
diagnostics: [],
error: null,
secondary: null,
});
const [source, setSource] = useState<Source | null>(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<Tab>("Source");
const [secondaryTool, setSecondaryTool] = useState<SecondaryTool | null>(
() => {
@ -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 (
<main className="flex flex-col h-full bg-ayu-background dark:bg-ayu-background-dark">
<Header
edit={source ? source.revision : null}
theme={theme}
version={ruffVersion}
onChangeTheme={setTheme}
onShare={handleShare}
/>
<div className="flex flex-grow">
{source ? (
<PanelGroup direction="horizontal" autoSaveId="main">
<PrimarySideBar
onSelectTool={(tool) => setTab(tool)}
selected={tab}
/>
<Panel id="main" order={0} className="my-2" minSize={10}>
<SourceEditor
visible={tab === "Source"}
source={source.pythonSource}
<>
<PanelGroup direction="horizontal" autoSaveId="main">
<PrimarySideBar onSelectTool={(tool) => setTab(tool)} selected={tab} />
<Panel id="main" order={0} className="my-2" minSize={10}>
<SourceEditor
visible={tab === "Source"}
source={source.pythonSource}
theme={theme}
diagnostics={checkResult.diagnostics}
onChange={onSourceChanged}
/>
<SettingsEditor
visible={tab === "Settings"}
source={source.settingsSource}
theme={theme}
onChange={onSettingsChanged}
/>
</Panel>
{secondaryTool != null && (
<>
<HorizontalResizeHandle />
<Panel
id="secondary-panel"
order={1}
className={"my-2"}
minSize={10}
>
<SecondaryPanel
theme={theme}
diagnostics={checkResult.diagnostics}
onChange={handlePythonSourceChange}
/>
<SettingsEditor
visible={tab === "Settings"}
source={source.settingsSource}
theme={theme}
onChange={handleSettingsSourceChange}
tool={secondaryTool}
result={checkResult.secondary}
/>
</Panel>
{secondaryTool != null && (
<>
<HorizontalResizeHandle />
<Panel
id="secondary-panel"
order={1}
className={"my-2"}
minSize={10}
>
<SecondaryPanel
theme={theme}
tool={secondaryTool}
result={checkResult.secondary}
/>
</Panel>
</>
)}
<SecondarySideBar
selected={secondaryTool}
onSelected={handleSecondaryToolSelected}
/>
</PanelGroup>
) : null}
</div>
</>
)}
<SecondarySideBar
selected={secondaryTool}
onSelected={handleSecondaryToolSelected}
/>
</PanelGroup>
{checkResult.error && tab === "Source" ? (
<div
style={{
@ -283,7 +199,7 @@ export default function Editor() {
<ErrorMessage>{checkResult.error}</ErrorMessage>
</div>
) : null}
</main>
</>
);
}

View file

@ -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({

View file

@ -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<Theme>("light");
const [localTheme, setLocalTheme] = useState<Theme>(() =>
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";
}
}

View file

@ -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(
<React.StrictMode>
<Editor />
<Chrome />
</React.StrictMode>,
);