udecode / plate

A rich-text editor powered by AI
https://platejs.org
Other
12.07k stars 736 forks source link

`serializeHtml` throws `Invalid hook call` error #2804

Open bfaulk96 opened 11 months ago

bfaulk96 commented 11 months ago

Description

When calling serializeHtml (from @udecode/plate-serializer-html), React throws an invalid hook error:

Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component.

Steps to Reproduce

I have a component, CustomPlateEditor. In this component, I have:

<DndProvider backend={HTML5Backend}>
  <Plate editorRef={editorRef} initialValue={initialValue ?? defaultInitialValue} onChange={onChange} plugins={PLATE_PLUGINS}>
    <FixedToolbar>
      <FixedToolbarButtons/>
    </FixedToolbar>
    <Editor focusRing={false} id='PlateEditor' />
    <FloatingToolbar>
      <FloatingToolbarButtons/>
    </FloatingToolbar>
  </Plate>
</DndProvider>

My editorRef is passed down from a parent component, which contains the following code:

export const SomeOtherComponent = () => {
  const editorRef = useRef<PlateEditor | null>(null);
  const [editorValue, setEditorValue] = useState<TElement[]>(defaultInitialValue);
  const [editorHtmlValue, setEditorHtmlValue] = useState('');

  const onEditorChange = (value: TElement[]) => {
    setEditorValue(value);
    if (!editorRef.current) return;

    const html = serializeHtml(editorRef.current, {
      nodes: editorRef.current.children,
      dndWrapper: (props) => <DndProvider backend={HTML5Backend} {...props} />
    });
    setEditorHtmlValue(html);
  };

  // Other logic here

  return <>
    {/* Other stuff here */}
    <CustomPlateEditor editorRef={editorRef} initialValue={editorValue} onChange={onEditorChange} />
  </>
}

When making any sort of change inside my Plate editor, I end up getting the invalid hook error, which I've narrowed down to only happening due to the nodes: editorRef.current.children part of the serializeHtml function. I've tried various other approaches including creating a temporary editor and passing in the editor value, but I get the same React hook error. I've checked that I don't have different versions of React running, so it seems to be an issue within @udecode/plate-serializer-html

Sandbox

I will make a code sandbox within a couple of days and update this issue.

Expected Behavior

I would expect that, with the given code I have in my app, making any change to the Plate rich text editor would result in a raw html string being set for my editorHtmlValue state value.

Environment

Funding

Fund with Polar

ningdev1 commented 10 months ago

Is there any update?

mkmrcodes commented 10 months ago

Same error. Here is the screenshot. Screenshot 2024-01-24 at 00 14 48

zbeyens commented 10 months ago

No ETA until consulting request.

leonardorb commented 10 months ago

I'm having the same issue.

juzjus10 commented 10 months ago

Having the issue too

andshonia commented 9 months ago

@zbeyens Any solution?

andshonia commented 9 months ago

After thorough debugging over several hours, it was determined that the issue arose from the inclusion of the createBlockSelectionPlugin with specific options, notably:

createBlockSelectionPlugin({ options: { sizes: { top: 0, bottom: 0, }, }, })

The solution involved filtering the array of plugins to exclude the blockSelection plugin:

const excludedSelectionPlugin = plugins?.filter(plugin => plugin?.key !== 'blockSelection');

Subsequently, the modified array excludedSelectionPlugin was passed to the createPlateEditor function:

serializeHtml(createPlateEditor({ plugins: excludedSelectionPlugin }), { nodes: textEditor, data, dndWrapper: props => <DndProvider backend={HTML5Backend} {...props} />, })

This adjustment successfully resolved the encountered issue.

Hope you will figure out the original issue which was caused by 'createBlockSelectionPlugin';

seanbruce commented 9 months ago

@andshonia Thank you. Your solution works in my case.

shobhit5186 commented 9 months ago

I have the same issue but the workaround is not working for me. It appears that other plugins might also have bug which is causing invalid hook call. Issue happens inside the serializeHtml() call.

zy-zero commented 9 months ago

image Here have the error message call serializeHtml function. how can I do to resolve this error?

holycrypto commented 8 months ago

Invalid hook call. Hooks can only be called inside of the body of a function component.

artur1214 commented 7 months ago

After thorough debugging over several hours, it was determined that the issue arose from the inclusion of the createBlockSelectionPlugin with specific options, notably:

createBlockSelectionPlugin({ options: { sizes: { top: 0, bottom: 0, }, }, })

The solution involved filtering the array of plugins to exclude the blockSelection plugin:

const excludedSelectionPlugin = plugins?.filter(plugin => plugin?.key !== 'blockSelection');

Subsequently, the modified array excludedSelectionPlugin was passed to the createPlateEditor function:

serializeHtml(createPlateEditor({ plugins: excludedSelectionPlugin }), { nodes: textEditor, data, dndWrapper: props => <DndProvider backend={HTML5Backend} {...props} />, })

This adjustment successfully resolved the encountered issue.

Hope you will figure out the original issue which was caused by 'createBlockSelectionPlugin';

I find out, that your solution is not working for me, and made some debugging. now I can See, that createTogglePlugin also causes this bug.

ankuragrwl commented 7 months ago

I was also facing the same issue:

image

Workaround (that worked for me): createPlateEditor({ plugins: plugins?.filter( (plugin) => plugin?.key !== 'toggle' && plugin?.key !== 'blockSelection' ), })

Lenghak commented 6 months ago

By filter out the toggle and blockSelection, the html serialization is now working for me. Furthermore, I do suggest adding context={window} on DndWrapper according to this comment: Multiple DndProviders inside a pure component can lead to Cannot have two HTML5 backends at the same time #3257, if anyone faces cannot have two HTML5Backend problem from DnDWrapper.

ajshovon commented 5 months ago

for me filtering toggleand blockSelectiondid not work. i needed to filter p also

const filteredPlugins = plugins?.filter((plugin) => plugin?.key !== 'toggle' && plugin?.key !== 'blockSelection' && plugin?.key !== 'p');

Latest v34

ajshovon commented 5 months ago

for me filtering toggleand blockSelectiondid not work. i needed to filter p also

const filteredPlugins = plugins?.filter((plugin) => plugin?.key !== 'toggle' && plugin?.key !== 'blockSelection' && plugin?.key !== 'p');

Latest v34

I removed the p filtering now it works without it!!!!

But i am facing another problem, i am desearilizing a html and showing it in the editor, but if there is any text, (p tag), the editor get reset if i type something

bagus2x commented 2 months ago

By creating a temporary editor and replacing serializeHtml with htmlReact, this solution worked for me

<Plate
    editor={editor}
    onChange={() => {
      const tmp = createPlateEditor({
        plugins: [HtmlReactPlugin],
      })
      const html= tmp.api.htmlReact.serialize({ nodes: editor.children })
      console.log(html)
    }}
  >
    <FixedToolbar>
      <FixedToolbarButtons />
    </FixedToolbar>
    <Editor />
    <FloatingToolbar>
      <FloatingToolbarButtons />
    </FloatingToolbar>
    <CommentsPopover />
</Plate>
leonfortgenss commented 2 months ago

By creating a temporary editor and replacing serializeHtml with htmlReact, this solution worked for me

<Plate
    editor={editor}
    onChange={() => {
      const tmp = createPlateEditor({
        plugins: [HtmlReactPlugin],
      })
      const html= tmp.api.htmlReact.serialize({ nodes: editor.children })
      console.log(html)
    }}
  >
    <FixedToolbar>
      <FixedToolbarButtons />
    </FixedToolbar>
    <Editor />
    <FloatingToolbar>
      <FloatingToolbarButtons />
    </FloatingToolbar>
    <CommentsPopover />
</Plate>

This worked for me! Lifesaver took me 2 days and your comment saved me.

zbeyens commented 2 months ago

You're omitting the components so this would serialize to default div and span without any styles. The workaround to above errors is to pass your own context-free components this way:

const tmp = createPlateEditor({
  plugins: [HtmlReactPlugin],
  override: {
    components: {
      p: HtmlParagraphElement,
      // ...
    }
  }
})
thiszhangjin commented 2 months ago

I solved it by blocking the BlockSelectionPlugin // BlockSelectionPlugin,

Lenghak commented 2 months ago

Doing like this doesn't it mean opting out the BlockSelectionPlugin feature on your editor? IMO, we only wanted to filter out the selection when the html serialization is needed.

zbeyens commented 2 months ago

We'll work on read-only components compatible with this serializer, that will also be useful for read-only editors.

syedaAnusha commented 3 weeks ago

Put editor.api.htmlReact.serialize({nodes:editor.children}) in a separate function. as it can't be use inside Plate's onChange handler.
Just a quick code for better understanding!, Hope this will help!

// PLATE COMPONENTS
import { Editor } from "@/components/plate-ui/Editor";
import { FixedToolbar } from "@/components/plate-ui/FixedToolbar";
import { FixedToolbarButtons } from "@/components/plate-ui/fixed-toolbar-buttons";
import { TooltipProvider } from "@/components/plate-ui/Tooltip";
import { useCreateEditor } from "@/hooks/useCreateEditor";
import { useEffect, useRef, useState } from "react";
import { Plate } from "@udecode/plate-common/react";

export const EditorComponent = () => {
  const [content, setContent] = useState<any>("");
  const [_html, setHTML] = useState<any>("");
  const [isClicked, setIsClicked] = useState<boolean>(false);
  const containerRef = useRef(null);
  const editor = useCreateEditor();

  useEffect(() => {
    console.log("CONTENT", content);
  }, [content, _html]);
  const converToHtml = () => {
    const html = editor.api.htmlReact.serialize({
      nodes: content,
      stripDataAttributes: false,
      stripWhitespace: false,
      // preserveClassNames: ["slate-h1", "slate-h2", "slate-h3"],
    });
    setHTML(html);
    console.log("Serialized HTML:", html);
    setIsClicked(true);
    console.log("HTML", _html);
  };

  return (
    <>
      <Plate
        editor={editor}
        onChange={({ value }) => {
          setContent(value);
        }}
      >
        <FixedToolbar className="sticky">
          <FixedToolbarButtons />
        </FixedToolbar>
        <TooltipProvider>
          <Editor ref={containerRef} className="h-auto" />
        </TooltipProvider>
      </Plate>
      <button
        className="border bg-purple-900 text-white text-2xl rounded-md p-2"
        onClick={converToHtml}
      >
        convert to HTML
      </button>

      {isClicked ? (
        <div
          className="w-full border border-black h-[100vh]"
          dangerouslySetInnerHTML={{ __html: _html }}
        />
      ) : null}
    </>
  );
};
pirmax commented 2 weeks ago

Could a kind soul give me some additional information on this useEditorRef must be used inside a Plate or PlateController problem?

I would like to revive the topic: I would like to render the HTML in relation to what was written, and if possible without a temporary editor (which is not, in my opinion, a viable solution).

zbeyens commented 2 weeks ago
  1. Copy/paste incompatible components (that use React context). For example block-selection.tsx -> block-selection-html.tsx
  2. Remove React context and state logic from those components
  3. Create an editor using those components
  4. editorHtml.api.htmlReact.serialize should work

This requires some effort. We'll abstract this as soon as we can.

syedaAnusha commented 2 weeks ago

@pirmax

Well, you can try to wrap up your editor component with Hoc. Like: const Editor = withHoc(Plate controller, ()=>{...your code}) Hopes, it will not throw an error.

ericaig commented 2 weeks ago

You're omitting the components so this would serialize to default div and span without any styles. The workaround to above errors is to pass your own context-free components this way:

const tmp = createPlateEditor({
  plugins: [HtmlReactPlugin],
  override: {
    components: {
      p: HtmlParagraphElement,
      // ...
    }
  }
})

I'm doing it like this as well, but I get plain divs with none of the applied styles:

<div>Playground</div><div class="slate-p">A rich-text editor with AI capabilities. Try the AI commands or use Cmd+J to open the AI menu.</div>

      const tmpEditor = createPlateEditor({
        plugins: [HtmlReactPlugin],
        override: {
          components: editorComponents,
        },
      })

      const html = tmpEditor.api.htmlReact.serialize({
        nodes: editor.children,
        convertNewLinesToHtmlBr: true,
        stripWhitespace: false,
        dndWrapper: (props) => (
          <DndProvider backend={HTML5Backend} {...props} />
        ),
      });

export const editorComponents = {
  // [AIPlugin.key]: AILeaf,
  [BlockquotePlugin.key]: BlockquoteElement,
  [BoldPlugin.key]: withProps(PlateLeaf, { as: "strong" }),
  [CodeBlockPlugin.key]: CodeBlockElement,
  [CodeLinePlugin.key]: CodeLineElement,
  [CodePlugin.key]: CodeLeaf,
  [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
  [ColumnItemPlugin.key]: ColumnElement,
  [ColumnPlugin.key]: ColumnGroupElement,
  [CommentsPlugin.key]: CommentLeaf,
  [DatePlugin.key]: DateElement,
  [EmojiInputPlugin.key]: EmojiInputElement,
  [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: "h1" }),
  [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: "h2" }),
  [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: "h3" }),
  [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: "h4" }),
  [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: "h5" }),
  [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: "h6" }),
  [HighlightPlugin.key]: HighlightLeaf,
  [HorizontalRulePlugin.key]: HrElement,
  [ImagePlugin.key]: ImageElement,
  [ItalicPlugin.key]: withProps(PlateLeaf, { as: "em" }),
  [KbdPlugin.key]: KbdLeaf,
  [LinkPlugin.key]: LinkElement,
  [MediaEmbedPlugin.key]: MediaEmbedElement,
  [MentionInputPlugin.key]: MentionInputElement,
  [MentionPlugin.key]: MentionElement,
  [ParagraphPlugin.key]: ParagraphElement,
  [SlashInputPlugin.key]: SlashInputElement,
  [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: "s" }),
  [SubscriptPlugin.key]: withProps(PlateLeaf, { as: "sub" }),
  [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: "sup" }),
  [TableCellHeaderPlugin.key]: TableCellHeaderElement,
  [TableCellPlugin.key]: TableCellElement,
  [TablePlugin.key]: TableElement,
  [TableRowPlugin.key]: TableRowElement,
  [TocPlugin.key]: TocElement,
  [TogglePlugin.key]: ToggleElement,
  [UnderlinePlugin.key]: withProps(PlateLeaf, { as: "u" }),
};
Divanny commented 1 week ago

You're omitting the components so this would serialize to default div and span without any styles. The workaround to above errors is to pass your own context-free components this way:

const tmp = createPlateEditor({
  plugins: [HtmlReactPlugin],
  override: {
    components: {
      p: HtmlParagraphElement,
      // ...
    }
  }
})

I'm doing it like this as well, but I get plain divs with none of the applied styles:

<div>Playground</div><div class="slate-p">A rich-text editor with AI capabilities. Try the AI commands or use Cmd+J to open the AI menu.</div>
      const tmpEditor = createPlateEditor({
        plugins: [HtmlReactPlugin],
        override: {
          components: editorComponents,
        },
      })

      const html = tmpEditor.api.htmlReact.serialize({
        nodes: editor.children,
        convertNewLinesToHtmlBr: true,
        stripWhitespace: false,
        dndWrapper: (props) => (
          <DndProvider backend={HTML5Backend} {...props} />
        ),
      });

export const editorComponents = {
  // [AIPlugin.key]: AILeaf,
  [BlockquotePlugin.key]: BlockquoteElement,
  [BoldPlugin.key]: withProps(PlateLeaf, { as: "strong" }),
  [CodeBlockPlugin.key]: CodeBlockElement,
  [CodeLinePlugin.key]: CodeLineElement,
  [CodePlugin.key]: CodeLeaf,
  [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
  [ColumnItemPlugin.key]: ColumnElement,
  [ColumnPlugin.key]: ColumnGroupElement,
  [CommentsPlugin.key]: CommentLeaf,
  [DatePlugin.key]: DateElement,
  [EmojiInputPlugin.key]: EmojiInputElement,
  [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: "h1" }),
  [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: "h2" }),
  [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: "h3" }),
  [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: "h4" }),
  [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: "h5" }),
  [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: "h6" }),
  [HighlightPlugin.key]: HighlightLeaf,
  [HorizontalRulePlugin.key]: HrElement,
  [ImagePlugin.key]: ImageElement,
  [ItalicPlugin.key]: withProps(PlateLeaf, { as: "em" }),
  [KbdPlugin.key]: KbdLeaf,
  [LinkPlugin.key]: LinkElement,
  [MediaEmbedPlugin.key]: MediaEmbedElement,
  [MentionInputPlugin.key]: MentionInputElement,
  [MentionPlugin.key]: MentionElement,
  [ParagraphPlugin.key]: ParagraphElement,
  [SlashInputPlugin.key]: SlashInputElement,
  [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: "s" }),
  [SubscriptPlugin.key]: withProps(PlateLeaf, { as: "sub" }),
  [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: "sup" }),
  [TableCellHeaderPlugin.key]: TableCellHeaderElement,
  [TableCellPlugin.key]: TableCellElement,
  [TablePlugin.key]: TableElement,
  [TableRowPlugin.key]: TableRowElement,
  [TocPlugin.key]: TocElement,
  [TogglePlugin.key]: ToggleElement,
  [UnderlinePlugin.key]: withProps(PlateLeaf, { as: "u" }),
};

You got a solution? I have same problem.

Nos43ratu commented 4 days ago

You're omitting the components so this would serialize to default div and span without any styles. The workaround to above errors is to pass your own context-free components this way:

const tmp = createPlateEditor({
  plugins: [HtmlReactPlugin],
  override: {
    components: {
      p: HtmlParagraphElement,
      // ...
    }
  }
})

I'm doing it like this as well, but I get plain divs with none of the applied styles:

<div>Playground</div><div class="slate-p">A rich-text editor with AI capabilities. Try the AI commands or use Cmd+J to open the AI menu.</div>
      const tmpEditor = createPlateEditor({
        plugins: [HtmlReactPlugin],
        override: {
          components: editorComponents,
        },
      })

      const html = tmpEditor.api.htmlReact.serialize({
        nodes: editor.children,
        convertNewLinesToHtmlBr: true,
        stripWhitespace: false,
        dndWrapper: (props) => (
          <DndProvider backend={HTML5Backend} {...props} />
        ),
      });

export const editorComponents = {
  // [AIPlugin.key]: AILeaf,
  [BlockquotePlugin.key]: BlockquoteElement,
  [BoldPlugin.key]: withProps(PlateLeaf, { as: "strong" }),
  [CodeBlockPlugin.key]: CodeBlockElement,
  [CodeLinePlugin.key]: CodeLineElement,
  [CodePlugin.key]: CodeLeaf,
  [CodeSyntaxPlugin.key]: CodeSyntaxLeaf,
  [ColumnItemPlugin.key]: ColumnElement,
  [ColumnPlugin.key]: ColumnGroupElement,
  [CommentsPlugin.key]: CommentLeaf,
  [DatePlugin.key]: DateElement,
  [EmojiInputPlugin.key]: EmojiInputElement,
  [HEADING_KEYS.h1]: withProps(HeadingElement, { variant: "h1" }),
  [HEADING_KEYS.h2]: withProps(HeadingElement, { variant: "h2" }),
  [HEADING_KEYS.h3]: withProps(HeadingElement, { variant: "h3" }),
  [HEADING_KEYS.h4]: withProps(HeadingElement, { variant: "h4" }),
  [HEADING_KEYS.h5]: withProps(HeadingElement, { variant: "h5" }),
  [HEADING_KEYS.h6]: withProps(HeadingElement, { variant: "h6" }),
  [HighlightPlugin.key]: HighlightLeaf,
  [HorizontalRulePlugin.key]: HrElement,
  [ImagePlugin.key]: ImageElement,
  [ItalicPlugin.key]: withProps(PlateLeaf, { as: "em" }),
  [KbdPlugin.key]: KbdLeaf,
  [LinkPlugin.key]: LinkElement,
  [MediaEmbedPlugin.key]: MediaEmbedElement,
  [MentionInputPlugin.key]: MentionInputElement,
  [MentionPlugin.key]: MentionElement,
  [ParagraphPlugin.key]: ParagraphElement,
  [SlashInputPlugin.key]: SlashInputElement,
  [StrikethroughPlugin.key]: withProps(PlateLeaf, { as: "s" }),
  [SubscriptPlugin.key]: withProps(PlateLeaf, { as: "sub" }),
  [SuperscriptPlugin.key]: withProps(PlateLeaf, { as: "sup" }),
  [TableCellHeaderPlugin.key]: TableCellHeaderElement,
  [TableCellPlugin.key]: TableCellElement,
  [TablePlugin.key]: TableElement,
  [TableRowPlugin.key]: TableRowElement,
  [TocPlugin.key]: TocElement,
  [TogglePlugin.key]: ToggleElement,
  [UnderlinePlugin.key]: withProps(PlateLeaf, { as: "u" }),
};

@Divanny

U need to specify every className that your custom components uses.

For example u have smth like this

export const ParagraphElement = withRef<typeof PlateElement>(
  ({ children, className, ...props }, ref) => {
    return (
      <PlateElement
        ref={ref}
        className={cn("m-0 px-0 py-1", className)}
        {...props}
      >
        {children}
      </PlateElement>
    );
  },
);

you need to specify every className in serializer

    const html = tmp.api.htmlReact.serialize({
      nodes: editor.children,
      stripDataAttributes: false,
      stripWhitespace: false,
      dndWrapper: props => <DndProvider backend={HTML5Backend} {...props} />,
      preserveClassNames: [
        "relative",
        "m-0",
        "px-0",
        "py-1",
        "slate-p",
        "slate-selectable",
      ],
    });

hope this helps