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.
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.
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.
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.
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.
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>
);
}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
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.
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>
);
}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