Building a Web Sandbox: Part 1

Published See discussion on Bluesky

Both at work and in my free time I've been working on an online browser-based live sandbox for interpreting JavaScript code and rendering output using React. Very much like tools such as Codesandbox, CodePen, or the variety of other online REPLs.

Since I've been exploring this concept for a bit of time I figured I would try to write about some of my learnings and tips and tricks for others that might be interested.

To start off, I figured I'd share the high level concepts for building a sandbox for the browser. At a super high level, you'll need:

  • An editor component
  • A preview component

A very basic implementation of this, expressed as a React component, might look something like this:

1export default function Sandbox() {
2 let [code, setCode] = useState(`
3render(
4 createElement('h1', {}, 'Hello World!'),
5 rootElement
6)`);
7
8 let [rootElement, setRootElement] = useState();
9
10 useEffect(() => {
11 if (!rootElement) {
12 return;
13 }
14
15 let func = new Function(
16 'React',
17 'createElement',
18 'render',
19 'rootElement',
20 code,
21 );
22
23 func(React, React.createElement, ReactDOM.render, rootElement);
24 }, [code, rootElement]);
25
26 return (
27 <>
28 <label>
29 Code:
30 <textarea
31 value={code}
32 onChange={(e) => {
33 setCode(e.target.value);
34 }}
35 />
36 </label>
37 <div ref={setRootElement} />
38 </>
39 );
40}
1export default function Sandbox() {
2 let [code, setCode] = useState(`
3render(
4 createElement('h1', {}, 'Hello World!'),
5 rootElement
6)`);
7
8 let [rootElement, setRootElement] = useState();
9
10 useEffect(() => {
11 if (!rootElement) {
12 return;
13 }
14
15 let func = new Function(
16 'React',
17 'createElement',
18 'render',
19 'rootElement',
20 code,
21 );
22
23 func(React, React.createElement, ReactDOM.render, rootElement);
24 }, [code, rootElement]);
25
26 return (
27 <>
28 <label>
29 Code:
30 <textarea
31 value={code}
32 onChange={(e) => {
33 setCode(e.target.value);
34 }}
35 />
36 </label>
37 <div ref={setRootElement} />
38 </>
39 );
40}

So let's break down the above code snippet a bit to better understand what is happening.

Our first concept from above was a code editor, in our above snippet the following code is playing that role of the code editor (managing the state and rendering the text editor using a textarea):

1export default function Sandbox() {
2 let [code, setCode] = useState(`
3render(
4 createElement('h1', {}, 'Hello World!'),
5 rootElement
6)`);
7
8 let [rootElement, setRootElement] = useState();
9
10 useEffect(() => {
11 if (!rootElement) {
12 return;
13 }
14
15 let func = new Function(
16 'React',
17 'createElement',
18 'render',
19 'rootElement',
20 code,
21 );
22
23 func(React, React.createElement, ReactDOM.render, rootElement);
24 }, [code, rootElement]);
25
26 return (
27 <>
28 <label>
29 Code:
30 <textarea
31 value={code}
32 onChange={(e) => {
33 setCode(e.target.value);
34 }}
35 />
36 </label>
37 <div ref={setRootElement} />
38 </>
39 );
40}
1export default function Sandbox() {
2 let [code, setCode] = useState(`
3render(
4 createElement('h1', {}, 'Hello World!'),
5 rootElement
6)`);
7
8 let [rootElement, setRootElement] = useState();
9
10 useEffect(() => {
11 if (!rootElement) {
12 return;
13 }
14
15 let func = new Function(
16 'React',
17 'createElement',
18 'render',
19 'rootElement',
20 code,
21 );
22
23 func(React, React.createElement, ReactDOM.render, rootElement);
24 }, [code, rootElement]);
25
26 return (
27 <>
28 <label>
29 Code:
30 <textarea
31 value={code}
32 onChange={(e) => {
33 setCode(e.target.value);
34 }}
35 />
36 </label>
37 <div ref={setRootElement} />
38 </>
39 );
40}

The second concept noted from above is the preview component of the system, in this example the preview component is highlighted below (including the system that transforms the code that the user authors, and evaluating that code):

1export default function Sandbox() {
2 let [code, setCode] = useState(`
3render(
4 createElement('h1', {}, 'Hello World!'),
5 rootElement
6)`);
7
8 let [rootElement, setRootElement] = useState();
9
10 useEffect(() => {
11 if (!rootElement) {
12 return;
13 }
14
15 let func = new Function(
16 'React',
17 'createElement',
18 'render',
19 'rootElement',
20 code,
21 );
22
23 func(React, React.createElement, ReactDOM.render, rootElement);
24 }, [code, rootElement]);
25
26 return (
27 <>
28 <label>
29 Code:
30 <textarea
31 value={code}
32 onChange={(e) => {
33 setCode(e.target.value);
34 }}
35 />
36 </label>
37 <div ref={setRootElement} />
38 </>
39 );
40}
1export default function Sandbox() {
2 let [code, setCode] = useState(`
3render(
4 createElement('h1', {}, 'Hello World!'),
5 rootElement
6)`);
7
8 let [rootElement, setRootElement] = useState();
9
10 useEffect(() => {
11 if (!rootElement) {
12 return;
13 }
14
15 let func = new Function(
16 'React',
17 'createElement',
18 'render',
19 'rootElement',
20 code,
21 );
22
23 func(React, React.createElement, ReactDOM.render, rootElement);
24 }, [code, rootElement]);
25
26 return (
27 <>
28 <label>
29 Code:
30 <textarea
31 value={code}
32 onChange={(e) => {
33 setCode(e.target.value);
34 }}
35 />
36 </label>
37 <div ref={setRootElement} />
38 </>
39 );
40}

Again, this snippet is a pretty basic example of a sandbox REPL in the browser, however there are several opportunities for enhancement here:

  • We render the preview in-line with the rest of the page, however if we wanted to truly sandbox no pun intended the preview, we should move it over into an iframe
  • We assume that the browser can understand the code that the user writes, but what about experimental syntax options or extensions like JSX?
  • We hardcode the scope that the code inside the editor can access via our new Function call, but ideally we'd like to support the user to specify what code they would like to load in the preview
  • A textarea isn't exactly the best code editing experience for developers, you miss out on convinience features that any developers are use to with their current code editors

In future versions of this blog post I'll dive into these points and more to see how we can enhance this development experience! If you have comments or questions, feel free to tweet at me or emailme