next.js: "getStaticProps is not defined" with mdx files

Bug report

I’m trying to use nextjs and mdx to build a simple site. When I export getStaticProps, I get an “undefined” error. It looks like an issue that happens client side.

To Reproduce

I followed the “with-mdx” example to add mdx pages to my application. https://github.com/zeit/next.js/tree/canary/examples/with-mdx

I try to generate static props from the mdx using exports (https://mdxjs.com/getting-started#exports)

// src/pages/index.mdx

# Helloworld

Content is here!

export function getStaticProps() {
    return {
        props: {hello: 'world'}
    }
}
// src/_app.tsx
...

export default function App(app: AppProps) {
  const { Component, pageProps } = app;

  return (<MDXProvider components={components}>
      <pre>{JSON.stringify(pageProps)}</pre>
      <Component {...pageProps} />
    </MDXProvider>
  );
}

I get an undefined error:

ReferenceError: getStaticProps is not defined
Module../src/pages/index.mdx
./src/pages/index.mdx:25
  22 | const layoutProps = {
  23 |   layout,
  24 | hello,
> 25 | getStaticProps
  26 | };
  27 | const MDXLayout = "wrapper"
  28 | export default function MDXContent({

The <pre>{hello: "world"}</pre> appears on my webpage. It looks like this error is client side only, and the code behave as expect on the server.

Screenshot of the full error below.

Expected behavior

I expect to see the props and the content.

Screenshots

Screenshot 2020-04-20 at 18 35 00

System information

  • OS: macOS
  • Version of Next.js: 9.3.4
  • Version of Node.js: 12.14.0
    "@mdx-js/loader": "^1.5.8",
    "@next/mdx": "^9.3.5",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "isomorphic-unfetch": "^3.0.0",
    "next": "^9.3.4",
    "pg": "^8.0.2",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "swr": "^0.2.0",
    "unfetch": "^4.1.0"

Thanks for supporting this project!

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 13
  • Comments: 23 (12 by maintainers)

Most upvoted comments

The problem is that MDX adds getStaticProps to layoutProps, but then next/babel removes the export, so now the reference is missing. I think you can patch the issue by creating this Babel plugin, which removes all data fetching functions from layoutProps:

/**
 * Currently it's not possible to export data fetching functions from MDX pages
 * because MDX includes them in `layoutProps`, and Next.js removes them at some
 * point, causing a `ReferenceError`.
 *
 * https://github.com/mdx-js/mdx/issues/742#issuecomment-612652071
 *
 * This plugin can be removed once MDX removes `layoutProps`, at least that
 * seems to be the current plan.
 */

// https://nextjs.org/docs/basic-features/data-fetching
const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps']

module.exports = () => {
  return {
    visitor: {
      ObjectProperty(path) {
        if (
          DATA_FETCH_FNS.includes(path.node.value.name) &&
          path.findParent(
            (path) =>
              path.isVariableDeclarator() &&
              path.node.id.name === 'layoutProps',
          )
        ) {
          path.remove()
        }
      },
    },
  }
}

Then add it to your Babel configuration, let’s say we call it babel-plugin-nextjs-mdx-patch.js:

{
  "presets": ["next/babel"],
  "plugins": ["./babel-plugin-nextjs-mdx-patch"]
}

I get a similar issue, but with async, after writing:

export async function getStaticProps(){
   return { props: { data: await myData() } }
}

image

Rewriting to:

export const getStaticProps = async () => {
   return { props: { data: await myData() } }
}

Just the same as you:

image

You have to export getStaticProps from the MDX page itself, not layout.

There is one syntax that seems to circumvent this incompatibility between MDX and Next.js:

export { getStaticProps } from 'path/to/module'

# Title

Your MDX content...

But I’d say that it’s somewhat of a bug in MDX that this works because all exports are meant to be passed as props to layout, e.g. export const meta = { ... } is a common pattern, and I’d expect export { meta } from './other-data to behave the same, but it doesn’t.

Spent some time trying to figure this out and can confirm that MDX V2 does fix this issue. If you’re ok being on the next version, you just need to install "@mdx-js/loader": "next" and set the loader up yourself.

I have some work I want to do in getStaticProps for all of my mdx pages (much like @kud, I think).

Providing a custom renderer for @mdx-js/loader seems like a promising approach.

next.config.js

const mdxRenderer = `
  import React from 'react'
  import { mdx } from '@mdx-js/react'

  export async function getStaticProps () {
    return {
      props: {
        foo: 'bar'
      }
    }
  }
`

const withMdx = require('@next/mdx')({
  options: {
    renderer: mdxRenderer
  }
})

module.exports = withMdx({
  pageExtensions: ['tsx', 'mdx'],
})

layout.tsx

import React from 'react'

interface Props {
  foo: string
}

const Layout: React.FC<Props> = ({ children, foo }) => (
  <div>
    <p>The static prop value is: {foo}</p>
    {children}
  </div>
)

export default Layout

The static props seem to get passed into my layout just like I wanted, but I haven’t thoroughly tested yet.

My next thought is whether I can export additional static prop getter functions from mdx pages to merge into the getStaticProps function defined in the mdx renderer.

No problem, glad it could help! In case you need extra markdown features like rendering tables, I ran into this issue switching to latest. You can fix it by adding remark-gfm:

{ loader: '@mdx-js/loader', options: { remarkPlugins: [gfm] }}

@souporserious Thanks for this! I installed the next version and I’m able to now export getStaticProps from my mdx pages, making life much easier. I’m fine with working around any stability issues as this is for a small personal blog anyway.

I’ve noticed that with @mdx-js/loader (latest or next) and xdm, using getStaticProps in .mdx files isn’t equivalent to a .tsx file.

For example, suppose I do something like what @leerob does with next-mdx-remote, but in my .mdx file:

https://github.com/leerob/leerob.io/blob/9992324086e07c1dfb31e8b0629a034da1810a03/pages/blog/[slug].js#L41-L42

---
title: My Test Page
---

import { getFiles, getFileBySlug } from '@/lib/mdx';

...Some content goes here...

export async function getStaticProps({ params }) {
  const post = await getFileBySlug('blog', params.slug);
  const tweets = await getTweets(post.tweetIDs);

  return { props: { ...post, tweets } };
}

I’ll get this error, presumably because getStaticProps and it’s imports aren’t being stripped from the client-side bundle:

…/node_modules/fs.realpath/index.js:8:0 Module not found: Can’t resolve ‘fs’ null

For comparison, a vanilla test.tsx page can use the utilities just fine and Next.js seems to strip getStaticProps and its dependencies:

import { getFiles, getFileBySlug } from '@/lib/mdx';

import { getContentPaths } from "@/utils/getContentPaths";

export default function Test() {
  return <h1>Test</h1>;
}

export async function getStaticProps({ params }) {
  const post = await getFileBySlug('blog', params.slug);
  const tweets = await getTweets(post.tweetIDs);

  return { props: { ...post, tweets } };
}

Thank you so much for this explanation, and for your time. I’ll give a try of this!

This remark plugin could look something like this (adjust the logic accordingly because file.path is a full path):

const remarkMdxFromNow = () => (tree, file) => {
  const LANG = getLangFromPathname(file.path)

  const splitUri = file.path.split("/")
  const homeUrl = `/${splitUri[1]}/${splitUri[2]}`
  const date = dayjs(getDateFromPath(file.path))
    .locale(LANG)
    .format("DD MMMM YYYY")
  const fromNow = dayjs(getDateFromPath(file.path)).locale(LANG).fromNow()

  file.data.fromNow = fromNow
}

module.exports = remarkMdxFromNow

Now you saved it to file.data, and you could build a separate plugin that inserts an export statement into your MDX file, you can copy my remark-mdx-export-file-data.

Then you would apply these two plugins like this:

const fromNow = require('./etc/remark-mdx-from-now')
const exportFileData = require('./etc/remark-mdx-export-file-data')

// ...

{
  remarkPlugins: [
    fromNow,
    [exportFileData, ['fromNow']],
  ]
}

Now your MDX file will both export fromNow and provide it to the layout, if one is provided. Notice that you can use remark-mdx-export-file-data for exporting anything attached to file.data.

But this is obviously a temporary solution, MDX should provide a much better interface to do stuff like this. I haven’t been following closely, but I think that’s what MDX v2 will do.

About the image, it means I can’t use remark-images for instance. It also breaks some editors like markdown preview on VS Code 😬 (which can be fixed for sure).

I didn’t try to solve this problem before, so I don’t know if there’s a way. 🤷

Super interesting! Thank you very much @silvenon

The thing is I would like to do “lots of stuff” so it could be a bit annoying to put all this logic inside each mdx as my mdx files are especially posts for my blog.

Definitely annoying! This is why I went a little nuts in my configuration by building custom unified plugins for MDX, for example:

Depending on what you want you might find them useful, they are written to be reusable and the tests should help you figure out how they work. I was planning to publish them, but it seems like MDX v2 is moving towards this direction anyway.

I wondered if this kind of global state stuff could be done on getInitialProps in a Custom App which puts the data an own Context Provider. Would that work with fully static sites?

No, getInitialProps is run by the server. 😕