DEV Community

Cover image for Hot Reload MDX changes in Next.js and Nx
Juri Strumpflohner for Nx

Posted on • Edited on • Originally published at blog.nrwl.io

Hot Reload MDX changes in Next.js and Nx

In the previous article we learned how to use next-mdx-remote to load and hydrate MDX content. In this article, we're going to learn how to implement a custom server for our Next.js app with Nx, that allows us to auto-refresh the rendering whenever something in our MDX files changes.

Having the live website (running locally on the computer) automatically refresh and reflect the changes made in Markdown is very convenient while writing a new blog article. The common behavior is to auto-refresh the page whenever something in the markdown (MDX) content changes. While this works for our Next.js components, we have to add support for our MDX files.

What is Fast Refresh aka Hot Reloading

Here's a quick excerpt from the official Next.js docs.

Fast Refresh is a Next.js feature that gives you instantaneous feedback on edits made to your React components. Fast Refresh is enabled by default in all Next.js applications on 9.4 or newer. With Next.js Fast Refresh enabled, most edits should be visible within a second, without losing component state.

This works out of the box for Next.js and obviously also with the Nx integration. Whenever you change something in a Next.js component, you should see a small Vercel logo appear at the lower right corner of the open browser window, fast refreshing the current page. The important part here is that it doesn't simply do a Browser refresh, but auto reloads the component, thus you should not lose any current component state.

We definitely want this type of behavior also for our MDX pages, so let's see how we can implement that.

Using next-remote-watch

There's a package next-remote-watch that allows doing exactly that. As their official GitHub account documents, after installing the package, simply change the npm scripts to the following:

// ...
"scripts": {
-  "start": "next dev"
+  "start": "next-remote-watch"
}
Enter fullscreen mode Exit fullscreen mode

โš ๏ธ WARNING (from the library GitHub repo): next-remote-watch utilizes undocumented APIs from next.js that are not subject to semantic versioning. This means that any version bump to next.js, major, minor, or patch, could cause it to break without warning. If you decide to adopt this package, you should lock your next.js version to patch, and be careful when upgrading.

The downside of using this package is that it controls the entire process, so rather than going through next dev, it handles the instantiation of the dev server on its own.

How it works

next-remote-watch uses chokidar to watch for file changes and then invokes a private Next.js API to signal the rebuild and reload of the page.

Something like

chokidar
  .watch(articlesPath, {
    usePolling: false,
    ignoreInitial: true,
  })
  .on('all', async (filePathContext, eventContext = 'change') => {
    // CAUTION: accessing private APIs
    app['server']['hotReloader'].send('building');
    app['server']['hotReloader'].send('reloadPage');
  });
Enter fullscreen mode Exit fullscreen mode

Note: As you can see, using such a private API is quite risky, so make sure you freeze the Next.js version and you test things accordingly when you upgrade to a new Next.js release.

Implementing Fast Refresh

By using next-remote-watch, all the Nx specific setup is being bypassed, since the script invokes the Next.js development server directly. We can however implement it with Nx ourselves in a quite easy and straightforward way.

The Nx Next.js executor (@nrwl/next:server) allows you to implement a custom server.

A custom server is basically a function with a certain signature that we register on our Nx Next.js executor. The file itself can be created wherever we want. We could simply add it to our Next.js app, but since it can be reused across different apps, but isn't really something that would require a dedicated library, I'm placing the file in the tools/next-watch-server folder.

// tools next-watch-server/next-watch-server.ts

import { NextServer } from 'next/dist/server/next';
import { NextServerOptions, ProxyConfig } from '@nrwl/next';

export default async function nextWatchServer(
  app: NextServer,
  settings: NextServerOptions & { [prop: string]: any },
  proxyConfig: ProxyConfig
) {
  ...
}
Enter fullscreen mode Exit fullscreen mode

Nx passes the instantiated Next.js app, the settings passed to the executor (these are the options configured in workspace.json) and the proxyConfig (if provided). These properties can then be used to implement the watch logic:

// tools/next-watch-server/next-watch-server.ts
import { NextServer } from 'next/dist/server/next';
import { NextServerOptions, ProxyConfig } from '@nrwl/next';

const express = require('express');
const path = require('path');
const chokidar = require('chokidar');

export default async function nextWatchServer(
  app: NextServer,
  settings: NextServerOptions & { [prop: string]: any },
  proxyConfig: ProxyConfig
) {
  const handle = app.getRequestHandler();
  await app.prepare();

  const articlesPath = '_articles';

  // watch folders if specified
  if (articlesPath) {
    chokidar
      .watch(articlesPath, {
        usePolling: false,
        ignoreInitial: true,
      })
      .on('all', async (filePathContext, eventContext = 'change') => {
        // CAUTION: accessing private APIs
        app['server']['hotReloader'].send('building');
        app['server']['hotReloader'].send('reloadPage');
      });
  }

  const server = express();
  server.disable('x-powered-by');

  // Serve shared assets copied to `public` folder
  server.use(
    express.static(path.resolve(settings.dir, settings.conf.outdir, 'public'))
  );

  // Set up the proxy.
  if (proxyConfig) {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const proxyMiddleware = require('http-proxy-middleware');
    Object.keys(proxyConfig).forEach((context) => {
      server.use(proxyMiddleware(context, proxyConfig[context]));
    });
  }

  // Default catch-all handler to allow Next.js to handle all other routes
  server.all('*', (req, res) => handle(req, res));

  server.listen(settings.port, settings.hostname);
}
Enter fullscreen mode Exit fullscreen mode

The implementation is basically copying the Nx default Next.js server (see here) and adding the watch implementation using chokidar to watch the specified folder.

Finally, we need to pass the new custom server to the executor configuration in the workspace.json

{
  "version": 2,
  "projects": {
    "site": {
      "root": "apps/site",
      ...
      "targets": {
        ...
        "serve": {
          "executor": "@nrwl/next:server",
          "options": {
            "buildTarget": "site:build",
            "dev": true,
            "customServerPath": "../../tools/next-watch-server/next-watch-server.ts"
          },
          ...
        },
       ...
      }
    },
  },
  ...
}
Enter fullscreen mode Exit fullscreen mode

To test this, change something in the current MDX file you're visualizing and hit save. You should see the Next.js fast refresh icon appear at the lower right corner, fast refreshing your changes.

Optional: Using an env variable for our _articles path

Right now we have our _articles path in two different places, so it might be something we might want to factor out. By using environment variables for instance.

Step 1: Refactor our code to use environment variables

First of all, let's open our [slug].tsx file where we specify our POSTS_PATH variable. let's move it into the getStaticProps and getStaticPaths function as those have full access to the node environment.

Furthermore, we change them as follows:

+ const POSTS_PATH = join(process.cwd(), '_articles');
- const POSTS_PATH = join(process.cwd(), process.env.articleMarkdownPath);
Enter fullscreen mode Exit fullscreen mode

Similarly in our tools/next-watch-server/next-watch-server.ts

export default async function nextWatchServer(
  app: NextServer,
  settings: NextServerOptions & { [prop: string]: any },
  proxyConfig: ProxyConfig
) {
  const handle = app.getRequestHandler();
  await app.prepare();

- const articlesPath = '_articles';
+ const articlesPath = process.env.articleMarkdownPath;

  // watch folders if specified
  if (articlesPath) {
    chokidar
      .watch(articlesPath, {
        usePolling: false,
        ignoreInitial: true,
      })
      .on('all', async (filePathContext, eventContext = 'change') => {
        // CAUTION: accessing private APIs
        app['server']['hotReloader'].send('building');
        app['server']['hotReloader'].send('reloadPage');
      });
  }
...
Enter fullscreen mode Exit fullscreen mode

Step 2: Specify the environment variables

Now that we refactored all our hard-coded values, let's go and specify our environment variables. We have two options for that

  1. create a .env.local file at the root of our Nx workspace
  2. use the env property in our app's next.config.js

The Next docs have guides for both, using the Next config as well as creating a .env file. Which one you're using merely depends on the type of environment key. Since we're technically in a monorepo, adding a .env.local key is global to the monorepo and thus would not easily allow us to customize it per application. Instead, specifying the environment variable in the next.config.js of our app, makes the key local to our application.

// apps/site/next.config.js
const withNx = require('@nrwl/next/plugins/with-nx');

module.exports = withNx({

  // adding a env variable with Next
  env: {
      articleMarkdownPath: '_articles',
  },
});
Enter fullscreen mode Exit fullscreen mode

In this specific example of a blog platform and given we have the _articles folder at root of our monorepo vs within the application itself, I'm proceeding with option 1).

At the root of the monorepo, create a new .env.local file and add the following:

articleMarkdownPath = '_articles'
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this article, we learned

  • What fast refresh is about and what options we have at the moment of writing this article to implement it
  • How to create a custom Next.js server with Nx and TypeScript
  • How to use the custom Next.js server to implement fast refresh for our MDX files
  • How to use environment variables with Next.js and Nx

See also:

GitHub repository

All the sources for this article can be found in this GitHub repositoryโ€™s branch:
https://github.com/juristr/blog-series-nextjs-nx/tree/05-hot-reload-mdx


Learn more

๐Ÿง  Nx Docs
๐Ÿ‘ฉโ€๐Ÿ’ป Nx GitHub
๐Ÿ’ฌ Nrwl Community Slack
๐Ÿ“น Nrwl Youtube Channel
๐Ÿฅš Free Egghead course
๐Ÿง Need help with Angular, React, Monorepos, Lerna or Nx? Talk to us ๐Ÿ˜ƒ

Also, if you liked this, click the โค๏ธ and make sure to follow Juri and Nx on Twitter for more!

#nx

Top comments (0)