Yilmaz
Yilmaz

Reputation: 49331

React Markdown type for paragraph renderer

I could not find anything, neither on GitHub nor index.d.ts of the package react.markdown. It looks like a very simple example, yet the entire Google does not contain any example. I wrote a custom renderer for a Markdown component, but I could not figure out the type for the renderer:

import ReactMarkdown, { Renderers, Renderer, NodeType } from "react-markdown";

const PostContent: React.FC<PostContent> = ({ blog }) => {
  const customRenderers: Renderers = {
    paragraph(paragraph) {
      const { node } = paragraph;
      if (node.children[0].type === "image") {
        const image = node.children[0];
        return (
          <div style={{ width: "100%", maxWidth: "60rem" }}>
            <Image
              src={`/images/posts/${blog.slug}/${image.src}`}
              alt={image.alt}
              width={600}
              height={300}
            />
          </div>
        );
      }
    },
  };

  return (
    <article className="">
      <ReactMarkdown renderers={customRenderers}>{blog.content}</ReactMarkdown>
    </article>
  );
};

What is the type of paragraph? I checked; it is Renderer:

   type Renderer<T> = (props: T) => ElementType<T>

I could not figure out what to pass as T. I tried, HtmlParagraphElement or any.

Upvotes: 0

Views: 5182

Answers (2)

Sehrish Waheed
Sehrish Waheed

Reputation: 1565

Just change 'renderer' to 'components':

<ReactMarkdown components={customRenderers}>{blog.content}</ReactMarkdown>

Upvotes: 0

Linda Paiste
Linda Paiste

Reputation: 42218

Renderer Type

The react-markdown package is very loosely typed. It declares the type of renderers as an object map

{[nodeType: string]: ElementType}

where the keys can be any string (not just valid node types) and the values have the type ElementType imported from the React typings. ElementType means that your renderer can be a built-in element tag name like "p" or "div" or a function component or class component that takes any props.

You could just type your object as

const customRenderers: {[nodeType: string]: ElementType} = { ...

Typing the Props

ElementType is not at all useful for getting type safety inside the render function. The type says the the props can be anything. It would be nice if we could know what props our render function is actually going to be called with.

Our paragraph gets called with props node and children. A code element gets called with props language, value, node and children. The custom props like the language and value are unfortunately not documented in Typescript anywhere. You can see them being set in the getNodeProps function of the react-markdown source code. There are different props for each node type.

Typing the Node

The props node and children are where we can actually get useful Typescript information.

The react-markdown types show that the type for a node is the Content type imported from the underlying markdown parser package mdast. This Content type is the union of all individual markdown node types. These individual types all have a distinct type property which a string literal that matches the key that we want to set on our renderers object!

So finally we know that the type for valid keys is Content["type"]. We also know that the node prop for a specific K key will be Extract<Content, { type: K }> which gives us the member of the union that matches this type property.

The children prop on the props object is just a typical React children prop, but not all node types have children. We we can know whether or not our props include children by looking at the type for the node and seeing whether or not it has a children property.

type NodeToProps<T> = {
  node: T;
  children: T extends { children: any } ? ReactNode : never;
};

(this matches the received props because the children prop is always set, but will be undefined if children are not supported)

So now we can define a strict type for your customRenderers -- or any custom renderer map:

type CustomRenderers = {
  [K in Content["type"]]?: (
    props: NodeToProps<Extract<Content, { type: K }>>
  ) => ReactElement;
};

Conditional Overriding

Your code will intercept all paragraph nodes, but will only return any content when the condition node.children[0].type === "image" is met. That means all other paragraphs get removed! You need to make sure that you always return something.

const PostContent: React.FC<PostContent> = ({ blog }) => {
  const customRenderers: CustomRenderers = {
    // node has type mdast.Paragraph, children is React.ReactNode
    paragraph: ({ node, children }) => {
      if (node.children[0].type === "image") {
        const image = node.children[0]; // type mdast.Image
        return (
          <div
            style={{
              width: "100%",
              maxWidth: "60rem"
            }}
          >
            <img
              src={`/images/posts/${blog.slug}/${image.src}`}
              alt={image.alt}
              width={600}
              height={300}
            />
          </div>
        );
      } 
      // return a standard paragraph in all other cases
      else return <p>{children}</p>;
    },
  };

  return (
    <article className="">
      <ReactMarkdown renderers={customRenderers}>{blog.content}</ReactMarkdown>
    </article>
  );
};

Code Sandbox Link

Upvotes: 4

Related Questions