A rich text editor for React
yjd is a dependency-free, tree-shakeable rich text editor. It has no framework baked in, so you wrap it in about 15 lines of a hook — controlled value ⇄ onChange, StrictMode-safe, with TypeScript types out of the box. Ship ~17 KB, not a whole editor framework.
Add the package
One dependency, no peer requirements. The stylesheet is a single import.
npm i @oix1987/yjdA reusable hook component
Create the editor once in an effect and destroy() it on cleanup — that cleanup is what makes it safe under React 18 StrictMode (which mounts effects twice in dev). Sync incoming value only when it differs, so typing never resets the caret.
import { useEffect, useRef } from 'react';
import yjd from '@oix1987/yjd';
import '@oix1987/yjd/styles.css'; // once in your app
export function Editor({ value, onChange, placeholder }) {
const host = useRef(null);
const ed = useRef(null);
useEffect(() => {
ed.current = new yjd(host.current, {
content: value ?? '',
placeholder,
onChange: (html) => onChange?.(html),
});
return () => ed.current.destroy(); // StrictMode-safe
}, []);
// controlled: set only when different (no caret jump)
useEffect(() => {
const e = ed.current;
if (e && value != null && value !== e.getContent()) e.setContent(value);
}, [value]);
return <div ref={host} />;
}const [html, setHtml] = useState('<p>Hello</p>');
<Editor value={html} onChange={setHtml} placeholder="Write…" />The editor itself
This is a real yjd instance (the same one the hook mounts). Type in it. Want it wrapped in actual React? Open the running React example.
Why wrap yjd instead of a React-only editor
~17 KB, tree-shakeable
Register only the formats you use from the /core entry. No editor framework in the bundle.
TypeScript types
Ships index.d.ts — props and options are typed with no extra install.
StrictMode-safe
The destroy() cleanup handles React 18's double-mount cleanly — zero leaks.
No lock-in
The same core runs in Vue, Svelte and vanilla — one editor across your stack.
useEffect it already does; for a whole page use dynamic(() => import('./Editor'), { ssr: false }).Next
Running React example · Using Vue instead? · Playground presets · Full docs