Skip to content

Commit

Permalink
feat(SSR): add support for initial SSR render
Browse files Browse the repository at this point in the history
Combine these PRs:

- FormidableLabs#322
- FormidableLabs#323

but v4 compatible.
  • Loading branch information
tujoworker committed Sep 13, 2023
1 parent 39850bd commit 52ca6e9
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 20 deletions.
45 changes: 31 additions & 14 deletions packages/react-live/src/components/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,50 @@ export type Props = {
tabMode?: "focus" | "indentation";
theme?: typeof themes.nightOwl;
onChange?(value: string): void;
};
editorRef?: React.RefObject<HTMLPreElement>;
} & Omit<React.HTMLAttributes<HTMLPreElement>, "onChange">;

const CodeEditor = (props: Props) => {
const editorRef = useRef(null);
const [code, setCode] = useState(props.code || "");
const {
code: origCode,
className,
style,
tabMode,
theme: origTheme,
prism,
language,
disabled,
onChange,
editorRef,
...rest
} = props;

const _editorRef = editorRef || useRef(null);
const [code, setCode] = useState(origCode || "");
const { theme } = props;

useEffect(() => {
setCode(props.code);
}, [props.code]);
setCode(origCode);
}, [origCode]);

useEditable(editorRef, (text) => setCode(text.slice(0, -1)), {
disabled: props.disabled,
indentation: props.tabMode === "indentation" ? 2 : undefined,
useEditable(_editorRef, (text) => setCode(text.slice(0, -1)), {
disabled: disabled,
indentation: tabMode === "indentation" ? 2 : undefined,
});

useEffect(() => {
if (props.onChange) {
props.onChange(code);
if (onChange) {
onChange(code);
}
}, [code]);

return (
<div className={props.className} style={props.style}>
<div className={className} style={style}>
<Highlight
prism={prism || Prism}
code={code}
theme={props.theme || themes.nightOwl}
language={props.language}
theme={origTheme || themes.nightOwl}
language={language}
>
{({
className: _className,
Expand All @@ -58,8 +74,9 @@ const CodeEditor = (props: Props) => {
...(theme && typeof theme.plain === "object" ? theme.plain : {}),
..._style,
}}
ref={editorRef}
ref={_editorRef}
spellCheck="false"
{...rest}
>
{tokens.map((line, lineIndex) => (
<span key={`line-${lineIndex}`} {...getLineProps({ line })}>
Expand Down
65 changes: 59 additions & 6 deletions packages/react-live/src/components/Live/LiveProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { useEffect, useState, ComponentType, PropsWithChildren } from "react";
import {
useEffect,
useState,
useRef,
ComponentType,
PropsWithChildren,
} from "react";
import LiveContext from "./LiveContext";
import { generateElement, renderElementAsync } from "../../utils/transpile";
import { themes } from "prism-react-renderer";
Expand All @@ -14,9 +20,10 @@ type Props = {
enableTypeScript?: boolean;
language?: string;
noInline?: boolean;
skipInitialRender?: boolean;
scope?: Record<string, unknown>;
theme?: typeof themes.nightOwl;
transformCode?(code: string): void;
transformCode?(code: string): string;
};

function LiveProvider({
Expand All @@ -29,13 +36,59 @@ function LiveProvider({
scope,
transformCode,
noInline = false,
skipInitialRender = false,
}: PropsWithChildren<Props>) {
const [state, setState] = useState<ProviderState>({
error: undefined,
element: undefined,
});
// avoid to render code twice when rendered initially (ssr)
const cache = useRef("initial");

// ssr render the code in sync
const [state, setState] = useState<ProviderState>(() => transpileSync(code));

function transpileSync(code: string) {
const returnObject: ProviderState = {
element: undefined,
error: undefined,
};

if (!skipInitialRender) {
const renderElement = (element: ComponentType) => {
return (returnObject.element = element);
};
const errorCallback = (error: unknown) => {
return (returnObject.error = String(error));
};

try {
const transformResult = transformCode ? transformCode(code) : code;

// Transpilation arguments
const input = {
code: transformResult,
scope,
enableTypeScript,
};

if (noInline) {
renderElementAsync(input, renderElement, errorCallback);
} else {
renderElement(generateElement(input, errorCallback));
}

cache.current = code;
} catch (e) {
errorCallback(e);
}
}

return returnObject;
}

async function transpileAsync(newCode: string) {
if (cache.current === newCode) {
cache.current = "used"; // do not check for null or undefined, in case the new code is such
return Promise.resolve();
}

const errorCallback = (error: Error) => {
setState({ error: error.toString(), element: undefined });
};
Expand Down

0 comments on commit 52ca6e9

Please sign in to comment.