Our Feature Flags course is now live!
Stackpack

Building Preview

Let us build out a preview for our live changes

Setting up Preview component

Create a new component called Preview (file preview.tsx) in the components directory.

src/components/preview.tsx
export default function Preview() {
  return <div className="h-full border bg-red-100">Preview</div>;
}

We can now import the Preview component in the App.tsx file and render it.

src/App.tsx
import { WebContainer } from '@webcontainer/api';
import React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import CodeEditor from './components/code-editor';
import Preview from './components/preview';
import Terminal from './components/terminal';
import { VITE_REACT_TEMPLATE } from './templates/react-vite';
 
export default function App() {
  const [webContainer, setWebContainer] = React.useState<WebContainer | null>(
    null,
  );
 
  React.useEffect(() => {
    const createWebContainer = async () => {
      const webContainerInstance = await WebContainer.boot();
      await webContainerInstance.mount(VITE_REACT_TEMPLATE.files);
      setWebContainer(webContainerInstance);
    };
 
    createWebContainer();
 
    // Ideally, we should clean up the WebContainer instance when the component is unmounted.
    // But there is an issue with the current implementation of WebContainer that prevents it from being torn down.
    // https://github.com/stackblitz/webcontainer-core/issues/1125
    // return () => {
    //   webContainer?.teardown();
    //   setWebContainer(null);
    // };
  }, []);
 
  return (
    <div className="h-dvh p-2">
      <PanelGroup direction="horizontal">
        <Panel>
          <PanelGroup direction="vertical">
            <Panel>
              <CodeEditor />
            </Panel>
            <PanelResizeHandle className="h-2 bg-blue-300" />
            <Panel>
              <Terminal webContainer={webContainer} />
            </Panel>
          </PanelGroup>
        </Panel>
        <PanelResizeHandle className="w-2 bg-blue-300" />
        <Panel>
          <Preview />
        </Panel>
      </PanelGroup>
    </div>
  );
}

Using iframe for Preview

We need a truly dynamic preview component that can render any HTML content. iframe elements help achieve this. Let's update the Preview component to use an iframe.

src/components/preview.tsx
import React from 'react';
 
export default function Preview() {
  const iframeRef = React.useRef<HTMLIFrameElement>(null);
 
  return (
    <iframe
      ref={iframeRef}
      className="h-full w-full border-2"
      src="loading.html"
    />
  );
}

We need to create a new file called loading.html at the root of our project. This file will be used to display a loading message while the preview is being loaded.

loading.html
Use the terminal to run a command!

Wiring up the Preview component with WebContainer API

We need to update the Preview component to listen to the webContainer instance and update the iframe content accordingly.

We get a server-ready event from the webContainer instance when the server is ready to serve the preview content. We can listen to this event and update the iframe content.

src/App.tsx
import { WebContainer } from '@webcontainer/api';
import React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import CodeEditor from './components/code-editor';
import Preview from './components/preview';
import Terminal from './components/terminal';
import { VITE_REACT_TEMPLATE } from './templates/react-vite';
 
export default function App() {
  const [webContainer, setWebContainer] = React.useState<WebContainer | null>(
    null,
  );
 
  React.useEffect(() => {
    const createWebContainer = async () => {
      const webContainerInstance = await WebContainer.boot();
      await webContainerInstance.mount(VITE_REACT_TEMPLATE.files);
      setWebContainer(webContainerInstance);
    };
 
    createWebContainer();
 
    // Ideally, we should clean up the WebContainer instance when the component is unmounted.
    // But there is an issue with the current implementation of WebContainer that prevents it from being torn down.
    // https://github.com/stackblitz/webcontainer-core/issues/1125
    // return () => {
    //   webContainer?.teardown();
    //   setWebContainer(null);
    // };
  }, []);
 
  return (
    <div className="h-dvh p-2">
      <PanelGroup direction="horizontal">
        <Panel>
          <PanelGroup direction="vertical">
            <Panel>
              <CodeEditor />
            </Panel>
            <PanelResizeHandle className="h-2 bg-blue-300" />
            <Panel>
              <Terminal webContainer={webContainer} />
            </Panel>
          </PanelGroup>
        </Panel>
        <PanelResizeHandle className="w-2 bg-blue-300" />
        <Panel>
          <Preview webContainer={webContainer} />
        </Panel>
      </PanelGroup>
    </div>
  );
}
src/components/preview.tsx
import type { WebContainer } from '@webcontainer/api';
import React from 'react';
 
export default function Preview({
  webContainer,
}: {
  webContainer: WebContainer | null;
}) {
  const iframeRef = React.useRef<HTMLIFrameElement>(null);
 
  React.useEffect(() => {
    if (!webContainer || !iframeRef.current) return;
 
    webContainer.on('server-ready', (_, url) => {
      iframeRef.current!.src = url;
    });
  }, [webContainer]);
 
  return (
    <iframe
      ref={iframeRef}
      className="h-full w-full border-2"
      src="loading.html"
    />
  );
}

Now, run the dev server through the terminal we built earlier and see the preview in action.

npm install && npm run dev

Final Output

Updating the Preview on code changes

But, we can see that the preview is not updating when we make changes to the code. We need to update the webContainer to serve the updated content when the code changes.

src/App.tsx
import { WebContainer } from '@webcontainer/api';
import React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import CodeEditor from './components/code-editor';
import Preview from './components/preview';
import Terminal from './components/terminal';
import { VITE_REACT_TEMPLATE } from './templates/react-vite';
 
export default function App() {
  const [webContainer, setWebContainer] = React.useState<WebContainer | null>(
    null,
  );
 
  React.useEffect(() => {
    const createWebContainer = async () => {
      const webContainerInstance = await WebContainer.boot();
      await webContainerInstance.mount(VITE_REACT_TEMPLATE.files);
      setWebContainer(webContainerInstance);
    };
 
    createWebContainer();
 
    // Ideally, we should clean up the WebContainer instance when the component is unmounted.
    // But there is an issue with the current implementation of WebContainer that prevents it from being torn down.
    // https://github.com/stackblitz/webcontainer-core/issues/1125
    // return () => {
    //   webContainer?.teardown();
    //   setWebContainer(null);
    // };
  }, []);
 
  return (
    <div className="h-dvh p-2">
      <PanelGroup direction="horizontal">
        <Panel>
          <PanelGroup direction="vertical">
            <Panel>
              <CodeEditor webContainer={webContainer} />
            </Panel>
            <PanelResizeHandle className="h-2 bg-blue-300" />
            <Panel>
              <Terminal webContainer={webContainer} />
            </Panel>
          </PanelGroup>
        </Panel>
        <PanelResizeHandle className="w-2 bg-blue-300" />
        <Panel>
          <Preview webContainer={webContainer} />
        </Panel>
      </PanelGroup>
    </div>
  );
}
src/components/code-editor.tsx
import { Editor } from '@monaco-editor/react';
import type { WebContainer } from '@webcontainer/api';
import React from 'react';
import { VITE_REACT_TEMPLATE } from '../templates/react-vite';
import { getLanguageFromFileName } from '../utils/get-language-from-file-name';
import FileTabs from './file-tabs';
 
export default function CodeEditor({
  webContainer,
}: {
  webContainer: WebContainer | null;
}) {
  const [activeFile, setActiveFile] = React.useState(
    () => VITE_REACT_TEMPLATE.entry,
  );
 
  const currentFile = VITE_REACT_TEMPLATE.files[activeFile];
  const language = getLanguageFromFileName(activeFile);
 
  const handleCodeChange = async (content: string) => {
    if (!webContainer) return;
 
    await webContainer.fs.writeFile(activeFile, content);
  };
 
  return (
    <div className="h-full">
      <FileTabs
        files={VITE_REACT_TEMPLATE.visibleFiles}
        activeFile={activeFile}
        onFileChange={setActiveFile}
      />
      <Editor
        theme="vs-dark"
        path={activeFile}
        onChange={(value) => handleCodeChange(value || '')}
        defaultValue={currentFile.file.contents as string} // Ideally, worry about the encoding in production, for our example, this is fine.
        defaultLanguage={language}
      />
    </div>
  );
}

By writing to the WebContainer API's file system, we can update the preview content when the code changes. The Monaco editor's onChange event triggers the handleCodeChange function, which writes the updated content to the file system.

Now, you should see the preview updating as you make changes to the code.

At this point, our code should match the code in the branch 6-building-preview.

Last updated on

On this page