Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

serializeHtml throws Invalid hook call error #2804

Closed
bfaulk96 opened this issue Dec 13, 2023 · 31 comments
Closed

serializeHtml throws Invalid hook call error #2804

bfaulk96 opened this issue Dec 13, 2023 · 31 comments

Comments

@bfaulk96
Copy link

bfaulk96 commented Dec 13, 2023

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

  • You can sponsor this specific effort via a Polar.sh pledge below
  • We receive the pledge once the issue is completed & verified
Fund with Polar
@bfaulk96 bfaulk96 added the bug Something isn't working label Dec 13, 2023
@ningdev1
Copy link

Is there any update?

@mkmrcodes
Copy link

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

@zbeyens
Copy link
Member

zbeyens commented Jan 24, 2024

No ETA until consulting request.

@leonardorb
Copy link

I'm having the same issue.

@juzjus10
Copy link

Having the issue too

@andshonia
Copy link

@zbeyens Any solution?

@andshonia
Copy link

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
Copy link

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

@shobhit5186
Copy link

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
Copy link

zy-zero commented Feb 23, 2024

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

@holycrypto
Copy link

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

@artur1214
Copy link

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
Copy link

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
Copy link
Collaborator

Lenghak commented May 5, 2024

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
Copy link

for me filtering toggle and blockSelection did 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
Copy link

for me filtering toggle and blockSelection did 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
Copy link

bagus2x commented Sep 22, 2024

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
Copy link

leonfortgenss commented Sep 23, 2024

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
Copy link
Member

zbeyens commented Sep 23, 2024

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
Copy link

I solved it by blocking the BlockSelectionPlugin
// BlockSelectionPlugin,

@Lenghak
Copy link
Collaborator

Lenghak commented Sep 24, 2024

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
Copy link
Member

zbeyens commented Sep 24, 2024

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

@syedaAnusha
Copy link

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
Copy link

pirmax commented Nov 12, 2024

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
Copy link
Member

zbeyens commented Nov 12, 2024

  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
Copy link

@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
Copy link

ericaig commented Nov 12, 2024

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
Copy link

Divanny commented Nov 14, 2024

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
Copy link

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

@felixfeng33
Copy link
Collaborator

we now have PlateStatic to serialize html in plate v41

@zbeyens
Copy link
Member

zbeyens commented Dec 20, 2024

We are deprecating @udecode/plate-html. The serializeHtml function has been migrated to @udecode/plate-core and @udecode/plate-common. The migration process for this stable fix requires creating a static version of all your components, that will make it work SSR.

You can find a detailed guide here. And the complete example including export to HTML, Tailwind CSS, and Prism.

2024-12-20 at 14 42 59

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests