app/components/admin/use-live-preview.ts
73 lines and 7 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| useLivePreview | 14 to 81 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:29> | 29 to 36 | 5 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| chooseLayout | 38 to 45 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:49> | 49 to 78 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:53> | 53 to 76 | 11 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:60> | 60 to 60 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:77> | 77 to 77 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/lib/error-message.mjs. Imported by app/components/admin/editor-bar.tsx, app/components/admin/post-editor.tsx.
Source
import { useEffect, useRef, useState } from "react";
import { errorMessage } from "~/lib/error-message.mjs";
export type Layout = "write" | "split" | "preview";
const PREVIEW_DELAY_MS = 600;
const LAYOUT_KEY = "post-editor:layout";
/**
* The editor's layout (Write, Split, Preview), remembered in this browser, and the rendered preview
* that the Split and Preview layouts show, re-requested a beat after the body stops changing.
*/
export function useLivePreview({
body,
fieldsSlug,
slug,
}: {
body: string;
/** The committed slug, which wins; the typed one stands in for a post not yet saved. */
fieldsSlug: string;
slug: string;
}) {
const [layout, setLayout] = useState<Layout>("write");
const [preview, setPreview] = useState<{ html: string } | { error: string } | null>(null);
const [previewing, setPreviewing] = useState(false);
// Read after mount, not during render, so the server and the first client paint agree.
useEffect(() => {
try {
const stored = window.localStorage.getItem(LAYOUT_KEY);
if (stored === "write" || stored === "split" || stored === "preview") setLayout(stored);
} catch {
// Storage disabled. The editor opens in Write, which is the default anyway.
}
}, []);
const chooseLayout = (next: Layout) => {
setLayout(next);
try {
window.localStorage.setItem(LAYOUT_KEY, next);
} catch {
// As above. The choice still applies to this session.
}
};
// A sequence number guards the response: with two renders in flight the slower can land last.
const previewSeq = useRef(0);
useEffect(() => {
if (layout === "write") return;
const seq = (previewSeq.current += 1);
setPreviewing(true);
const timer = window.setTimeout(async () => {
const form = new FormData();
form.set("body", body);
form.set("slug", fieldsSlug || slug);
try {
const response = await fetch("/admin/preview", { method: "POST", body: form });
// Not JSON is a failed request, such as an expired session answering with a page.
const result = (await response.json().catch(() => null)) as {
html?: string;
error?: string;
} | null;
if (seq !== previewSeq.current) return;
if (!result || (!response.ok && !result.error)) {
setPreview({ error: `The preview request failed with HTTP ${response.status}.` });
return;
}
setPreview(result.error ? { error: result.error } : { html: result.html ?? "" });
} catch (error) {
if (seq !== previewSeq.current) return;
setPreview({ error: errorMessage(error) });
} finally {
if (seq === previewSeq.current) setPreviewing(false);
}
}, PREVIEW_DELAY_MS);
return () => window.clearTimeout(timer);
}, [body, layout, fieldsSlug, slug]);
return { layout, chooseLayout, preview, previewing };
}