Deno + React 18 Alpha with Suspense
In this tutorial, we are going to build Deno + React 18 app demo.
🎓 What can I accomplish after doing this Tutorial?
* You will understand how SSR and Suspense works in React 18
* You will be able setup your own React/Deno App using SSR with Suspense
💡 What should I know?
* Web Technologies: HTML, CSS, JS
* React
* Deno (preferred)
Setup
Or install Deno cli locally, and create folder called deno-react-demo
Getting Started
We are going to build a deno/react environment from scratch so we can understand how the new technologies will work. In production, we would most likely choose a meta framework like NextJS or AlephJS, etc.
Skip ahead by forking this repo - https://github.com/twilson63/deno-react-demo
The first thing we will do is create a folder called scripts
and create a file called dev.sh
in the scripts folder. This file will be our script to run our dev server environment.
scripts/dev.sh
#!/usr/bin/env bash
deno run --unstable -A --import-map=import_map.json --no-check server.js
Run a command to turn our script into an executable script
chmod +x ./scripts/dev.sh
Lets create some files:
touch import_map.json
touch server.js render.jsx bundle.js
mkdir components
touch components/client.jsx
touch components/App.jsx
touch components/data.jsx
mkdir public
touch public/style.css
touch README.md
import_map.json is a json file that contains key/value associations to module urls, so that we can reference these external dependencies as human readable strings.
import-map.json
{
"imports": {
"react": "https://esm.sh/react@18.0.0-alpha-7ec4c5597?dev",
"react-dom": "https://esm.sh/react-dom@18.0.0-alpha-7ec4c5597?dev",
"react-dom/server": "https://esm.sh/react-dom@18.0.0-alpha-7ec4c5597/server?dev"
}
}
components/client.jsx
import React from 'react'
import { hydrateRoot } from 'react-dom'
import { App } from './App.jsx'
hydrateRoot(document, <App assets={window.assetManifest} />)
components/App.jsx
import React from 'react'
export function App() {
return (
<h1>Hello World</h1>
)
}
server.js
import { opine, serveStatic } from 'https://deno.land/x/opine@1.5.3/mod.ts'
import render from './render.jsx'
import bundle from './bundle.js'
// bundle client
const clientJS = await bundle('./components/client.jsx')
const app = opine()
app.use(serveStatic('public'))
app.get('/scripts/client.js', (req, res) => {
const js = clientJS.files['deno:///bundle.js']
res.type('application/javascript').send(js)
})
app.get('/', (req, res) => {
render(req.url, res)
})
app.listen({port: 3000})
render.jsx
import React from 'react'
import server from 'react-dom/server'
import { App } from './components/App.jsx'
import { DataProvider } from './components/data.jsx'
let assets = {
'client.js': '/scripts/client.js'
}
export default function render(url, res) {
const data = createServerData()
const html = server.renderToString(
<DataProvider data={data}>
<App assets={assets} />
</DataProvider>
)
res.send(html)
}
function createServerData() {
let done = false;
let promise = null;
return {
read() {
if (done) {
return;
}
if (promise) {
throw promise;
}
promise = new Promise(resolve => {
setTimeout(() => {
done = true;
promise = null;
resolve();
}, 2000);
});
throw promise;
},
};
}
bundle.js
export default function (bundle) {
return Deno.emit(
bundle,
{
bundle: "module",
compilerOptions: {
lib: ["dom", "dom.iterable", "esnext"],
},
importMap: {
"imports": {
"react": "https://esm.sh/react@alpha?dev",
"react-dom": "https://esm.sh/react-dom@alpha?dev",
"react-dom/server": "https://esm.sh/react-dom@alpha/server?dev"
}
},
importMapPath: 'file:///import-map.json'
}
)
}
React 18 Suspense
Introduction
React 18 alpha was released and announced in June 2021, the purpose of this major release is to rollout Suspense, a feature that has been in the works for several years and has had several iterations on its approach. React 18's goal is to formalize a final stable suspense feature.
In React 18, the core team has refactored SSR (Server-side rendering) to improve the Suspense implementation. Now, with the new SSR foundations, you can dynamically hydrate different components of your application, while in the process of streaming your html, from your server.
For more details about React 18 suspense, this is a great post to read in the React 18 discussion channel.
Example
With Suspense we can let react render a fallback component while the data bound component is loading data, then when the data bound component is ready, react will swap the fallback out with the data component.
function Content() {
return (
<>
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
</>
)
}
Demo
Deno Bundle
Introduction
Deno has a built in bundler that will take a bunch of javascript files and bundle them into one file. We are using the Deno bundler to bundle our client-side javascript and serve it via the server. We can also do code-splitting and use Deno bundler to compile our split code as well.
Example
bundle.js
export default function (bundle) {
return Deno.emit(
bundle,
{
bundle: "module",
compilerOptions: {
lib: ["dom", "dom.iterable", "esnext"],
},
importMap: {
"imports": {
"react": "https://esm.sh/react@alpha?dev",
"react-dom": "https://esm.sh/react-dom@alpha?dev",
"react-dom/server": "https://esm.sh/react-dom@alpha/server?dev"
}
},
importMapPath: 'file:///import-map.json'
}
)
}
server.js
import bundle from './bundle.js'
// bundle client
const clientJS = await bundle('./components/client.jsx')
...
app.get('/scripts/client.js', (req, res) => {
const js = clientJS.files['deno:///bundle.js']
res.type('application/javascript').send(js)
})
SSR
To take full advantage of Suspense, you want streaming html
Create an HTML
Component
components/Html.jsx
import React from "react";
export default function Html({ assets, children, title }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
</head>
<body>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>
{children}
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,
}}
/>
<script async src={assets['client.js']} />
</body>
</html>
);
}
Now lets use the Html component to wrap our content in our App Component
components/App.jsx
import React from "react";
import Html from './Html.jsx'
export function App({ assets }) {
return (
<Html assets={assets} title="React 18">
<Content />
</Html>
)
}
function Content() {
return (
<>
<h1>Hello World</h1>
</>
)
}
Lets add some Lazy Component and Suspense to our App.jsx
...
const { Suspense, lazy } from React
const LazyComponent = lazy(() => import('./LazyComponent.jsx'))
...
function Content() {
return (
<>
<Suspense fallback={<div>loading...</div>}>
<LazyComponent />
</Suspense>
<h1>Hello World</h1>
</>
)
}
Create a LazyComponent.jsx
import React from 'react'
export default function () {
return (
<img src="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQjOtzqqREcLSOZ4Aaef5hNzwRIRcjIiYqkaA&usqp=CAU" />
)
}
We need to bundle that component separate in our server
const lazyComponent = bundle('./components/LazyComponent.jsx')
...
app.get("/scripts/LazyComponent.jsx", (req, res) => {
const js = lazyComponent.files['deno:///bundle.js'];
res.type('application/javascript').send(js)
})
Streaming HTML
const { startWriting, abort } = server.pipeToNodeWritable(
<DataProvider data={data}>
<App assets={assets} />
</DataProvider>,
res,
{
onReadyToStream() {
res.statusCode = 200
res.type('text/html')
res.write('<!doctype html>')
startWriting()
},
onError(x) {
didError = true,
console.error(x)
}
}
)
Unfortunately, I was unable to get the streaming HTML to work in Deno, if you want to see a demo of the streaming html check out this sandbox from the ReactJS Team:
Summary
This short workshop walks through a manual setup of React with SSR on Deno, it is not meant to be production ready, but a demonstration, that shows the moving parts of a React 18 application. Hopefully, it was informative and useful.
Resources
- Gitpod - https://gitpod.io - run dev env in the cloud
- Deno - https://deno.com
- ReactJS - https://reactjs.com
- Opine - https://deno.land/x/opine@1.5.3