spinner in autorun mode; autorun refactorings
This commit is contained in:
		
							parent
							
								
									d6cc3a419b
								
							
						
					
					
						commit
						3bdc5c67a2
					
				| 
						 | 
				
			
			@ -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<{
 | 
			
		|||
      )}
 | 
			
		||||
      <Toggle
 | 
			
		||||
        texts={["Autorun", "Paused"]}
 | 
			
		||||
        icons={[CheckCircleIcon, PauseIcon]}
 | 
			
		||||
        icons={[
 | 
			
		||||
          CheckCircleIcon, // note: we could replace this icon with RefreshIcon when spin is on, but that would cause blinking on each keystroke
 | 
			
		||||
          PauseIcon,
 | 
			
		||||
        ]}
 | 
			
		||||
        status={autorunMode}
 | 
			
		||||
        onChange={onAutorunModeChange}
 | 
			
		||||
        spinIcon={isRunning}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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<PlaygroundProps> = ({
 | 
			
		||||
  defaultCode = "",
 | 
			
		||||
  height = 500,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,13 +8,15 @@ type Props = {
 | 
			
		|||
  onChange: (status: boolean) => void;
 | 
			
		||||
  texts: [string, string];
 | 
			
		||||
  icons: [IconType, IconType];
 | 
			
		||||
  spinIcon?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const Toggle: React.FC<Props> = ({
 | 
			
		||||
  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<Props> = ({
 | 
			
		|||
      onClick={() => onChange(!status)}
 | 
			
		||||
    >
 | 
			
		||||
      <div>
 | 
			
		||||
        <CurrentIcon className="w-6 h-6" />
 | 
			
		||||
        <CurrentIcon className={clsx("w-6 h-6", spinIcon && "animate-spin")} />
 | 
			
		||||
      </div>
 | 
			
		||||
      <span>{status ? onText : offText}</span>
 | 
			
		||||
    </button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								packages/components/src/lib/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								packages/components/src/lib/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
export { useMaybeControlledValue } from "./useMaybeControlledValue";
 | 
			
		||||
export { useSquiggle, useSquigglePartial } from "./useSquiggle";
 | 
			
		||||
export { useRunnerState } from "./useRunnerState";
 | 
			
		||||
							
								
								
									
										22
									
								
								packages/components/src/lib/hooks/useMaybeControlledValue.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packages/components/src/lib/hooks/useMaybeControlledValue.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
type ControlledValueArgs<T> = {
 | 
			
		||||
  value?: T;
 | 
			
		||||
  defaultValue: T;
 | 
			
		||||
  onChange?: (x: T) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function useMaybeControlledValue<T>(
 | 
			
		||||
  args: ControlledValueArgs<T>
 | 
			
		||||
): [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];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								packages/components/src/lib/hooks/useRunnerState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								packages/components/src/lib/hooks/useRunnerState.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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 });
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -5,7 +5,7 @@ import {
 | 
			
		|||
  run,
 | 
			
		||||
  runPartial,
 | 
			
		||||
} from "@quri/squiggle-lang";
 | 
			
		||||
import { useEffect, useMemo, useState } from "react";
 | 
			
		||||
import { useEffect, useMemo } from "react";
 | 
			
		||||
 | 
			
		||||
type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
 | 
			
		||||
  code: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -42,23 +42,3 @@ export const useSquigglePartial = (
 | 
			
		|||
export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => {
 | 
			
		||||
  return useSquiggleAny(args, run);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ControlledValueArgs<T> = {
 | 
			
		||||
  value?: T;
 | 
			
		||||
  defaultValue: T;
 | 
			
		||||
  onChange?: (x: T) => void;
 | 
			
		||||
};
 | 
			
		||||
export function useMaybeControlledValue<T>(
 | 
			
		||||
  args: ControlledValueArgs<T>
 | 
			
		||||
): [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];
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user