Writing a React SSR app in Deno

craigmorten

Craig Morten

Posted on May 21, 2020

Writing a React SSR app in Deno

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
Enter fullscreen mode Exit fullscreen mode

PowerShell (Windows):

iwr https://deno.land/x/install/install.ps1 -useb | iex
Enter fullscreen mode Exit fullscreen mode

Homebrew (Mac):

brew install deno
Enter fullscreen mode Exit fullscreen mode

Chocolatey (Windows):

choco install deno
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"),
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

Here's what is going on:

  1. First we import our main dependencies of React, ReactDOMServer and the Opine web framework.
  2. 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.
  3. First we use the Deno.bundle() method to create our client-side JavaScript bundle from our application.
  4. 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.
  5. Finally we start the server using the listen() method on port 3000.

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
Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
craigmorten
Craig Morten

Posted on May 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related