RSS
 

Posts Tagged ‘ReactJS’

Adding Markdown (mdx) Support to NextJS with App Router

04 Jul

Markdown allows text content to be written with implicit formatting that nearly matches how one would write text without thought of formatting. A lot of web content is also simply text content; a lot of web content is built on the ReactJS library; and NextJS has become a popular framework, extending ReactJS. With that, I was motivated to use the Markdown content of my NextJS web app, but I had trouble simply relying on their documentation, so I thought I would document what I got working.

  1. Use Markdown for a text-heavy portion of a page of my web app, and
  2. To be able to have an entire page of content come from a single Markdown page

The former is a feature of NextJS to allow markdown content to be imported as a “component,” which can be rendered like any component in ReactJs.

The latter would rely NextJS’s App Router that assumes many URL paths for the app parallel the file/directory hierarchy of the app itself. So, merely placing a “page” file in the app’s directory will create a new URL path for the website. That means that you can create a “page.md” or “page.mdx” file with Markdown text content to define the content for that URL. .mdx files also allow JSX syntax so that React/Next components can be embedded within the Markdown.

Setup

The first issue was getting things set up. Here were the steps that worked for me.

  1. Add packages. The highlighted line was missing from the documentation.
    npm install @mdx-js/loader @mdx-js/react @next/mdx
    npm install -D @types/mdx
    
  2. In next.config.*, add
    1. import withMDX from "@next/mdx";
    2. Add “md” (optional) and “mdx” extensions for App Router support: nextConfig.pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx']
    3. Create, then wrap the Next config with mdx-configwithMDX = createMDX({})export default withMDX(nextConfig)

    Note: I had to avoid using .ts to avoid syntax errors to support the “remark-gfm” plugin. I recommend using .js or .mjs extensions, I chose the latter.

    import type {NextConfig} from 'next';
    
    const nextConfig: NextConfig = {
      typescript: {
        ignoreBuildErrors: true,
      },
      eslint: {
        ignoreDuringBuilds: true,
      },
    };
    
    export default nextConfig;
    asdfasdf asdf asdf asd f
    import type {NextConfig} from 'next';
    import withMDX from "@next/mdx";
    
    const nextConfig: NextConfig = {
      pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
      typescript: {
        ignoreBuildErrors: true,
      },
      eslint: {
        ignoreDuringBuilds: true,
      },
    };
    
    const withMdxConfig = withMDX({
      options: {
        remarkPlugins: [],
        rehypePlugins: [],
      },
    });
    
    export default withMdxConfig(nextConfig);
  3. Add the mdx-components.tsx file to src/. This file allows overriding how tags and components will be rendered, providing a mapping from tag/component name to an overriding render function; see the official documentation, mdx-components.js, but I will elaborate on how we’ll use this in the following sections.
    import type { MDXComponents } from 'mdx/types'
    
    export function useMDXComponents(components: MDXComponents): MDXComponents {
       return {
          ...components,
       }
    }
    
  4. Help VSCode resolve imports
    1. Open settings
    2. Search for “file associations”
    3. Add item key = *.mdx, value = markdown. (Articles say, “react-markdown,” but that didn’t work for me).
  5. Styles will need to be defined for all the generated HTML tags. See the next sections. 

Defining Markdown Output Style

Markdown syntax is translated to HTML. By default, the HTML tags have had their formatting stripped, for some reason. That means that the tags need to have style/class definitions applied to render properly. See How to use markdown and MDX in Next.js, for the official word.

The following are tags that correspond to Markdown syntax:

  Markdown HTML
Headings # Level 1
## Level 2
### Level 3

###### Level 6
<h1>Level 1</h1>
<h2>Level 2</h2>
<h3>Level 3</h3>

<h6>Level 6</h6>
Bold, Italic, Bold Italic * and _ are interchangeable
**bold** or __bold__
*italic* or _italic_
***bold-italic***
<b>bold</b>
<i>italic</i>
<b><i>bold-italic</i></b> or <i><b>bold-italic</b></i>
Bullet list - Item 1
- Item 2
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
Ordered list 1. Item 1
2. Item 2
<ol>
<li>Item 1</li>
<li>Item 2</li>
</ol>
Unformatted 

```
unformatted 
   text
```

<pre>
unformatted
  text
</pre>

Link [description](http://…) <a href="http://…">description</a>
Plain or paragraph text text … <p>text …</p>
Checkbox (an extended feature supported by the remark-gfm plugin) 

- [ ]Incomplete task
- [x]Completed task

<ul class="contains-task-list">
  <li class="task-list-item"><input disabled="" type="checkbox">Incomplete task</li>
  <input disabled="" type="checkbox"><li class="task-list-item" checked>Completed task</li>
</ul>

By default, NextJS removes the styling for the Markdown HTML tags, so we have to define them. 

Wrapper

Styles and classnames can be defined for any tag and component by redefining it to render with a style or className property, as desired. Instead, we will take advantage of a special Component named “wrapper” that wraps any .mdx content and define tag styles within that container context. Then we define all tag’s styles in an importable CSS module. 

import type { MDXComponents } from 'mdx/types'
import styles from './mdx-components.module.css'

export function useMDXComponents(components: MDXComponents): MDXComponents { 
   return { 
      ...components, 
      wrapper: ({ children }) => <div className={styles.mdxWrapper}>{children}</div>,
   } 
}

The file name and the wrapper class names are irrelevant so long as they match. Here is a good starting point for the “mdx-components.module.css” that I use. Note the class name, “_mdxWrapper” parent style definition. 

/**
 * Style definitions for tags corresponding to Markdown for MDX components
 * This file provides a reset for common HTML elements used in MDX content
 * to ensure consistent styling.
 */
._mdxWrapper {
  h1, h2, h3, h4, h5, h6, p, blockquote, pre, table {
    display: block;
    margin-block: initial;
    margin-inline: initial;
  }

  h1, h2, h3, h4, h5, h6 { font-weight: bold; }

  h1 {
    border-top: 1px solid lightgray;
    margin-block-start: 0.67em;
    font-size: 2em;
  }
  h2 { margin-block-start: 0.83em; font-size: 1.5em; }
  h3 { margin-block-start: 1em;    font-size: 1.17em; }
  h4 { margin-block-start: 1.67em; font-size: 1em; }
  h5 { margin-block-start: 1.33em; font-size: 0.83em; }
  h6 { margin-block-start: 2.33em; font-size: 0.67em; }

  p { margin-block: 0.4em; }

  a {
    color: -webkit-link;
    text-decoration: underline;
    cursor: pointer;
  }

  ul, ol {
    display: block;
    padding-inline-start: 1.5625rem;
  }
  ul { list-style-type: disc; }
  ul ul { list-style-type: circle; }
  ul ul ul { list-style-type: square; }
  ol { list-style-type: decimal; }
  
  blockquote {
    margin-block: 1em;
    margin-inline: 40px;
  }
  code, pre {
    font-family: monospace;
  }
  pre {
    display: block;
    margin-block: 1em;
    white-space: pre;
  }

  img {
    display: inline-block;
    max-width: 100%;
    height: auto;
  }

  table {
    /* Table styling variables */
    /* --table-border-color: blue; */
    /* --table-border-width: 2px;  */    /* Enable to non-0 to surround table with border */
    /* --table-header-background-color: #f2f2f2; */ /* Enable for distinct header background	color */
    --table-header-border-color: #BBB;   /* Defaults to cell border color */
    --table-header-border-width: 2px;    /* Defaults to row border width */
    --table-body-row-border-width: 1px;  /* Enable to non-0 show row dividers */
    --table-body-col-border-width: var(--table-body-row-border-width,1px);  /* Enable to non-0 show col dividers */
    --table-body-cell-border-color: lightgray;  /* Override border color */
    --table-body-background-color: white;/* Even row background color */
    --table-body-background-color-alt: #f9f9f9; /* Odd row background color */
    margin-left: var(--table-body-row-border-width,0);
    border-collapse: collapse;
    text-indent: initial;
  }
  thead, tbody, tfoot {
    vertical-align: middle;
  }
  thead {
    border-color: var(--table-border-color);
    border-width: var(--table-border-width,0) var(--table-border-width,0) 0 var(--table-border-width,0);
  }
  tbody {
    border-color: var(--table-border-color);
    border-width: 0 var(--table-border-width,0) var(--table-border-width,0) var(--table-border-width,0);
  }
  thead th {
    background-color: var(--table-header-background-color, inherit);
    /* border-style: solid; */
    border-color: var(--table-header-border-color, var(--table-body-cell-border-color));
    border-bottom-width: var(--table-header-border-width,var(--table-body-row-border-width,0));
  }
  tr {
    vertical-align: inherit;
    border: none; /* Disable row borders */
    border-color: inherit;
  }
  td, th {
    /* No top border, conditional side/bottom borders */
    border-width: 0 var(--table-body-col-border-width, 0px) var(--table-body-row-border-width, 0px) var(--table-body-col-border-width, 0px);
    border-color: var(--table-body-cell-border-color);
    padding-inline: 8px;
  }
  /* No side borders */
  td:last-child, th:last-child { border-right: none; }
  td:first-child, th:first-child { border-left: none; }

  /* Alternating background colors for table rows */
  tbody tr:nth-child(odd)  { background-color: var(--table-body-background-color); }
  tbody tr:nth-child(even) { background-color: var(--table-body-background-color-alt); }
} /* ._mdxWrapper */

Checkbox Support via “remark-gfm”

Github markup is supported via the remark-gfm plugin, however I was never able to get it to work as documented (i.e., import and add  remarkGfm  to the plugins array. Instead I had to use the string name of the plugin and add an array to the plugins array:

const withMdxConfig = withMDX({
  options: {
    remarkPlugins: [['remark-gfm']],
    rehypePlugins: [],
  },
});

export default withMdxConfig(nextConfig);
import withMDX from "@next/mdx"
import remarkGfm from 'remark-gfm'

const nextConfig: NextConfig = {
  pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],
}

const withMdxConfig = withMDX({
  options: {
    // If you use remark-gfm, you'll need to use next.config.mjs
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})

export default withMdxConfig(nextConfig);

The styling for task/checkboxes needs to be set. Unfortunately, remark-gfm uses class names to identify the list and list item tags. Since importing a CSS modules mangles names, we have to import the CSS definitions directly. We can create a CSS file, randomly named, “remark-gfm.css”

/* remarkGfm MDX Checkbox support for remarkGfm */
ul.contains-task-list li.task-list-item {
  list-style-type: none; /* Remove list style for task list items */
  text-indent: -18px;    /* ...and outdent it so it aligns        */
}
/* Undo <li> inherited styling within nested chckbox lists */
ul.contains-task-list li:not(.task-list-item) {
  list-style-type: initial;
  text-indent: initial;
}

Then import this directly into “mdx-components.tsx.” Alternatively you can add these style definitions in global.css.

import type { MDXComponents } from 'mdx/types'
import styles from './mdx-components.module.css'
import './remark-gfm.css'

export function useMDXComponents(components: MDXComponents): MDXComponents {
   return {
      ...components,
      wrapper: ({ children }) => <div className={styles._mdxWrapper}>{children}</div>,
   }
}

Note: This style definition will affect all content on the page, even outside an MDX component. 

Summary

I was led down a bunch of rabbit holes due to NextJS’s Markdown support not matching the documented steps. With the steps described in this post, others should be able to get it working without the headaches I had to go through.

  1. Npm-install mdx support packages
  2. Update VSCode file association settings (optionally)
  3. Update “next.config.mjs”
  4. Create src/mdx-components.tsx and import a CSS module to define and wrap tag style definitions
  5. If Github markdown support is desired, add the plugin and import CSS definitions into mdx-components.tsx

Then you can use Markdown content for App Router pages as page.mdx and React components, implicitly.

# Page Title

This is an example a NextJS App Router page, written with Markdown syntax. 

 

import UpdateContent from "./updates.mdx" // Markdown content

export default async function UpdatesPage() { // App Router page component

return (
  <>
     <h1>Updated News</h1>
     <UpdateContent/> {/* Render Markdown component */}
  </>
)

 

 
 

ReactJS: componentWillReceiveProps() is Dead! (*sniff*)

21 Apr

ReactJSThings evolve. ReactJS evolves. With version 16.3, there were several changes to the component life-cycle methods. In particular, componentWillReceiveProps is disappearing. In its place, they say, you can use the getDerivedStateFromProps static function. I found this a bit challenging, but I did find an interesting pattern when the component-state was dependent on fetched information.

I should mention that I had a specific goal to better encapsulate data within the component. While I could pass in all the needed data as properties, that would require the surrounding component know what to pass and how to get it. That shouldn’t necessarily be necessary; the component knows what it needs and how to retrieve it.

For example, say you have a component which accepts a phone number and displays the phone number and the state that it’s from. Certainly, you could write a simple component that accepts both pieces of information as properties.

<ShowPhoneLocation number="+12065551212" city="Seattle" />

Which might be implemented as:

class ShowPhoneLocation extends React.Component {
  render() {
    return (
      <div>{this.props.number} is from {this.props.city}</div>
    )
  } // render()
} // class ShowPhoneLocation

But, since the component should be able to infer the state from the phone number (by its area code), it shouldn’t be incumbent on its container to know what it is.

class ShowPhoneLocation extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    let location = getCityFromPhone(nextProps.number)
    return {
      city: location
    }
  }
  render() {
    return (
      <div>{this.props.number} is from {this.state.city}</div>
    )
  } // render()
} // class ShowPhoneLocation

That’s all well and good, but what if getCityFromPhone() has to call a web service? We don’t want getDerivedStateFromProps() to stall, waiting for a response. However, it is static and does not have a this reference to the object for which it is returning state; so an asynchronous fetch doesn’t know what object’s state to set. Instead, don’t wait for the result to save in the state, save the request’s Promise in the state and update the state, once the promise resolves.

function getCityFromPhone(number) {
  return fetch('http://saas.com/get/'+number+'/city') // Returns fetch promise
}
class ShowPhoneLocation extends React.Component {
  static getDerivedStateFromProps(nextProps, prevState) {
    let location = getCityFromPhone(nextProps.number)
    return {
      city: location
    }
  }
  componentDidUpdate() {
    let location = this.state.city
    if (location instanceof Promise) {
      this.setState({ city: '...waiting...' })
      location.then(city => this.setState({ city: city }) )
        .catch(() => this.setState({ city: 'not found' }) )
    }
  }
  render() {
    return (
      <div>
        {this.props.number} is from {this.state.city instanceof Promise
         ? '...'
        : this.state.city}</div>
    )
  } // render()
} // class ShowPhoneLocation

In componentDidUpdate() you can define the completion handlers to set the object’s state, base on the returned information from the service.

It is a common pattern to perform a fetch in componentDidMount(). The problem is that there may not be enough information to perform the fetch, that early, or the information for the fetch changes after the component has been mounted.

I am going to miss componentWillReceiveProps()… without it, things become a bit more convoluted but it’s going the way of the Dodo.