@handlewithcare/react-prosemirror

repository·main·Indexed 19 days ago

https://github.com/handlewithcarecollective/react-prosemirror

A React-based rendering engine for ProseMirror that allows developers to build rich text editors using React components for the editor view and individual document nodes (node views). It provides a controlled and uncontrolled <ProseMirror /> component, specialized hooks for safe EditorView and EditorState access (such as useEditorEffect and useEditorStateSelector), and a migration path for Tiptap users via the /tiptap entry point.

Tokens
13.5K
Snippets
34
Records
55
Agent score
63%

What's inside @handlewithcare/react-prosemirror

  1. Build React Node Views

    main

    To create custom node views using React components, pass your components to the nodeViewComponents prop of the ProseMirror component.

    Requirements for Node View Components:

    1. Ref Forwarding: Components must pass their ref to their top-level DOM element.
    2. Content DOM: Components that render children must pass nodeProps.contentDOMRef to the parent element of those children.
    3. Props: Components receive NodeViewComponentProps, which includes nodeProps (containing node, getPos, decorations, and contentDOMRef).

    Node View Hooks:

    • useNodePos(): Returns the current position of the node. Note: Only use this in callbacks or effects; using it in render will not trigger re-renders on position changes.
    • useIsNodeSelected(): Returns true if the node is currently selected.
    • useIsComposingIn(): Returns true if an IME composition is active inside the node.
    • useStopEvent(handler): Registers a stopEvent handler.
    • useIgnoreMutation(handler): Registers an ignoreMutation handler.
    • useSelectNode(selectNode, deselectNode): Registers selection handlers.
  2. How to access the EditorView safely with hooks

    main

    Because the EditorView relies on the DOM, accessing its methods (like coordsAtPos) during the React render cycle can lead to out-of-sync data. To safely interact with the EditorView from children of the <ProseMirror /> component, use these hooks:

    • useEditorEffect: Use this for side effects that depend on the DOM being in sync with the latest EditorState, such as positioning a floating widget relative to the user's cursor.
    • useEditorEventCallback: Use this to create stable function references for event handlers (like onClick) that need to access the EditorView to dispatch transactions.
    • useEditorEventListener: Use this if you need to listen to events originating inside the contenteditable element (e.g., keydown). These listeners are registered via ProseMirror's handleDOMEvents. Returning true or calling event.preventDefault() prevents other listeners from running.
    // Example: Using useEditorEffect to position a widget
    useEditorEffect((view) => {
      if (!view || !ref.current) return;
      const viewClientRect = view.dom.getBoundingClientRect();
      const coords = view.coordsAtPos(view.state.selection.anchor);
      ref.current.style.top = coords.top - viewClientRect.top;
      ref.current.style.left = coords.left - viewClientRect.left;
    });
  3. Migrate to React ProseMirror v2: Core Architecture Changes

    main

    In v2, the library moved from using React Portals to render into ProseMirror-managed DOM nodes to a model where React has full responsibility for rendering.

    Key improvements in v2:

    • Eliminated extra HTML wrappers: Unlike the v1 portal approach, v2 does not wrap custom node views in extra elements, making styling and valid DOM production easier.
    • Prevented state tearing: v2 avoids the 'double render' issue where React-based node views would render with stale state during a ProseMirror update.
    • Idiomatic React: The EditorView DOM update cycle is disabled, and the library uses a React-based implementation of the ProseMirror update algorithm.
  4. Migrate ProseMirror and ProseMirrorDoc props in v3

    main

    v3 simplifies how CSS classes and element attributes are passed to the editor. Redundant ways to set className have been removed.

    New Prop Locations:

    1. attributes.class on ProseMirror: Use this to set classes on the editor container.
    2. className on ProseMirrorDoc: Use this to set classes on the document content element.

    Changes to as prop: In v2, as accepted a JSX element (e.g., <article data-editor />). In v3, as accepts a component or an element type string (e.g., "article" or Article). Any additional props passed to the element in the as prop should now be passed directly to ProseMirrorDoc or via attributes on ProseMirror.

    function Editor() {
      return (
        <ProseMirror
          defaultState={EditorState.create({ schema, plugins: [reactKeys()] })}
        >
          {/* v3: Use 'as' for type and 'className'/'props' for attributes */}
          <ProseMirrorDoc as="article" className="editor" data-editor />
        </ProseMirror>
      );
    }
  5. Migrate Node View contentDOM handling in v3

    main

    In v3, the library no longer automatically detects contentDOM. Instead, it provides a contentDOMRef via nodeProps. If your node view component renders children, you must explicitly assign nodeProps.contentDOMRef to the parent element of those children.

    If your node view consists of a single element where the ref (the node's DOM element) and the contentDOM are the same, use the useMergedDOMRefs utility hook to combine them.

    import { useMergedDOMRefs } from "@handlewithcare/react-prosemirror";
    
    // For components with a single element (merging ref and contentDOM)
    function Paragraph({
      nodeProps,
      ref,
      children,
      ...props
    }: NodeViewComponentProps) {
      return (
        <p ref={useMergedDOMRefs(ref, nodeProps.contentDOMRef)} {...props}>
          {children}
        </p>
      );
    }
    
    // For components with nested children (explicitly assigning contentDOMRef)
    function Card({ nodeProps, ref, children, ...props }: NodeViewComponentProps) {
      return (
        <div ref={ref} {...props}>
          <div ref={nodeProps.contentDOMRef}>{children}</div>
        </div>
      );
    }
  6. Update ProseMirror component usage in v2

    main

    The ProseMirror component API has been updated:

    1. Mounting: Instead of using a mount prop with a ref to a DOM element, you must now render a ProseMirrorDoc component as a child of the ProseMirror component.
    2. Node Views: The nodeViews prop no longer follows the standard ProseMirror API. It is now a map from node type names to React components.
      • Important: This map must be memoized or defined outside of your React component to prevent unnecessary re-renders.
    3. Standard ProseMirror Node Views: If you need to pass standard ProseMirror node view constructors (non-React), use the customNodeViews prop instead of nodeViews.
  7. Migrate from @tiptap/react to @handlewithcare/react-prosemirror/tiptap

    main

    To avoid issues with state tearing, asynchronous rendering causing extra DOM nodes, and broken React context in portals, replace @tiptap/react with the React ProseMirror integration layer. This allows you to keep your existing Tiptap extensions and commands while using a safer React-based rendering system that doesn't require wrapping DOM nodes.

    Follow these steps to migrate:

    1. Replace useEditor with useTiptapEditor.
    2. Wrap EditorContent with TiptapEditorView and use TiptapEditorContent instead.
    3. Replace useEffect and useCallback that depend on the editor with useTiptapEditorEffect and useTiptapEditorEventCallback.
    4. Migrate custom node views using the tiptapNodeView HOC.
    // 1. Replace useEditor
    import { useTiptapEditor } from "@handlewithcare/react-prosemirror/tiptap";
    const editor = useTiptapEditor({ extensions });
    
    // 2. Replace EditorContent
    import {
      TiptapEditorView,
      TiptapEditorContent,
      useTiptapEditor,
    } from "@handlewithcare/react-prosemirror/tiptap";
    
    export function Editor() {
      const editor = useTiptapEditor({ extensions });
      return (
        <TiptapEditorView editor={editor}>
          <TiptapEditorContent editor={editor} />
        </TiptapEditorView>
      );
    }
    
    // 3. Replace useEffect/useCallback
    import { useTiptapEditorEffect, useTiptapEditorEventCallback } from "@handlewithcare/react-prosemirror/tiptap";
    
    useTiptapEditorEffect((editor) => {
      editor.commands.focus();
    }, [editor]);
    
    const onClick = useTiptapEditorEventCallback((editor) => {
      editor.commands.focus();
    });
    
    // 4. Migrate Node Views
    import { tiptapNodeView } from "@handlewithcare/react-prosemirror/tiptap";
    import { Node } from "@tiptap/core";
    import Paragraph from "./ParagraphView.jsx";
    
    const extension = Node.create({ name: "paragraph" });
    export const paragraph = tiptapNodeView({
      extension,
      component: Paragraph,
    });
    
    // Register in Editor
    const nodeViewComponents = { paragraph };
    <TiptapEditorView editor={editor} nodeViewComponents={nodeViewComponents}>
      <TiptapEditorContent editor={editor} />
    </TiptapEditorView>"}],
  8. Migrate ProseMirror view prop names in v3

    main

    To align with the ProseMirror EditorView API, v3 renames the props used to register node and mark views. This distinguishes between React components and native ProseMirror view constructors.

    Mapping for migration:

    • nodeViews (v2) $\rightarrow$ nodeViewComponents (v3) [for React components]
    • markViews (v2) $\rightarrow$ markViewComponents (v3) [for React components]
    • customNodeViews (v2) $\rightarrow$ nodeViews (v3) [for native constructors]
    • customMarkViews (v2) $\rightarrow$ markViews (v3) [for native constructors]
    function Editor() {
      return (
        <ProseMirror
          defaultState={EditorState.create({ schema, plugins: [reactKeys()] })}
          nodeViewComponents={nodeViewComponents} // Formerly nodeViews
          nodeViews={nodeViewConstructors}        // Formerly customNodeViews
        >
          <ProseMirrorDoc />
        </ProseMirror>
      );
    }
  9. Install @handlewithcare/react-prosemirror

    main

    Install the library along with its required peer dependencies.

    Important Compatibility Notes:

    1. ProseMirror View: Releases are coupled to specific prosemirror-view versions. Ensure your prosemirror-view version matches the one specified in the peer dependencies.
    2. React Reconciler: You must ensure your react-reconciler version matches your react and react-dom versions.

    React/Reconciler Compatibility Matrix:

    React versionReact Reconciler version
    19.x0.32.0
    >= 18.2.0, < 190.29.0
    18.1.x0.28.0
    18.0.x0.27.0
    17.x0.26.1

    CSS Requirement: You must import the prosemirror-view stylesheet for the editor to render correctly.

    npm install @handlewithcare/react-prosemirror \
        react@^19.1.0 \
        react-dom@^19.1.0 \
        react-reconciler@0.32.0 \
        prosemirror-view@1.42.0 \
        prosemirror-state \
        prosemirror-model
    // Import the required stylesheet
    import "prosemirror-view/style/prosemirror.css";
  10. Implement React-based Node Views in v2

    main

    React-based node views are now standard React components. You no longer need to provide a ProseMirror node view constructor function.

    Requirements for Node View Components:

    • Props: Components must accept NodeViewComponentProps.
    • Data Access: ProseMirror-specific data (the node, getPos function, and decorations) is now located inside the nodeProps property of the props object.
    • Prop Spreading: All props (except children and nodeProps) must be spread onto the root element of the component.
    • Ref Forwarding: All node view components must forward their ref to the root element of the component.
    • Functionality: For features typically handled in a node view spec (like event handling or selection), use provided hooks like useStopEvent and useSelectNode.
  11. Build React node views for ProseMirror

    main

    You can use React components as ProseMirror node views by passing a nodeViewComponents object to the <ProseMirror /> component.

    Requirements for Node View Components:

    1. Stable Reference: Define nodeViewComponents outside of your main editor component (or memoize it with useMemo) to prevent unnecessary re-renders.
    2. Refs: All node view components must pass a ref to their top-level DOM element. If using React <=18, use forwardRef to access the ref prop.
    3. Content DOM: Components that render children must pass nodeProps.contentDOMRef to the element that wraps the children. If the top-level element is also the content container, use useMergedDOMRefs(ref, nodeProps.contentDOMRef).
    4. Props Spreading: Spread all received props onto the top-level DOM element to ensure ProseMirror Decorations (attributes) are applied correctly.

    Complex Layouts: If your top-level element is a wrapper (e.g., a div containing a p), use the ref for the wrapper and nodeProps.contentDOMRef for the inner element.

    // Example: A simple Paragraph node view
    function Paragraph({ children, nodeProps, ref, ...props }) {
      return (
        <p
          {...props}
          ref={useMergedDOMRefs(ref, nodeProps.contentDOMRef)}
        >
          {children}
        </p>
      );
    }
    
    const nodeViewComponents = {
      paragraph: Paragraph,
    };
    
    // Usage
    <ProseMirror nodeViewComponents={nodeViewComponents} ... />
  12. Integrate Tiptap with React ProseMirror

    main
    The @handlewithcare/react-prosemirror package provides several components and hooks to bridge Tiptap with React-based ProseMirror implementations. You can use TiptapEditor as a high-level component, or use the useTiptapEditor hook for more granular control over the editor lifecycle. For custom node views within Tiptap, use tiptapNodeView to wrap React components.