loading...
Cover image for Writing a React SSR app in Deno

Writing a React SSR app in Deno

craigmorten profile image Craig Morten Updated on ・6 min read

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

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
├── deps.ts
└── server.tsx

app.tsx will contain our React component code, server.tsx will hold all of our server code and deps.ts will hold all of our dependencies and versions (a bit like a substitute package.json). Be careful to get the correct file extensions!

Importing our dependencies

In the deps.ts file, add the following to re-export all the required dependencies at the versions we need:

export { default as React } from "https://dev.jspm.io/react@16.13.1";
export { default as ReactDOMServer } from "https://dev.jspm.io/react-dom@16.13.1/server";
export { opine } from "https://deno.land/x/opine@0.19.1/mod.ts";

First we import / re-export React and React DOM Server 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 master branch that you can't wait to try, you can just import it!

Here we are importing React and React DOM Server from JSPM, but you could equally use any CDN that provides React as an ES Module. Check out the Deno website for CDN alternatives.

We also import the Opine web framework (a port of ExpressJS for Deno) and some of it's types.

We could choose to not use a deps.ts, and import all of these dependencies directly into our app and server code, but it is useful to have a single place where you can define all of your dependency versions, a bit like a package.json, so when you want to update a dependency to a newer version, you can change it in one place instead of all over your codebase!

Let's now write our first React component in Deno!

Writing the React component

Our app.tsx:

import { React } from "./deps.ts";

declare global {
  namespace JSX {
    interface IntrinsicElements {
      button: any;
      div: any;
      h1: any;
      p: any;
    }
  }
}

const App = () => {
  const [count, setCount] = (React as any).useState(0);

  return (
    <div>
      <h1>Hello DenoLand!</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.

Next we declare some intrinsic elements for typescript to use when compiling our app. This isn't anything new for typescript, and in fact, we don't need to use typescript at all if we didn't want to. Deno supports JavaScript out of the box as well - just change the file extension to .js (but be wary you might need to change to .jsx if you are using JSX components!).

Lastly we create a small React component called App which uses hooks to create a button click counter - simple!

You may notice that I've cast React as any in this example, but you can equally use fully typed React by importing the types from the DefinitelyTyped GitHub repo or by using the Deno Types hint above any import lines for React, for example:

// @deno-types="https://deno.land/x/types/react/v16.13.1/react.d.ts"
import React from "https://dev.jspm.io/react@16.13.1"

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:

import {
  opine,
  React,
  ReactDOMServer,
} from "./deps.ts";

import App from "./app.tsx";

const app = opine();
const browserBundlePath = "/browser.js";

const js =
  `import React from "https://dev.jspm.io/react@16.13.1";\nimport ReactDOM from "https://dev.jspm.io/react-dom@16.13.1";\nconst App = ${App};\nReactDOM.hydrate(React.createElement(App), document.body);`;

const html =
  `<html><head><script type="module" src="${browserBundlePath}"></script><style>* { font-family: Helvetica; }</style></head><body>${
    (ReactDOMServer as any).renderToString(<App />)
  }</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:

  1. First we import our main dependencies or 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. 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 code so we can hydrate the React application on the client.
  4. 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 ./server.tsx

Note the use of the --allow-net flag! 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.

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.

If you're just wanting to be able to quickly run any React application with minimal work, you can also check out deno-react-base-server which exports a CLI and a module that wraps up this server implementation so you can quickly and easily run React applications.

For example, to run the app.tsx in this article using the CLI, you can just run:

deno run --allow-net --allow-read --reload "https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/cli.tsx" --port 3000 --path "./app.tsx"

And it'll automatically run the React applications using SSR.

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 by:

craigmorten profile

Craig Morten

@craigmorten

26 • London • That JS Guy • JavaScript, TypeScript, React, Node, Deno, Kubernetes, Azure • I also tweet stuff

Discussion

markdown guide
 

[notecomm@localhost react]$ deno run --allow-net ./server.tsx

error: TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'.
Type 'URL' is not assignable to type 'string'.
return new URL(url).pathname
~~~
at deno.land/std@0.59.0/path/win32.ts...

TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'.
Type 'URL' is not assignable to type 'string'.
return new URL(url).pathname;
~~~
at deno.land/std@0.59.0/path/posix.ts...

TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'.
Type 'URL' is not assignable to type 'string'.
return new URL(url).pathname
~~~
at deno.land/std@0.58.0/path/win32.ts...

TS2345 [ERROR]: Argument of type 'string | URL' is not assignable to parameter of type 'string'.
Type 'URL' is not assignable to type 'string'.
return new URL(url).pathname;
~~~
at deno.land/std@0.58.0/path/posix.ts...

TS2345 [ERROR]: Argument of type 'ParsedURL' is not assignable to parameter of type 'string'.
const loc = encodeUrl(new URL(originalUrl).toString());
~~~~~~~~~~~
at deno.land/x/opine@0.14.0/src/middl...

Found 5 errors.

github.com/apiel/adka/issues/1

 

on deps.ts change
export { opine } from "deno.land/x/opine@0.19.1/mod.ts"; to

export { opine } from "deno.land/x/opine@0.19.1/mod.ts"; or
export { opine } from "deno.land/x/opine@main/mod.ts";

 

Ah yes! Thanks for pointing this out.

Unfortunately the release of Deno 1.2.0 brought about some breaking changes meaning it no longer works with several std libraries pre 0.61.0

I will try to update this blog post to list the latest versions to use - unfortunately with the pace Deno is moving blog post become outdated very quickly 😂

 

Hi Craig, thanks for the nice tutorial!

Launching the example with a today updated deno installation (deno 1.0.2, v8 8.4.300, typescript 3.9.2) fails with following errors:

deno run --allow-net --allow-read --reload "https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/example/entrypoint.tsx" >> error.txt
Download https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/example/entrypoint.tsx
Compile https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/example/entrypoint.tsx
Download https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx
Compile https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx
error: TS2307 [ERROR]: Cannot find module 'https://dev.jspm.io/react@16.13.1' or its corresponding type declarations.
import React from "https://dev.jspm.io/react@16.13.1";
                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:1:19

TS2307 [ERROR]: Cannot find module 'https://dev.jspm.io/react-dom@16.13.1/server' or its corresponding type declarations.
import ReactDOMServer from "https://dev.jspm.io/react-dom@16.13.1/server";
                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:2:28

TS7006 [ERROR]: Parameter '_req' implicitly has an 'any' type.
  app.use(browserBundlePath, (_req, res, _next) => {
                              ~~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:27:31

TS7006 [ERROR]: Parameter 'res' implicitly has an 'any' type.
  app.use(browserBundlePath, (_req, res, _next) => {
                                    ~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:27:37

TS7006 [ERROR]: Parameter '_next' implicitly has an 'any' type.
  app.use(browserBundlePath, (_req, res, _next) => {
                                         ~~~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:27:42

TS7006 [ERROR]: Parameter '_req' implicitly has an 'any' type.
  app.use("/", (_req, res, _next) => {
                ~~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:31:17

TS7006 [ERROR]: Parameter 'res' implicitly has an 'any' type.
  app.use("/", (_req, res, _next) => {
                      ~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:31:23

TS7006 [ERROR]: Parameter '_next' implicitly has an 'any' type.
  app.use("/", (_req, res, _next) => {
                           ~~~~~
    at https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/mod.tsx:31:28

Found 8 errors.

I'm unfortunatelly not an expert in deno. Do you have already an idea to solve these problems?

 

Really sorry about that! I may have missed something when copying snippets from IDE to blog post! Alternatively it may be the Deno version - I tested this with Deno 1.0.0, and I know the newer versions broke some dependencies with type errors (namely the EVT Deno library had issues with v1.0.1 at one point!) - I’ll take a look when I can next, make fixes, and get back to you as soon as possible :)

In the meantime, you can try and see if it’s a Deno version by downgrading Deno to v1.0.0 - there instructions on how to install specific versions here: github.com/denoland/deno_install#i... (I’ll test myself as well!)

 

I can confirm this code, including:

deno run --allow-net --allow-read --reload "https://raw.githubusercontent.com/asos-craigmorten/deno-react-base-server/master/example/entrypoint.tsx"

Appears to be broken for versions >= 1.0.1, so there must have been an accidental breaking change in Deno?

For now, the code should work fine on v1.0.0 (just tested) and I'll look to get it updated to be compatible with these latest versions of Deno :)

Further to my last comment, the easiest way to change to a specific version of Deno is to use the upgrade command:

deno upgrade --version 1.0.0

Edit:

I have found the relevant issues, and they should hopefully be resolved by the Deno team in the next Deno release. In the meantime I have updated the article to provide guidance on how to set Deno version to 1.0.0, added links to the issues and will look to update the code samples to support v1.0.1 and v1.0.2.

Edit 2:

Code and article updated to support Deno v1.0.2 🦕 🥳. Unfortunately Deno v1.0.1 support won't be possible due to a bug in the Deno core.

Upgrade to Deno v1.0.2 using the upgrade command:

deno upgrade --version 1.0.2

With deno 1.0.2 I do still get Cannot find module 'https://dev.jspm.io/react@16.13.1' or its corresponding type declarations

Oh no! 😢I’ll take another look - the underlying problem (as far as I know) is a bug in Deno which will be released in 1.0.3, check out github.com/denoland/deno/issues/5772

For now I believe v1.0.0 should be fine, so can downgrade in the meantime to get started.

Another open issue is that the unstable Import Map feature of Deno doesn’t play nicely with React atm - see github.com/denoland/deno/issues/5815

Off the top of my head, one thing to try is to use the Deno Types hint:

// @deno-types="https://deno.land/x/types/react/v16.13.1/react.d.ts"
import React from "https://dev.jspm.io/react@16.13.1"

Will test again myself and get back to you!

Ok, I am able to reproduce! Here are the steps that I took:

a. Ran the Deno upgrade command to change to v1.0.2:

deno upgrade --version 1.0.2

b. Checked that I was definitely testing on Deno v1.0.2

deno --version

deno 1.0.2
v8 8.4.300
typescript 3.9.2

c. Cleared my Deno cache in case I had managed to get the module previously and cache it:

deno info

And then deleting everything inside the directories listed for Remote modules cache and TypeScript compiler cache.

d. Created an app.tsx and server.tsx as per this article.

e. Ran the following command:

deno run --allow-net ./server.tsx

And voila, errors:

Compile ~/server.tsx
error: TS2307 [ERROR]: Cannot find module 'https://dev.jspm.io/react@16.13.1' or its corresponding type declarations.
import React from "https://dev.jspm.io/react@16.13.1";
                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    at ~/server.tsx:1:19

TS2307 [ERROR]: Cannot find module 'https://dev.jspm.io/react-dom@16.13.1/server' or its corresponding type declarations.
import ReactDOMServer from "https://dev.jspm.io/react-dom@16.13.1/server";
                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    at ~/server.tsx:2:28

Found 2 errors.

In this instance, adding the @deno-types directive (though a good thing to do to get the React types) does nothing to solve the issue because Deno has the current bug that it's typescript compiler ignores all .tsx and .jsx files (see github.com/denoland/deno/pull/5785) and they are exactly the extensions we've been using!

Unfortunately neither Deno nor Typescript have a particularly great solution to this problem 😞 but there are 2 workarounds that you can use for now until v1.0.3 is released (if can find more then please comment!):

a. Use // @ts-ignore comments above each top-level React import statement. For example

// @ts-ignore
import React from "https://dev.jspm.io/react@16.13.1";
// @ts-ignore
import ReactDOMServer from "https://dev.jspm.io/react-dom@16.13.1/server";

Though I prefer option 2 (which I will use to update this blog post):

b. Create a dep.ts (note the .ts extension will mean that Deno v1.0.2 will compile it despite the bug). We will then import all of our dependencies into this file, and re-export them for use in our app.tsx and server.tsx:

// dep.ts
export { default as React } from "https://dev.jspm.io/react@16.13.1";
export { default as ReactDOMServer } from "https://dev.jspm.io/react-dom@16.13.1/server";
export { opine } from "https://deno.land/x/opine@0.3.0/mod.ts";
export {
  Request,
  Response,
  NextFunction,
} from "https://deno.land/x/opine@0.3.0/src/types.ts";
// app.tsx
import { React } from "./dep.ts";

// Rest of the app code stays the same
// server.tsx
import {
  opine,
  React,
  ReactDOMServer,
  Request,
  Response,
  NextFunction,
} from "./dep.ts";

// Rest of the server code stays the same

You can then run deno run --allow-net --reload ./server.tsx and it should start the application 🥳 🎉

Let me know how you get on!

Edit: Blog post all updated! Thanks so much for providing the feedback - articles become so much more useful to everyone through comments and pointing out bugs! I've certainly learned things 😄

 

Great tutorial, able to get it running within few minutes!

I'm new to JS world, have not used nodejs, and directly learning deno. I wanted to convert this little react js, and in the process learn deno & react :

github.com/anborg/react-pwa

Can someone help me to convert the above node to deno - to make it well organized source/subcomponent staticpage/images etc.

I am looking for an example deno-react project that is structurally similar e.g src/index.js, /public_html/index.html, writing into div "root" instead of rewriting "body", react subcomponents in separate subfolders etc.

 

Hey @anborg ! 👋

Wow, I'd say a PWA React app is a reasonably complex thing! 😲

I have an example of how you can write an MVC like setup in another post including static CSS and EJS templates.

I've not seen any particularly complex examples of React + Deno - if you google there are a few examples / videos that use a mixture of Node and Deno for the desired effect. If you find any awesome examples then please do share!

I would be happy to create a more complex example React application which has some sub-components, uses static CSS etc. and tutorial to go with it - would that be useful?

 

I’ve put together a quick example of using SSR React with suspense, static CSS and sub-components here —> github.com/asos-craigmorten/opine/...

I plan to write up a new post about it over the next couple of days.

@craig Thanks for the SSR. Would be great to have a CSR (client side rendering) React & Deno example, with a structured project so it can be used as a template to write maintainable production quality code.

Hey sure, thanks for the suggestion. CSR would be very similar to the example, but instead of performing a server-side renderToString (here github.com/asos-craigmorten/opine/...) you would just not bother! Instead effectively setting the content of the React root (here github.com/asos-craigmorten/opine/...) as empty.

Finally instead of hydrating the content in your client-side script (here github.com/asos-craigmorten/opine/...), you would just use the reactDOM.render() method to create your app completely on the client. 🎉

For this particular example you could then likely refactor a couple of things such as the isServer prop that is used in this example to manage the fact that React Suspense is yet supported server-side.

 

Great stuff, very promising!

 

Hi Craig, nice tutorial!

I would suggest using denon, like nodemon but for deno, to ease the development process of this kind of apps.

 

Is there any way to import css/less module?

 

Unfortunately CSS Modules etc. aren't there yet with Deno as they generally require a bundler to make them work.

You are able to use static CSS easily, see here for an example of how you can server static CSS.

I've also got a post on how to write a more complex app using static CSS here.

Fingers crossed tooling for things like CSS modules will be developed by the community shortly!