Next.js MDX App Router Gotcha

Jan 05, 2024

Photo by Annie Spratt on Unsplash

Overview

The move from Pages to the App Router in Next.js 13 / 14 has brought some big feature changes but also a lot of divided opinion. I'm split at the moment. There are things I like and which have made life easier but other features which feel un-natural and verbose and also very tightly coupled to Next.js and having to run your project on Vercel.

I'm finding little gotchas (and some not so little) as I'm moving work over in various projects from the pages model to the App Router router model. One such gotcha I am documenting here for future reference and if it helps anyone else.

I'm using MDX in a lot of work as it allows really rich documents to be created. While moving to the App Router I started to receive a Next.js client side error prompting me to add 'use client' when displaying MDX pages and had I read the docs more closely I would have seen the solution and notes about some additional code now required for the app router. But I thought I'd document here as the solution follows a similar pattern to a lot of issues when moving to the app router from the pages model.

TL / DR

Add a component called mdx-components.tsx at the root of application, either the /src folder or the parent folder of your /app folder. This is a component which is used to wrap the MDX content in a client side provider and allows MDX to work with the App Router.

App Router

There are a lot of things to get your head around moving form the Pages model to App Router where all your pages now reside in an /app folder and not the /pages folder.

When displaying any MDX pages with the App Router I was receiving the classic client side error 'use client'

This can appear a lot as you work with with the App Router as all pages and sub components/layouts are by default server side rendered and not client side rendered as is the case in the previous pages model. This means that if you use any client side code in your pages you need to add a 'use client' directive at the top of that code file which converts it to be client side rendered and not rendered from the server side. What's interesting is that you can still pass server components to your client components by passing them as children and they will continue function as server components from the client component.

But any components / pages that have client side code like useState, useEffect, useRef etc. all need to be have the 'use client' directive added to work as client side components or you will receive the dreaded Add the "use client" directive at the top of the file error. This also applies to anything using createContext, useContext which can be a common pattern to share state between components.

I've become quite used to doing this and thinking this way. Where you use providers (which by default now require client code) and want to be wrapped in layouts and pages you can create a custom file to wrap the provider and add 'use client' to make it work.

// ClientSessionProvider.tsx
// An example of a custom file used to wrap a the next-auth provider in
// client side directive so it can be used in server side page

"use client";

import { SessionProvider } from "next-auth/react";

export interface AuthContextProps {
  React.ReactNode;
}

export default function ClientSessionProvider({ children }: AuthContextProps) {
  return <SessionProvider>{children}</SessionProvider>;
}

this can then be used in a server side rendered file such as a layout file like this and which will allow the layout file to be still be server side rendered by default while encapsulating the client side provider.

// Layout.tsx
// An example of a layout file which wraps the custom AuthContext component

import React from "react";
import ClientSessionProvider from "./ClientSessionProvider";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <ClientSessionProvider>
      <div className="container mx-auto px-4">{children}</div>
    </ClientSessionProvider>
  );
}

Going back to the 'use client' error I was receiving, I couldn't at first figure out where I was receiving the client error from. I checked page.tsx and layout.tsx and all looked fine. On closer looking at the error I could see it was surfacing from the underlying @mdx-js/react library itself (this is installed as part of the packages to use MDX with Next.js).

Something more fundamental with my configuration / setup was wrong. Checking next.config.js I could see this all looked good and wrapping the nextConfig with withMDX.

const withMDX = require("@next/mdx")();

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configure `pageExtensions` to include MDX files
  pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
  // Optionally, add any other Next.js config below
};

module.exports = withMDX(nextConfig);

So what was wrong and what was causing this client side error and wanting me to add the 'use client' directive somewhere ?

Well it is there in black and white in the Next.js MDX docs (although you could easily miss it) with a side note for the App Router and MDX.

There is a requirement and convention based approach that there needs to be a component called mdx-components.tsx in the root of the /app folder. This is a component which is used to wrap the MDX content in a client side provider and is used by the App Router to render the MDX content.

Without it the MDX content will not render and you will receive the client side Add the "use client" directive at the top of the file error.

So here is the code for the mdx-components.tsx component which is required in the root of application, either the /src folder or the parent folder of your /app folder

// mdx-components.tsx
// This is required in the /app folder for MDX to work with the App Router
import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
  }
}

As soon as this is added to the root of the /app folder everything works and MDX content is hydrated and appears. As this is a wrapper to the MDX content you can add custom components to it as well and overwrite existing behaviour.

// mdx-components.tsx
// This is required in the /app folder for MDX to work with the App Router
import type { MDXComponents } from 'mdx/types'
...

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
    h1: (props) => <h1 style={{ color: '#cccccc' }} {...props} />,
    MyCustomComponent: MyCustomComponent,
  }
}

Conclusion

Again like so many things, the fix is simple but I thought I'd document the fix but also some of the context of what lies behind the fix and why it is required as this patterns applies to many areas when working with the Next.js App Router.