React – inksane
inksane

React

Use the React bindings: a provider, a component, and hooks.

@inksane/react wraps the core editor for React. It exports a provider, a component that renders the mount element, and hooks for the editor and its selection. React 18 and newer are supported.

pnpm add @inksane/react

The pieces

ExportWhat it does
useInksane(options)Creates an editor for the component lifetime and returns it, or null.
<Inksane />Renders the element the editor mounts into.
<InksaneProvider />Creates one editor and shares it with the tree through context.
useInksaneContext()Reads the nearest editor from context.
useInksaneSelection()Reads the current selection and re-renders when it changes.

options is EditorOptions without element, because the binding owns the mount element.

React bindings do not add feature extensions automatically. Pass BaseKit for the standard setup, or register individual extensions.

Mount an editor

useInksane creates the editor after the first render, so it returns null on that render. Pass the editor to <Inksane />, which mounts it into a div.

import { BaseKit } from "@inksane/core/extensions/base-kit";
import { Inksane, useInksane } from "@inksane/react";

export function Editor() {
  const editor = useInksane({ content: "# Hello inksane", extensions: [BaseKit()] });

  return <Inksane editor={editor} className="rounded-md border" />;
}

The editor is client-only

The editor is created in an effect and needs the DOM. During server rendering it does not exist, so render <Inksane /> and let it hydrate on the client. Do not read editor.view during render.

Share the editor with context

InksaneProvider creates the editor and provides it. Any component below it can read the editor with useInksaneContext(), which is how you build a toolbar next to the editor.

This toolbar uses bold and italic commands. Its provider must register those extensions or BaseKit.

import { useInksaneContext, useInksaneSelection } from "@inksane/react";

export function Toolbar() {
  const editor = useInksaneContext();
  const selection = useInksaneSelection();

  return (
    <div className="flex gap-2 border-b p-2">
      <button
        type="button"
        disabled={!editor}
        onClick={() => editor?.chain().toggleBold().focus().run()}
      >
        Bold
      </button>
      <button
        type="button"
        disabled={!editor}
        onClick={() => editor?.chain().toggleItalic().focus().run()}
      >
        Italic
      </button>
      <span className="ml-auto">
        {selection ? `${selection.main.from}–${selection.main.to}` : "no selection"}
      </span>
    </div>
  );
}

Put the toolbar and the editor under one provider:

import { BaseKit } from "@inksane/core/extensions/base-kit";
import { Inksane, InksaneProvider } from "@inksane/react";
import { Toolbar } from "./toolbar";

export function App() {
  return (
    <InksaneProvider content="# Hello inksane" extensions={[BaseKit()]}>
      <div className="overflow-hidden rounded-md border">
        <Toolbar />
        <Inksane />
      </div>
    </InksaneProvider>
  );
}

Under a provider, <Inksane /> uses the editor from context, so you do not pass editor yourself.

Options are read once

useInksane captures its options when the editor is created. Changing extensions, theme, or other options later has no effect. content is the exception: it stays in sync with the content prop.

import type { Extension } from "@inksane/core";
import { BaseKit } from "@inksane/core/extensions/base-kit";
import { Inksane, useInksane } from "@inksane/react";

function Editor({ extensions = [BaseKit()] }: { extensions?: Extension[] }) {
  const editor = useInksane({ extensions });

  return <Inksane editor={editor} />;
}

To apply new options, remount the editor with a key:

<Editor key={storageKey} extensions={extensions} />

Read and write content

editor.content and the content option stay in sync, but the editor also emits events, so you can build a controlled component the other way around.

import { useEffect, useState } from "react";
import { BaseKit } from "@inksane/core/extensions/base-kit";
import { Inksane, useInksane } from "@inksane/react";

export function ControlledEditor() {
  const [markdown, setMarkdown] = useState("# Start here");
  const editor = useInksane({ content: markdown, extensions: [BaseKit()] });

  useEffect(() => {
    if (!editor) return;

    const handleUpdate = () => setMarkdown(editor.getMarkdown());
    editor.on("update", handleUpdate);
    return () => editor.off("update", handleUpdate);
  }, [editor]);

  return (
    <>
      <Inksane editor={editor} />
      <pre>{markdown}</pre>
    </>
  );
}

See Events for every event and its payload.

Selection

useInksaneSelection() returns the editor’s current selection or null before the editor exists. It subscribes to selectionUpdate, so the component re-renders when the user moves the cursor or selects text. Pass an editor to read a selection outside a provider.

const selection = useInksaneSelection(editor);
selection?.main.from; // anchor
selection?.main.to; // head

Next steps