esolang/ui/renderer-wrapper.tsx

57 lines
2.1 KiB
TypeScript

import React from "react";
import { LanguageProvider } from "../languages/types";
const RENDER_INTERVAL_MS = 50;
export interface RendererRef<RS> {
/** Update runtime state to renderer */
updateState: (state: RS | null) => void;
}
/**
* React component that acts as an imperatively controller wrapper
* around the actual language renderer. This is to pull renderer state updates
* outside of Mainframe and debouncing rerenders for performance reasons.
*/
const RendererWrapperComponent = <RS extends {}>(
{ renderer }: { renderer: LanguageProvider<RS>["Renderer"] },
ref: React.Ref<RendererRef<RS>>
) => {
const [renderedState, setRenderedState] = React.useState<RS | null>(null);
/**
* Re-rendering on each state update becomes a bottleneck for running code at high speed.
* The main browser thread will take too much time handling each message and the message queue
* will pile up, leading to very visible delay in actions like pause and stop.
*
* To avoid this, we do some debouncing here. We only truly re-render at every X ms: on
* other updates we just store the latest state in a ref.
*/
// Timer for re-rendering with the latest state
const debounceTimerRef = React.useRef<NodeJS.Timeout | null>(null);
// Ref to store the very latest state, that may be rendered upto X ms later
const latestState = React.useRef<RS | null>(null);
/**
* Update the current renderer state. Debounces re-render by default,
* pass `immediate` as true to immediately re-render with the given state.
*/
const updateState = (state: RS | null) => {
latestState.current = state;
if (debounceTimerRef.current) return; // Timeout will automatically render liveState
// Set a timer to re-render with the latest result after X ms
debounceTimerRef.current = setTimeout(() => {
setRenderedState(latestState.current);
debounceTimerRef.current = null;
}, RENDER_INTERVAL_MS);
};
React.useImperativeHandle(ref, () => ({ updateState }));
return renderer({ state: renderedState });
};
export const RendererWrapper = React.forwardRef(RendererWrapperComponent);