diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx
index 5ca84040..a2fadb7d 100644
--- a/packages/components/src/components/SquigglePlayground.tsx
+++ b/packages/components/src/components/SquigglePlayground.tsx
@@ -1,7 +1,7 @@
import React, { FC, useState, useEffect, useMemo } from "react";
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup";
-import { useMaybeControlledValue } from "../lib/hooks";
+import { useMaybeControlledValue, useRunnerState } from "../lib/hooks";
import { yupResolver } from "@hookform/resolvers/yup";
import {
ChartSquareBarIcon,
@@ -358,54 +358,18 @@ const RunControls: React.FC<{
)}
);
};
-const useRunnerState = (code: string) => {
- const [autorunMode, setAutorunMode] = useState(true);
- const [renderedCode, setRenderedCode] = useState(code); // used in manual run mode only
- const [isRunning, setIsRunning] = useState(false); // used in manual run mode only
-
- // This part is tricky and fragile; we need to re-render first to make sure that the icon is spinning,
- // and only then evaluate the squiggle code (which freezes the UI).
- // Also note that `useEffect` execution order matters here.
- // Hopefully it'll all go away after we make squiggle code evaluation async.
- useEffect(() => {
- if (renderedCode === code && isRunning) {
- // It's not possible to put this after `setRenderedCode(code)` below because React would apply
- // `setIsRunning` and `setRenderedCode` together and spinning icon will disappear immediately.
- setIsRunning(false);
- }
- }, [renderedCode, code, isRunning]);
-
- useEffect(() => {
- if (!autorunMode && isRunning) {
- setRenderedCode(code); // TODO - force run even if code hasn't changed
- }
- }, [autorunMode, code, isRunning]);
-
- const run = () => {
- // The rest will be handled by useEffects above, but we need to update the spinner first.
- setIsRunning(true);
- };
-
- return {
- run,
- renderedCode: autorunMode ? code : renderedCode,
- isRunning,
- autorunMode,
- setAutorunMode: (newValue: boolean) => {
- if (!newValue) setRenderedCode(code);
- setAutorunMode(newValue);
- },
- };
-};
-
export const SquigglePlayground: FC = ({
defaultCode = "",
height = 500,
diff --git a/packages/components/src/components/ui/Toggle.tsx b/packages/components/src/components/ui/Toggle.tsx
index 3295bda0..17ecc3f6 100644
--- a/packages/components/src/components/ui/Toggle.tsx
+++ b/packages/components/src/components/ui/Toggle.tsx
@@ -8,13 +8,15 @@ type Props = {
onChange: (status: boolean) => void;
texts: [string, string];
icons: [IconType, IconType];
+ spinIcon?: boolean;
};
export const Toggle: React.FC = ({
- texts: [onText, offText],
- icons: [OnIcon, OffIcon],
status,
onChange,
+ texts: [onText, offText],
+ icons: [OnIcon, OffIcon],
+ spinIcon,
}) => {
const CurrentIcon = status ? OnIcon : OffIcon;
return (
@@ -28,7 +30,7 @@ export const Toggle: React.FC = ({
onClick={() => onChange(!status)}
>
-
+
{status ? onText : offText}
diff --git a/packages/components/src/lib/hooks/index.ts b/packages/components/src/lib/hooks/index.ts
new file mode 100644
index 00000000..01fb46f9
--- /dev/null
+++ b/packages/components/src/lib/hooks/index.ts
@@ -0,0 +1,3 @@
+export { useMaybeControlledValue } from "./useMaybeControlledValue";
+export { useSquiggle, useSquigglePartial } from "./useSquiggle";
+export { useRunnerState } from "./useRunnerState";
diff --git a/packages/components/src/lib/hooks/useMaybeControlledValue.ts b/packages/components/src/lib/hooks/useMaybeControlledValue.ts
new file mode 100644
index 00000000..aa7abc50
--- /dev/null
+++ b/packages/components/src/lib/hooks/useMaybeControlledValue.ts
@@ -0,0 +1,22 @@
+import { useState } from "react";
+
+type ControlledValueArgs = {
+ value?: T;
+ defaultValue: T;
+ onChange?: (x: T) => void;
+};
+
+export function useMaybeControlledValue(
+ args: ControlledValueArgs
+): [T, (x: T) => void] {
+ let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue);
+ let value = args.value ?? uncontrolledValue;
+ let onChange = (newValue: T) => {
+ if (args.value === undefined) {
+ // uncontrolled mode
+ setUncontrolledValue(newValue);
+ }
+ args.onChange?.(newValue);
+ };
+ return [value, onChange];
+}
diff --git a/packages/components/src/lib/hooks/useRunnerState.ts b/packages/components/src/lib/hooks/useRunnerState.ts
new file mode 100644
index 00000000..e12eb5ad
--- /dev/null
+++ b/packages/components/src/lib/hooks/useRunnerState.ts
@@ -0,0 +1,92 @@
+import { useEffect, useReducer } from "react";
+
+type State = {
+ autorunMode: boolean;
+ renderedCode: string;
+ isRunning: boolean;
+ executionId: number;
+};
+
+const buildInitialState = (code: string) => ({
+ autorunMode: true,
+ renderedCode: code,
+ isRunning: false,
+ executionId: 0,
+});
+
+type Action =
+ | {
+ type: "SET_AUTORUN_MODE";
+ value: boolean;
+ code: string;
+ }
+ | {
+ type: "PREPARE_RUN";
+ }
+ | {
+ type: "RUN";
+ code: string;
+ }
+ | {
+ type: "STOP_RUN";
+ };
+
+const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "SET_AUTORUN_MODE":
+ return {
+ ...state,
+ autorunMode: action.value,
+ };
+ case "PREPARE_RUN":
+ return {
+ ...state,
+ isRunning: true,
+ };
+ case "RUN":
+ return {
+ ...state,
+ renderedCode: action.code,
+ executionId: state.executionId + 1,
+ };
+ case "STOP_RUN":
+ return {
+ ...state,
+ isRunning: false,
+ };
+ }
+};
+
+export const useRunnerState = (code: string) => {
+ const [state, dispatch] = useReducer(reducer, buildInitialState(code));
+
+ useEffect(() => {
+ if (state.isRunning) {
+ if (state.renderedCode !== code) {
+ dispatch({ type: "RUN", code });
+ } else {
+ dispatch({ type: "STOP_RUN" });
+ }
+ }
+ }, [state.isRunning, state.renderedCode, code]);
+
+ const run = () => {
+ // The rest will be handled by dispatches above on following renders, but we need to update the spinner first.
+ dispatch({ type: "PREPARE_RUN" });
+ };
+
+ if (state.autorunMode && state.renderedCode !== code && !state.isRunning) {
+ run();
+ }
+
+ return {
+ run,
+ autorunMode: state.autorunMode,
+ renderedCode: state.renderedCode,
+ isRunning: state.isRunning,
+ executionId: state.executionId,
+ setAutorunMode: (newValue: boolean) => {
+ dispatch({ type: "SET_AUTORUN_MODE", value: newValue, code });
+ },
+ };
+};
diff --git a/packages/components/src/lib/hooks.ts b/packages/components/src/lib/hooks/useSquiggle.ts
similarity index 65%
rename from packages/components/src/lib/hooks.ts
rename to packages/components/src/lib/hooks/useSquiggle.ts
index b52c23be..03b8c69d 100644
--- a/packages/components/src/lib/hooks.ts
+++ b/packages/components/src/lib/hooks/useSquiggle.ts
@@ -5,7 +5,7 @@ import {
run,
runPartial,
} from "@quri/squiggle-lang";
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo } from "react";
type SquiggleArgs> = {
code: string;
@@ -42,23 +42,3 @@ export const useSquigglePartial = (
export const useSquiggle = (args: SquiggleArgs>) => {
return useSquiggleAny(args, run);
};
-
-type ControlledValueArgs = {
- value?: T;
- defaultValue: T;
- onChange?: (x: T) => void;
-};
-export function useMaybeControlledValue(
- args: ControlledValueArgs
-): [T, (x: T) => void] {
- let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue);
- let value = args.value ?? uncontrolledValue;
- let onChange = (newValue: T) => {
- if (args.value === undefined) {
- // uncontrolled mode
- setUncontrolledValue(newValue);
- }
- args.onChange?.(newValue);
- };
- return [value, onChange];
-}