One of the most important elements in a developer blog or technical documentation is the code playground. Also known as a “sandbox”, this widget lets you edit a code snippet and instantly see the results.
Here's the playground I use on this blog:
Hello World!
function
App() {
return (
<>
<h1>I'm a playground!</h1>
<p>Edit me, and see the changes below. 😄</p>
</>
);
}
export default App;
I recently rebuilt this blog's playground, using Sandpack, a modern playground framework built by the folks at CodeSandbox. It's phenomenal. I'm super happy with it, and I thought it'd be helpful for me to share what I've learned about this tool.
This is not a sponsored post. These are my honest thoughts on a tool I actually use, on this blog and in my course platform.
A few years ago, Dutch developer and all-around wonderful person Ives Van Hoorne launched CodeSandbox, a browser-based development environment.
The "secret sauce" for CodeSandbox has been the in-browser bundler. It can fetch dependencies from NPM, transpile your JSX, and even supports modern quality-of-life features like hot module reloading. It does this all in-browser, without needing to download hundreds of megabytes of build-system code. It's wild.
Sandpack is the actual bundler used by CodeSandbox. They've open-sourced it. So you're getting the battle-tested code they've been iterating on for years.
There are two relevant repositories:
sandpack-client, a lightweight JS client that sits on top of the bundler
sandpack-react, a fully-featured React-based playground, built on top of sandpack-client.
In this article, we'll be talking mostly about sandpack-react
.
One of the coolest things about Sandpack's React bindings is that they offer several levels of abstraction.
At the highest level, a <Sandpack>
component will create an entire playground for you in one line of code. Here's a quick “hello world”:
This example uses the “React” template, but Sandpack include many other templates, including Vue, Svelte, Angular, and more.
Each template produces a ready-to-go application, complete with multiple (virtual) files. The React template uses create-react-app, with a root /index.js
, an /App.js
, and a /public/index.html
.
To include our own starter code, we can overwrite any of these files with the files
prop:
If we supply multiple files, Sandpack will show a tab bar letting us switch between them:
As you'd expect, we can pick from several pre-defined themes. For example, we can use Sarah Drasner's wonderful “Night Owl” theme:
If you'd like, you can build your own theme with their handy-dandy tool.
The <Sandpack>
component is the fastest way to get started using Sandpack, but it offers the least amount of flexibility.
Instead, you can build your own playground by mixing and matching the provided lower-level components.
For example: the <Sandpack>
component comes with a built-in responsive layout, showing the code side-by-side on large screens, but stacking vertically on smaller screens. What if we wanted them to stack vertically all of the time?
We could do this by using the lower-level components:
This is really cool. Sandpack exposes all of the LEGO™ bricks we need to build the playground of our dreams.
SandpackProvider
is our main component, the one that takes a set of files and bundles them into an application. It then provides the necessary data to all child components via React context.
There are lots of additional LEGO™ bricks for us to mix and match, including things like SandpackFileExplorer
, SandpackConsole
, and SandpackTests
.
For full customization, however, we need to access the underlying state. And that's where their custom hooks come in.
In addition to exposing a bunch of low-level components, Sandpack also comes with several custom hooks.
For example, let's suppose we wanted to know which file was currently selected, in the code editor. We could get that information with the useSandpack
hook:
For a more practical example: let's suppose we don't like the built-in “Refresh” button. Here's how we could build our own:
This is so cool! Not only can we read state, we can trigger updates.
Internally, Sandpack uses a Redux-style action dispatcher. The refresh
function will dispatch the appropriate action for us, to trigger a page refresh.
We can even access the dispatcher directly. This means that, if we're willing to spelunk through their codebase, we could figure out how to change any state / trigger any event. We have an amazing amount of control.
On this blog, I use the lower-level components and the hooks to add things like:
A "format" button, to format the code with Prettier
A "reset" button, to revert the code to its initial state
A console, showing any console messages
My own custom layout and aesthetic
Here's my custom wrapper, with these features:
Hello World!
function
App() {
return (
<>
<h1>I'm a playground!</h1>
<p>Edit me, and see the changes below. 😄</p>
</>
);
}
console.warn("I'm a console warning!");
export default App;
On my course platform, I take it even further, adding things like:
Automatic saving, so that any code changes are persisted to localStorage and restored on subsequent visits
Integrated ESLint warnings
A draggable divider, to resize the code editor.
A "fullscreen" mode, where the editor fills the screen
A "Strict Mode" toggle that flips React-based code to/from Strict Mode.
I've spent a lot of time refining my playground, and so far, I've been able to implement every feature I wanted. The amount of flexibility that Sandpack provides is wild.
By default, Sandpack uses CodeMirror as its code editor.
This is a bit surprising; CodeSandbox uses VSCode as its editor, and so I thought for sure Sandpack would use Monaco, the editor that powers VSCode.
After experimenting with it, however, I've discovered I quite like CodeMirror. It's not as fully-featured as Monaco, but then it doesn't need to be; most web-based playgrounds are meant to demonstrate a concept, not serve as a daily-driver editor.
CodeMirror is also highly extensible. For example, the wonderful new React docs make use of the @codemirror/lint NPM package to show lint warnings in the editor.
![Screenshot of a playground with a lint warning: “React hook useEffect has a missing dependency”](https://www.joshwcomeau.com/_next/image/?url=%2Fimages%2Fnext-level-playground%2Freact-docs-lint-warning.png&w=3840&q=75)
Because Sandpack is so modular, however, you don't need to stick with CodeMirror if you don't like it! In fact, they even have a guide for switching to Monaco!
Something interesting about the way Sandpack is architected: the bundler isn't running locally.
When we render a <SandpackPreview>
component, it produces an iframe. This iframe is hosted by CodeSandbox, and all of the bundling happens on their external domain.
Essentially, when the user makes a change to the code in the editor, the new code is dispatched over to the site in the iframe. That page will re-bundle the code and display the new result.
The good thing about this approach is that it's secure. If the user writes some JS that attempts to read cookies / localStorage, for example, they'll be accessing the stuff on CodeSandbox's domain, not your own.
That said, I was a bit wary of having such a hard dependency on an external service. If CodeSandbox ever goes down, I don't want it to affect my playgrounds!
Fortunately, there is another option: self-hosting.
Essentially, we can build + deploy the bundler code ourselves, to whatever domain we'd like. Then we can specify that domain when rendering a Sandpack instance:
This way, we get the security benefits of an externally-hosted preview, while still maintaining 100% control.
You can learn how to self-host the bundler in their docs.
By default, Sandpacks render with a lil’ button that will open the current code in CodeSandbox:
![Screenshot of the first Sandpack we saw, with an annotation highlighting a button with an “open external” icon](https://www.joshwcomeau.com/_next/image/?url=%2Fimages%2Fnext-level-playground%2Fopen-in-codesandbox.png&w=3840&q=75)
Initially, I didn't think much of this feature, but I've come to realize that it's amazing.
It makes it super easy for folks to share chunks of code. If someone gets stuck on something and need help, they can click this button and send me the URL, instead of having to describe what they did, or copy/paste all of the code (which is often split across multiple files).
This feature works even when self-hosting the bundler.
If you'd rather not have any sort of explicit dependency on CodeSandbox, this button can easily be omitted with a prop:
As I've written about before, this blog is built with MDX.
If you're not familiar with MDX, it's a Markdown-based format that allows me to embed custom React components within the document. It's the best of both worlds: I can author new blog posts easily, without having to wrap each paragraph in a <p>
tag. At the same time, I can include all manner of custom content using bespoke React components.
Integrating Sandpack with MDX was mostly painless. I created my own Sandbox
component that manages the layout and all of my customizations, and forwards along files
and other Sandpack props.
The only real gotcha is that you're not allowed to have blank lines in MDX (at least, not when using next-mdx-remote; I can't speak to other MDX implementations).
For example, this code will throw an exception because of the highlighted blank line:
The MDX parser treats this as two separate chunks of code, and can't figure out what it's supposed to be, and so the whole thing blows up.
To solve this problem, I need to add \n\
to every blank line:
\n
is an explicit newline character, which adds a line break. Unfortunately, there's also an implicit newline, and so with only \n
, we'd wind up with two line breaks. The trailing \
cancels out the implicit newline.
This is obviously not ideal, but I got used to it pretty quickly. It also may not be necessary, depending on which tools you use, but I wanted to share this in case you were running into the same issue.
So, overall, I've found very little to complain about. Using Sandpack has been a really smooth experience, and when I've run into small issues, the team has been very responsive.
That said, there are always tradeoffs, even with the best tools. Let's talk about the drawbacks I've found.
Sandpack comes with lots of templates for common JS frameworks, and it comes with a "Vanilla JS" template that uses Parcel to bundle vanilla JavaScript apps.
What it doesn't have is something like the “static” template on CodeSandbox, which serves plain ol’ HTML, CSS, and JS. No bundler involved.
This is surprisingly problematic, because even the “Vanilla JS” template auto-generates the index.html
file. If I try and add a <link>
or <style>
tag to include some CSS, it'll get stripped away in the compile process.
This means that, as far as I can tell, Sandpack can't be used to show an HTML/CSS snippet. As a result, I haven't been able to adopt it in my CSS course yet.
I know the team has a static template on their roadmap, and so I suspect this drawback will disappear. 😄
So, CodeMirror recently did a big rewrite, going from v5 to v6. Sandpack uses v0.19, which is a beta version for the v6 rewrite.
This can make it confusing when checking the CodeMirror docs, or when googling issues, since there are tons of breaking changes between different versions.
I spoke with Danilo, the lead developer for Sandpack, and he told me that they've been working on updating CodeMirror, but there's no ETA yet because it's a significant challenge.
The good news is that we can always supply a custom editor, thanks to Sandpack's modularity. In theory, we could provide our own editor based on CodeMirror 6. It wouldn't be as tightly-coupled, but it should still work just fine.
Having a live-editable code editor + preview is an essential part of any educational developer resource.
As I wrote about in my blog post “How To Learn Stuff Quickly”, there needs to be an active component for a tutorial/resource to be effective. Playgrounds allow the learner to tinker with ideas, experiment, and commit the lessons to memory.
For the past two years, I've been building a React course, The Joy of React, and Sandpack has helped make sure that active practice is a core part of the course. The course already has over 200 individual Sandpack instances!
You can learn more about the course, and discover the joy of building with React:
I'm really grateful to the team at CodeSandbox for building and maintaining such an amazing free resource. Thanks to Ives and Danilo from CodeSandbox for reviewing this blog post. 💜