I've found examples of building a code editor within React, but the npm packages that are used throw a window / navigator not found in NextJS. This is how I got around that.
Create App
First create your app
npx create-next-app@latest
then I am going to install Tailwind
run npm run dev
and bam! Let's set up our folder structure. Create a components folder in the root directory and add 4 files: editor.js
, editorApp.js
, editor.module.css
, and useLocalStorage.js
.
Sweet, we are ready to build this editor.
editor.module.css
@media (min-width: 500px) {
.topPane {
display: flex;
}
}
Skeleton
Let's start by adding to our editor.js
(you'll need to install the classnames module)
const Editor = () => {
return(
<div>Editor</div>
)
}
export default Editor;
Then we'll import that into our editorApp.js
(3 times for html, css, and js)
import Editor from "./editor";
import * as style from "./editor.module.css";
import cn from "classnames";
const EditorApp = () => {
return (
<>
<div className={cn("bg-zinc-500", style.topPane)}>
<Editor />
<Editor />
<Editor />
</div>
<div style={{ height: "68vh" }}>
<iframe
title="output"
sandbox="allow-scripts"
frameBorder="0"
height="100%"
width="100%" />
</div>
</>
)
};
export default EditorApp;
And then we can rip out all the boiler code in index.js
and import our editorApp
import CodeEditor from "../components/editorApp";
export default function Home() {
return (
<CodeEditor />
)
}
You should see Editor 3 times on your screen. Cool.
Editor.js
We need to install codemirror, so run npm install react-codemirror2-react-17 codemirror --save
Let's import the styles for codeMirror and our theme. You can test different themes here, and we will also import the codemirror editor.
import "codemirror/lib/codemirror.css";
import "codemirror/theme/tomorrow-night-bright.css";
import { Controlled as ControlledEditor } from "react-codemirror2-react-17";
We have to import the languages differently as to not throw a ReferenceError of navigator is not defined.
let languageLoaded = false;
if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
require("codemirror/mode/xml/xml");
require("codemirror/mode/css/css");
require("codemirror/mode/javascript/javascript");
languageLoaded = true;
}
We are going to add create our editor now, with a title and body. This includes our props and handleChange
function.
import "codemirror/lib/codemirror.css";
import "codemirror/theme/tomorrow-night-bright.css";
import { Controlled as ControlledEditor } from "react-codemirror2-react-17";
let languageLoaded = false;
if (typeof window !== "undefined" && typeof window.navigator !== "undefined") {
require("codemirror/mode/xml/xml");
require("codemirror/mode/css/css");
require("codemirror/mode/javascript/javascript");
languageLoaded = true;
}
const Editor = (props) => {
const { language, editorTitle, value, onChange, className } = props;
function handleChange(editor, data, value) {
onChange(value);
}
return(
<div className={className}>
<div className="flex justify-between text-white rounded-t-lg py-2 pr-2 pl-4 bg-zinc-700">
{editorTitle}
</div>
<ControlledEditor
onBeforeChange={handleChange}
value={value}
className="grow rounded-b-lg overflow-hidden text-left"
options={{
lineWrapping: true,
lint: true,
mode: language,
theme: "tomorrow-night-bright",
lineNumbers: true,
}}
/>
</div>
)
}
export default Editor;
editorApp.js
We need to fill out our <Editor />
now. Add a
const [html, setHtml] = useState('');
(import useState)
and for the first editor
<Editor
language="xml"
editorTitle="HTML"
value={html}
onChange={setHtml}
/>
Copy this pattern for css and javascript.
Then we are going to add a useEffect to set the iframe srcDoc.
const [srcDoc, setSrcDoc] = useState("");
useEffect(() => {
const timeout = setTimeout(() => {
setSrcDoc(`
<html lang="en">
<body>${html}</body>
<style>${css}</style>
<script>${js}</script>
</html>
`);
}, 200);
return () => clearTimeout(timeout);
}, [html, css, js]);
and add the srcDoc prop as srcDoc to the iframe. You should have
import { useState, useEffect } from "react";
import Editor from "./editor";
import * as style from "./editor.module.css";
import cn from "classnames";
const EditorApp = () => {
const [html, setHtml] = useState("");
const [css, setCss] = useState("");
const [js, setJs] = useState("");
const [srcDoc, setSrcDoc] = useState("");
useEffect(() => {
const timeout = setTimeout(() => {
setSrcDoc(`
<html lang="en">
<body>${html}</body>
<style>${css}</style>
<script>${js}</script>
</html>
`);
}, 200);
return () => clearTimeout(timeout);
}, [html, css, js]);
return (
<>
<div className={cn("bg-zinc-500", style.topPane)}>
<Editor
className="px-2 py-3 md:w-1/3 md:pl-3 md:pr-2"
language="xml"
editorTitle="HTML"
value={html}
onChange={setHtml}
/>
<Editor
className="px-2 py-3 md:w-1/3 md:px-2"
language="css"
editorTitle="CSS"
value={css}
onChange={setCss}
/>
<Editor
className="px-2 py-3 md:w-1/3 md:pl-1 md:pr-3"
language="javascript"
editorTitle="JS"
value={js}
onChange={setJs}
/>
</div>
<div style={{ height: "68vh" }}>
<iframe
srcDoc={srcDoc}
title="output"
sandbox="allow-scripts"
frameBorder="0"
height="100%"
width="100%"
/>
</div>
</>
);
};
export default EditorApp;
You should now be able to type in the the editors and see your results displayed.
useLocalStorage.js
Last step.
In this component, we use localStorage
, which will either throw a localStorage is not defined
or window is not defined
in NextJS.NextJS pre-renders every page which results in Static Site Generation or Server Side Rendering. localStorage
is a property of the window object which is available on the client side / browser.
To get around this, we first have to check if the window is defined.
import { useEffect, useState } from "react";
function getStorageValue(key, initialValue) {
if (typeof window !== "undefined") {
const savedValue = localStorage.getItem(key);
return savedValue !== null ? JSON.parse(savedValue) : initialValue;
}
}
export const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
return getStorageValue(key, initialValue);
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
};
Import and replace useState in editorApp.js
for your html, css, and js with useLocalStorage
const [html, setHtml] = useLocalStorage("html", "");
const [css, setCss] = useLocalStorage("css", "");
const [js, setJs] = useLocalStorage("js", "");
Boom
You should now have a working codeEditor. If you want to prefill any section with code, just put your code in the empty "" ex. const [html, setHtml] = useLocalStorage("html", "<div>foo</div>");
. Keep in mind that localStorage is being saved so you may need to clear your browsing data one to many times.
Getting Started
npm run dev
# or
yarn dev
Open http://localhost:3000 with your browser to see the result.
This is my first post, I hope it was easy to follow and helps someone out :-)
Top comments (0)