React / Next.js integration
Mount SVeditor in React and Next.js App Router with stable options, auto-save, and SSR-safe client boundaries.
React / Next.js integration
Three rules: create on the client only, keep a stable options reference, call destroy() on unmount.
Prerequisites: ESM package installed. Examples use Next.js App Router; other React frameworks can skip routing notes.
SSR and the ESM bundle
The ESM build may touch browser globals at import time.
- Import
@wztlink1013/sveditor/style.cssfrom a client module. - Load
@wztlink1013/sveditoronly in the browser —next/dynamicwithssr: falseor dynamicimport()inuseEffect.
Details: ESM integration — Next.js.
Reusable mount component
Copy this pattern into your app (for example components/sveditor-mount.tsx):
"use client";
import { useEffect, useRef } from "react";
import { SVeditor } from "@wztlink1013/sveditor";
import type { EditorOptions } from "@wztlink1013/sveditor";
import { cn } from "@/lib/utils"; // your className helper
import "@wztlink1013/sveditor/style.css";
export type SveditorMountProps = {
className?: string;
options: Omit<EditorOptions, "el">;
};
export function SveditorMount({ className, options }: SveditorMountProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const editor = SVeditor.create({ el, ...options });
return () => editor.destroy();
}, [options]);
return (
<div ref={containerRef} className={cn("min-h-[640px] w-full", className)} />
);
}For App Router, export it through next/dynamic(..., { ssr: false }) from your page's client wrapper.
Stabilize options with useMemo
A new options object every render recreates the editor and can lose focus or re-run onFetch.
"use client";
import { useMemo } from "react";
import type { EditorOptions } from "@wztlink1013/sveditor";
import { SveditorMount } from "@/components/sveditor-mount";
type EditorInitOptions = Omit<EditorOptions, "el">;
export function DocEditor({ docId }: { docId: string }) {
const options = useMemo<EditorInitOptions>(
() => ({
extensionsOptions: {
autoSave: {
onFetch: async () => {
const res = await fetch(`/api/docs/${docId}`, { cache: "no-store" });
const data = await res.json();
return { content: data.content ?? "" };
},
onSaveContent: async (json) => {
await fetch(`/api/docs/${docId}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: json }),
});
},
debounceMs: 1000,
},
},
}),
[docId],
);
return <SveditorMount options={options} />;
}Auto-save with metadata: Auto-save guide.
Server Component page
Keep the page as a Server Component; pass props into a client child:
import { DocEditor } from "@/components/doc-editor";
export default async function DocPage({
params,
}: {
params: Promise<{ docId: string }>;
}) {
const { docId } = await params;
return <DocEditor docId={docId} />;
}Lazy load (optional)
import dynamic from "next/dynamic";
const DocEditor = dynamic(
() => import("@/components/doc-editor").then((m) => m.DocEditor),
{ ssr: false, loading: () => <p>Loading editor...</p> },
);Common patterns
Auto-save + metadata
extensionsOptions: {
autoSave: {
onFetch: () => fetchContent(docId),
onSaveContent: (json) => saveContent(docId, json),
},
meta: {
onFetchMeta: () => fetchMeta(docId),
onSaveMeta: (meta) => saveMeta(docId, meta),
},
}Do not return meta from autoSave.onFetch.
Custom notifications
extensionsOptions: {
notifications: {
toast: myToastAdapter,
builtinToaster: false, // set true only if Sonner is not mounted globally
},
}Checklist
| Practice | Why |
|---|---|
"use client" on mount components | SDK is browser-only |
useMemo([docId, ...]) | Rebuild when document id changes |
Import style.css once globally or in mount module | Required for layout |
destroy() in effect cleanup | Prevents leaks |
Notes
- Never call
SVeditor.create()in a Server Component. - When
docIdchanges, include it inuseMemodependencies. - See Playground for a live wired example.