How To Add Monaco Editor to a Next.js app
Bottom Line Up Front
I use a slightly modified version of the steps mentioned in this GitHub comment. Modifications were necessary because I use TailwindCSS with Next.js.
- YouTube recording (18 mins)
- Dev.to Embed:
Motivations
Monaco Editor is [the open source editor] (https://github.com/microsoft/monaco-editor) used in VS Code, which itself is open source. I used to write my blogposts in VS Code, and as I make my own Dev.to CMS, I wanted to have all the familiar trappings of Monaco to help me out while I write.
Problems
However there are some issues we have to deal with:
- Monaco is framework agnostic, so it requires writing some React bindings.
- You could do it yourself, but also you could just skip that and use https://github.com/react-monaco-editor/react-monaco-editor
- Monaco is written for a desktop Electron app, not for a server-side rendered web app.
- This is solved by using
import dynamic from "next/dynamic"
and making Monaco a dynamic import.
- This is solved by using
- Monaco also wants to offload syntax highlighting to web workers, and we need to figure that out
- Next.js doesn’t want any dependencies importing CSS from within
node_modules
, as this assumes a bundler and loader setup (e.g. webpack) and can have unintentional global CSS side effects (all global CSS is intended to be in_app.js
).- we can re-enable this with
@zeit/next-css
andnext-transpile-modules
- we can re-enable this with
We can solve this with a solution worked out by Elliot Hesp on GitHub and a config from Joe Haddad of the Next.js team.
Solution
The solution I use is informed by my usage of Tailwind CSS, which requires a recent version of PostCSS, which @zeit/next-css
only has at 3.0 (because it is deprecated and not maintained).
I also use TypeScript, which introduces a small wrinkle, because Monaco Editor attaches a MonacoEnvironment
global on the window
object - I just @ts-ignore
it.
// next.config.js
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const withTM = require("next-transpile-modules")([
// `monaco-editor` isn't published to npm correctly: it includes both CSS
// imports and non-Node friendly syntax, so it needs to be compiled.
"monaco-editor"
]);
module.exports = withTM({
webpack: config => {
const rule = config.module.rules
.find(rule => rule.oneOf)
.oneOf.find(
r =>
// Find the global CSS loader
r.issuer && r.issuer.include && r.issuer.include.includes("_app")
);
if (rule) {
rule.issuer.include = [
rule.issuer.include,
// Allow `monaco-editor` to import global CSS:
/[\\/]node_modules[\\/]monaco-editor[\\/]/
];
}
config.plugins.push(
new MonacoWebpackPlugin({
languages: [
"json",
"markdown",
"css",
"typescript",
"javascript",
"html",
"graphql",
"python",
"scss",
"yaml"
],
filename: "static/[name].worker.js"
})
);
return config;
}
});
and then in your Next.js app code:
import React from "react";
// etc
import dynamic from "next/dynamic";
const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false });
function App() {
const [postBody, setPostBody] = React.useState("");
// etc
return (<div>
{/* etc */}
<MonacoEditor
editorDidMount={() => {
// @ts-ignore
window.MonacoEnvironment.getWorkerUrl = (
_moduleId: string,
label: string
) => {
if (label === "json")
return "_next/static/json.worker.js";
if (label === "css")
return "_next/static/css.worker.js";
if (label === "html")
return "_next/static/html.worker.js";
if (
label === "typescript" ||
label === "javascript"
)
return "_next/static/ts.worker.js";
return "_next/static/editor.worker.js";
};
}}
width="800"
height="600"
language="markdown"
theme="vs-dark"
value={postBody}
options={{
minimap: {
enabled: false
}
}}
onChange={setPostBody}
/>
</div>)
}
Since I’m using Tailwind, I’m also using PostCSS, which also tries to eliminate Monaco’s CSS. You have to tell it to ignore that:
// postcss.config.js
const purgecss = [
"@fullhuman/postcss-purgecss",
{
// https://purgecss.com/configuration.html#options
content: ["./components/**/*.tsx", "./pages/**/*.tsx"],
css: [],
whitelistPatternsChildren: [/monaco-editor/], // so it handles .monaco-editor .foo .bar
defaultExtractor: content => content.match(/[\w-/.:]+(?<!:)/g) || []
}
];
Catch up on the Dev.to CMS LiveStream!
- Day 1 - Setup Next.js and Tailwind UI, list posts through API routes - 90 mins
- Day 2 - setting up a Markdown Editor with Next.js, Tailwind UI, Highlight.js, React Hook Form, and React Query - 3 hours
- Quick Fix - How To Add Monaco Editor to a Next.js app - 18 mins
- Day 3 - Refactoring to Edit Existing Posts - 3 hours
- Day 4 - Polish Day! Implementing Notifications, Markdown preview, and programmatic Redirects, and Using Web Components in Next.js - 3 hours
- Instant GraphQL with OneGraph - Screenshares in Public with Sean Grove - refactoring handrolled Dev.to API access with OneGraph and GraphQL
- How and Why to Un-Reset Tailwind’s CSS Reset