How to implement a simple React Playground
The playground has two parts:
- The code editor: Allows us to enter the React code we want to evaluate
- The code evaluation sandbox: Evaluates (executes) the React code from the code editor and creates the corresponding DOM
On the left, we have the editor and on the right, we have the sandbox iframe. Each time the code is changed, the sandbox evaluates the code again.
Setting up the sandbox
We run everything in the browser, so we need to load the two React scripts: react.js
and react-dom.js
.
<!-- Load React. -->
<!-- Note: when deploying, replace "development.js" with "production.min.js". -->
<script
src="https://unpkg.com/react@17/umd/react.development.js"
crossorigin
></script>
<script
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
crossorigin
></script>
We also want JSX, so we need Babel to transpile JSX to plain JavaScript. Usually, we use Babel at build time, but here we need it at runtime, so we need to use the standalone version.
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
When we need to traspile code, we use Babel like this:
try {
let result = Babel.transform(code, {
presets: ["env", "es2015", "react"],
}).code;
Function(result)(window);
} catch (err) {}
Every time we change the code we want to evaluate, we need to get rid of the current evaluation result (all DOM nodes created by the current evaluation).
There are several ways to do this, but I prefer to put the code evaluation piece in a separate iframe, sandbox.html
<!DOCTYPE html>
<html>
<head>
<script
src="https://unpkg.com/react@17/umd/react.development.js"
crossorigin
></script>
<script
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
crossorigin
></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script>
let origin;
// listen for messages
window.addEventListener("message", (e) => {
origin = e.source;
document.getElementById("error").style.display = "none";
document.getElementById("app").style.display = "block";
const { code } = e.data;
if (code) {
// here the code is evaluated in the iframe
try {
let result = Babel.transform(code, {
presets: ["env", "es2015", "react"],
}).code;
Function(result)(window);
} catch (err) {
document.getElementById("app").style.display = "none";
document.getElementById("error").style.display = "block";
document.getElementById("error").innerHTML = err.message;
}
}
});
</script>
</head>
<body>
<pre id="error"></pre>
<div id="app"></div>
</body>
</html>
Inside the iframe, we have
- a
pre
that displays the error details if necessary - a div with id “app” that is the container used by the React. Only one of them is displayed at a time.
Code editor
We already covered setting up CodeMirror, so we won’t do that here.
The part I want to highlight here is the code evaluation( calling evaluateCode()
) which is done
- when the page is loaded
- each time the user changes the code in the editor
const editor = new EditorView({
state: EditorState.create({
extensions: [
basicSetup,
javascript(),
myTheme,
EditorView.updateListener.of((v) => {
if (v.docChanged) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
evaluateCode(editor.state.doc.toString());
}, 500);
}
}),
],
doc: "",
}),
parent: document.getElementById("editor"),
});
// first time evaluation
window.addEventListener("DOMContentLoaded", (event) => {
setTimeout(() => {
evaluateCode(editor.state.doc.toString());
}, 1000);
});
The evaluation function simply sends the code to the sandbox iframe:
const evaluateCode = (code) => {
console.clear();
try {
const sandbox = document.getElementById("sandbox");
sandbox.contentWindow.postMessage({ code }, "*");
} catch (err) {
console.error(err);
}
};
Putting them together
Inside index.html
we load the editor.bundle.js
which will create the code editor and also the sandbox iframe.
Note that we need to set some properties to allow the iframe to execute the code we send to it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<header>React Playground</header>
<main>
<div id="editor"></div>
<!-- SANDBOX -->
<iframe
id="sandbox"
src="./sandbox.html"
sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>
</main>
<!-- EDITOR -->
<script src="editor.bundle.js"></script>
</body>
</html>