Have you ever wondered how online React editors like CodesandBox and Codepen are implemented? If you’ve ever used Semantic React or React Styleguidist, just modify the above example in your browser and get a live preview.

This weekend I finally put the pieces together and implemented a simple solution. This article is an overview of the implementation process. If you still don’t know the final result, I suggest you browse to the bottom of the article and try out the embedded code editor first.

All right, let’s cut to the chase.

Challenges we need to overcome

  • Convert JSX/ES6 in the browser
  • Module processing, we may introduce modules in the editor
  • How do I parse and modify Javascript code

Dependency packages used

  • @babel/standaloneConvert in the browserJSX/ES6
  • acornParse JS into AST
  • escodegenConvert the modified AST back to JS
  • debounce, object-path

strategy

It’s surprisingly easy. Here are some steps:

  1. conversionJSX/ES6code
  2. In the transformed code, find a JSX expression. We’ll get to that after we go through the AST processing section.
  3. conversionJSXExpression to “wrap it into”renderIn the way
  4. Create a function that contains the code generated above and inject the dependency as an argument.
  5. Whenever the code changes, the function of Step 4 is called


Mentally? Don’t worry, let’s just look at the example.

Suppose we start with a piece of code like this:



How do I get this code to render on our web page?

Our task now is to transform the code above, process the button component introduced, and render JSX on line 12.


Here is the converted version:



Here’s what we need to generate “dynamically” :



After we generate the function above, we can call it by passing a React object, a render function, and a module handler function as arguments.

Also, notice that we include line 10 of the transformed code in the call to the render function.


Hopefully you got the whole idea. So let’s look at some concrete code.

import React from "react"; import ReactDOM from"react-dom"; import ObjPath from"object-path"; import * as Acorn from"acorn"; import { generate as generateJs } from"escodegen"; import { transform as babelTransform } from"@babel/standalone";function isReactNode(node) {    const type = node.type; //"ExpressionStatement"    const obj = ObjPath.get(node, "expression.callee.object.name");    const func = ObjPath.get(node, "expression.callee.property.name");    return (        type= = ="ExpressionStatement" &&        obj === "React" &&        func === "createElement"); }export function findReactNode(ast) {    const { body } = ast;    returnbody.find(isReactNode); }export function createEditor(domElement, moduleResolver = () => null) {    function render(node) {        ReactDOM.render(node, domElement);    }    function require(moduleName) {        return moduleResolver(moduleName);    }    function getWrapperFunction(code) {        try {            // 1. transform code            const tcode = babelTransform(code, { presets: ["es2015"."react"] })                .code;            // 2. get AST            const ast = Acorn.parse(tcode, {                sourceType: "module"            });            // 3. find React.createElement expression in the body of program            const rnode = findReactNode(ast);            if (rnode) {                const nodeIndex = ast.body.indexOf(rnode);                // 4. convert the React.createElement invocation to sourceand remove the trailing semicolon const createElSrc = generateJs(rnode).slice(0, -1); // 5. transform React.createElement(...) to render(React.createElement(...) ), / /where render is a callback passed from outside                const renderCallAst = Acorn.parse(`render(${createElSrc})`)                    .body[0];                ast.body[nodeIndex] = renderCallAst;            }            // 6. create a new wrapper function with all dependency as parameters            return new Function("React"."render"."require", generateJs(ast));        } catch (ex) {            // in case of exception render the exception message            render(<pre style={{ color: "red"}}>{ex.message}</pre>); }}return {        // returns transpiled code in a wrapper function which can be invoked later        compile(code) {            return getWrapperFunction(code);        },        // compiles and invokes the wrapper function        run(code) {            this.compile(code)(React, render, require);        },        // just compiles and returns the stringified wrapper function        getCompiledCode(code) {            return getWrapperFunction(code).toString();        }    };}Copy the code


When we call the createEditor function, we create an editor instance. This function takes two arguments:

  1. The DOM element that will render the result
  2. A module handler function

The key implementation is getWrappedFunction. Here is a reference to an AST tree generated from the example to help you understand how we detect and modify JSX expressions in the program.



Compare the AST above to understand how isReactNode and findReactNode work. The acorn.parse method is called with an arbitrary string of code, which treats the code as if it were a complete javascript program, so the parsed result contains all statements. We need to find the phrase react. createElement.


Let’s look at the (full) implementation again:

import "./styles.scss"; import React from"react"; import ReactDOM from"react-dom"; import { createEditor } from"./editor"; import debounce from"debounce"; // default code const code = `import x from'x'; // edit this examplefunctionGreet() {  return<span>Hello World! </span>}<Greet />`; class SandBox extends React.Component { state = { code }; editor = null; el = null;componentDidMount() {    this.editor = createEditor(this.el);    this.editor.run(code);  }  onCodeChange = ({ target: { value } }) => {    this.setState({ code: value });    this.run(value);  };  run = debounce(() => {    const { code } = this.state;    this.editor.run(code);  }, 500);  render() {    const { code } = this.state;    return (      <div className="app">        <div className="split-view">          <div className="code-editor">            <textarea value={code} onChange={this.onCodeChange} />          </div>          <div className="preview" ref={el => (this.el = el)} />        </div>      </div>    );  }}const rootElement = document.getElementById("root"); ReactDOM.render(<SandBox />, rootElement);Copy the code

Where can you use it?

This is a really interesting experiment, and I believe this technique (implementation) will be very useful in the following scenarios:

  • Component document
  • Online IDE
  • A simple dynamic JSX rendering

It’s up to you

link

[! [Edit react-live-editor](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-live-editor-x qw3b? fontsize=14)

The last

You may have noticed that I didn’t implement the module handling part. It’s really easy, so I’ll leave it to my readers.

Thanks for reading!