Writing a React SSR app in Deno
Craig Morten
Posted on May 21, 2020
As featured in Open JS World 2020 by Ryan Dahl.
Deno v1 has shipped and is causing a real buzz in the JavaScript community.
For those that haven't come across it yet, Deno is a new runtime for JavaScript and TypeScript outside of the web browser. It based on the V8 JavaScript Engine, written in Rust and was created by Ryan Dahl, the original founder of Node.js.
If you want to find out more about Deno and it's mission, check out the Deno 1.0 launch blogpost written by the creators.
Background over, let's begin with writing our React SSR application in Deno!
Installation
Deno can be installed using all the main package installers as well using the official installer scripts. Here are some of the main ways to install:
Shell (Mac, Linux):
curl -fsSL https://deno.land/x/install/install.sh | sh
PowerShell (Windows):
iwr https://deno.land/x/install/install.ps1 -useb | iex
Homebrew (Mac):
brew install deno
Chocolatey (Windows):
choco install deno
Head over to the Deno installation page for other installation methods and further details!
Getting started
Please note that Deno is rapidly developing, which can quickly make articles like this one become outdated. Always check to see if there are newer versions of the modules used to ensure the code works for the latest version of Deno.
Having installed Deno you can now run make use of the deno
command! Use deno help
to explore the commands on offer. We'll be using this command to run our React SSR app later on.
But first let's create a project!
In a new project directory let's create three files:
.
├── app.tsx
├── client.tsx
└── server.tsx
app.tsx
will contain our React component code, server.tsx
will hold all of our server code and client.tsx
will act as our entrypoint to the client-side bundle. Be careful to get the correct file extensions!
Writing our client-side bundle
In the client.tsx
file, add the following code to set up our client-side entrypoint:
import React from "https://dev.jspm.io/react@16.13.1";
import ReactDOM from "https://dev.jspm.io/react-dom@16.13.1";
import App from "./app.tsx";
(ReactDOM as any).hydrate(
<App />,
//@ts-ignore
document.getElementById("root"),
);
First we import React and React DOM like we're used to in any React app, but instead of importing from "react"
, we're importing it from a url...!?
That's right, in Deno you can import modules from any URL and relative or absolute file path that exports a module. This means you can easily pull in any code from the web, e.g. gists, GitHub code and are no longer tied to versions that have been released - if there's something on a main
branch that you can't wait to try, you can just import it!
Here we are importing React and React DOM from JSPM, but you could equally use any CDN that provides React as an ES Module. Check out the Deno website for CDN alternatives.
Following our imports of React libraries we import our App component (yet to be written!) and finally set up the code to render our application, using the React DOM hydrate method.
Now let's write our first React component in Deno!
Writing the React component
Our app.tsx
:
// @deno-types="https://raw.githubusercontent.com/Soremwar/deno_types/4a50660/react/v16.13.1/react.d.ts"
import React from "https://dev.jspm.io/react@16.13.1";
const App = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<h1>Hello Deno Land!</h1>
<button onClick={() => setCount(count + 1)}>Click the 🦕</button>
<p>You clicked the 🦕 {count} times</p>
</div>
);
};
export default App;
There's a lot going on here, so let's break it down -
First we import React
like we're used to in any React app, but notice we also are using a @deno-types
hint comment. This allows us to inform deno of where to find the TypeScript types for the imported module - neat huh?
You can choose to omit this type hint, but TypeScript will require you to provide the types yourself (imported or custom written). Alternatively, you have avoid using TypeScript altogether simply by changing the file extension to .jsx
. Deno supports both TypeScript and JavaScript out-of-the-box!
Lastly we create a small React component called App
which uses hooks to create a button click counter - simple! Overall, there isn't much difference to writing a React component in NodeJS.
Writing the server
For the server we will be using the Deno web framework Opine, which is a port of the ExpressJS web framework that is commonly used in NodeJS.
Here's the code we'll be using for server.tsx
:
// @deno-types="https://raw.githubusercontent.com/Soremwar/deno_types/4a50660/react/v16.13.1/react.d.ts"
import React from "https://dev.jspm.io/react@16.13.1";
import ReactDOMServer from "https://dev.jspm.io/react-dom@16.13.1/server";
import { opine } from "https://deno.land/x/opine@0.25.0/mod.ts";
import App from "./app.tsx";
/**
* Create our client bundle - you could split this out into
* a preprocessing step.
*/
const [diagnostics, js] = await Deno.bundle(
"./client.tsx",
undefined,
{ lib: ["dom", "dom.iterable", "esnext"] },
);
if (diagnostics) {
console.log(diagnostics);
}
/**
* Create our Opine server.
*/
const app = opine();
const browserBundlePath = "/browser.js";
const html =
`<html><head><script type="module" src="${browserBundlePath}"></script><style>* { font-family: Helvetica; }</style></head><body><div id="root">${
(ReactDOMServer as any).renderToString(<App />)
}</div></body></html>`;
app.use(browserBundlePath, (req, res, next) => {
res.type("application/javascript").send(js);
});
app.use("/", (req, res, next) => {
res.type("text/html").send(html);
});
app.listen({ port: 3000 });
console.log("React SSR App listening on port 3000");
Here's what is going on:
- First we import our main dependencies of
React
,ReactDOMServer
and the Opine web framework. - We then import the React app we just created, being careful to include the
.tsx
extension - file extensions are required by Deno unlike in NodeJS. - First we use the
Deno.bundle()
method to create our client-side JavaScript bundle from our application. - Next we create an Opine app, much like you would do with ExpressJs, and define some routes: one to serve a simple HTML page containing our rendered app, and another
/browser.js
route to server our app's client-side bundle so we can hydrate the React application on the client. - Finally we start the server using the
listen()
method on port3000
.
And that's it! We're now ready to run our React application 🎉.
Running our React SSR application
We can now run our React SSR application using the following deno
command:
deno run --allow-net --allow-read --unstable ./server.tsx
Note the use of the various flags! A major difference between Deno and NodeJS is that Deno was build with security in mind. Any action that needs to access the web, read or write to files, or even consume environment variables needs to have the permission granted before Deno will allow it.
To find out more, check out the Deno permissions section of the Deno Manual.
For our example application, we require --allow-net
so that our server is allowed to access the network, --allow-read
is required by the Opine framework (so it can read templates if make use of it's "views" feature), and we also need the --unstable
flag to make use of the Deno.bundle()
API, which is still in preview.
Head over to http://localhost:3000/ and voila! You should now see your React SSR application running in your browser. 😄
Next steps
This is just a basic server and app setup, but by now you should hopefully see that there isn't too much to do to convert your existing applications over to Deno.
That's all gang! Would love to hear your thoughts and how you're getting on with Deno - drop your comments below!
Update 29-06-2020: Deno is progressing quickly and all the aforementioned bugs with JSX are resolved, so I have removed references to them in this article!
Update 20-07-2020: If you followed this tutorial prior to the release of Deno 1.2.0 you will find that after upgrading there are several url related errors. Deno 1.2.0 brought about a breaking change for the std library so any module using std version before 0.61.0 may well error! Generally try seeing if modules you are using can be upgraded to a later version, and if that doesn’t fix it then try opening an issue on the repo you are having issues with!
Posted on May 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.