How to implement a simple React Playground

The playground has two parts:

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.

The React Playground in action

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

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

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>

Resouces

Want to learn more?