Strongly-Typed, Server-Rendered Views with React and TypeScript
After completing this tutorial, you’ll be able to use React to render templates server side as part of a Node.js application. This will be set up in a way for the TypeScript compiler to catch any issue with rendering the view, including incorrect view model data and dealing with renamed files.
I used Visual Studio Code for the tutorial, since it’s a great cross platform editor/IDE, but you can use any tools you like. Other popular options include WebStorm.
The Problem
Those new to TypeScript should check out its main site. It’s a compile time tool that “transpiles” down to whichever level of JavaScript you need, be it Node.js or code capable of running in older web browsers. Their quickstart shows how to use it in many popular frameworks.
TypeScript catches lots of errors at compile time, but here’s where it fails us when it comes to traditional templating engines. To be framework agnostic, the following is pseudocode. It demonstrates the usual way to render a view. A function representing the controller action passes a string with a template’s name and some data to a function tasked with rendering the view:
function myControllerAction(response: FrameworkHttpResponse) {
response.render('home_page', { msg: 'Hello, World!' });
}
The compiler’s errors and the IDE’s code completion can warn us if we misspell the response
parameter or the render
method, or if we don’t use the proper syntax when creating the object literal with the “Hello, World!” message.
However, the first argument to the render function is “stringly-typed”. The compiler knows nothing about the string. Without custom extensions for the IDE meant to understand the templating engine, there’s no way for the IDE to warn us if renaming the view file breaks this function call.
There’s also no way for the IDE to understand whether or not that data makes sense for that view. You’ve got strong typing up until the data gets sent to the view to be rendered.
This made sense in the pre-TypeScript world. JavaScript has no compiler. There was no urgency to deal with this string typing issue because the entire code base would be weakly typed anyways. In the post-TypeScript world, we need a better way to render views.
Enter JSX
This is the secret sauce here. We’ll be using JSX to create our views, and we’ll set up the TypeScript compiler to check it for us. JSX is a programming language that looks like XML and transpiles to JavaScript expressions. In English? This:
<div>
<p>Hello, World!</p>
</div>
transpiles to:
React.createElement("div", null,
React.createElement("p", null, "Hello, World!"));
React users, most of whom use it to create client side SPAs, can use this syntax to use a code-driven approach as they create templates for their views. They can even use JavaScript functions like filter
and map
to enumerate over arrays of data, converting them to child HTML elements inline in the JSX:
<div>
<ul>
{[1, 2, 3].map(n => (<li>{n}</li>))}
</ul>
</div>
transpiles to JavaScript capable of rendering:
<div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
If you’re already familiar with React and JSX, then I’m preaching to the choir and need say no more. If you’re new to this, I recommend checking out Facebook’s official React documentation which explains JSX in more detail.
Integrating React with Node.js and Adding TypeScript
Now that we know what we’ll be working with, it’s time to put this together. We’ll start with a new app. Create a new app with the npm init
command, and create a src
directory for our source code. After that, your directory structure will look like this:
src/
package.json
To upgrade this JavaScript app to a TypeScript app, we must add TypeScript as a dev dependency, with the npm install -D typescript
command. Because TypeScript can be thought of as a tool specific to building this project, whose version changes often and should be checked in, it is best to use it as a dev dependency, not globally installed on the system.
A tsconfig.json
file describes how the TypeScript compiler will interpret your code as it builds it. Rather than create this from scratch, an effective way to bootstrap your new TypeScript Node app is to use the tsconfig.json file from Microsoft’s TypeScript-Node-Starter repo.
After adding this to your app, your directory structure will look like this:
node_modules/
src/
package.json
tsconfig.json
Because the point of this tutorial isn’t to focus on controller or model design, we won’t worry about databases or separating out our source code into many directories. All we need to render HTML to the browser is a simple HTTP server. Using Express will be less verbose than using raw Node.js, so we’ll add it with the npm install express
command.
To be able to use the Express TypeScript type definitions in our IDE, we can add them with the npm i -D @types/express` command. Again, since they’re only useful in the context of building our app (not running it), we should install them as dev dependencies.
Create a file in the src
directory called index.ts
for the entry point of our Node.js app. We can use the following code for the file, which will render some HTML to the browser when visitors send a request to http://localhost:3000
:
// src/index.ts
import * as express from 'express';
const app = express();
app.get('/', function (req, res) {
const resData = `<!doctype html>
<html>
<head>
<title>App</title>
</head>
<body>
<p>Hello, World!</p>
</body>
</html>
`;
res.send(resData);
});
const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);
At this point, your directory structure will look like this:
node_modules/...
src/
index.ts
package.json
tsconfig.json
To test building this app, run the TypeScript compiler with the command ./node_modules/typescript/bin/tsc
from the project directory, or use the tool in your IDE appropriate to compile the TypeScript.
If you’re using Visual Studio Code, you can do this with the “build” task which will be automatically available under Tasks > Run Build Task...
.
If you’re using WebStorm, a good option is to use the “Compile TypeScript” pre-launch task. However, WebStorm users must remove the "esModuleInterop": true
property from the tsconfig.json
file.
After the compile step completes, you can run the app with the command node dist/index.js
(or use an appropriate tool in your IDE to launch the app).
Creating a React Component for the Home Page View
Now let’s create a React component to be a view for our page. We’ll need some more NPM modules, and their type definition files. Use the commands npm install react react-dom
and npm install -D @types/react @types/react-dom
.
The react
package contains code for the main React constructs like components (which we’ll use to build our views). The react-dom
package contains code to assist with either linking to the DOM (which client side apps do, and we won’t) or rendering the components into static HTML that the server can send in the response (which we will do).
Create a directory called views for our views, and a file called Home.tsx in the directory for our first component, which will be for our Home page view:
// src/views/Home.tsx
import * as React from 'react';
export class Home extends React.Component {
render() {
return <div>
<p>Hello, World!</p>
</div>;
}
}
At this point, your directory structure will look like this:
node_modules/...
src/
views/
Home.tsx
index.ts
package.json
tsconfig.json
Adding JSX Support to TypeScript Compiler
At this point, you will notice an issue when trying to compile the app (with the tsc
command) or from IDE warnings:
[ts] Cannot use JSX unless the '--jsx' flag is provided
The TypeScript compiler is unable to handle the JSX syntax without the --jsx
flag added. We can fix this by adding it to the tsconfig.json
file. After adding the flag, it will look like this:
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"*": [
"node_modules/*",
"src/types/*"
]
},
"jsx": "react"
},
"include": [
"src/**/*"
]
}
Rendering the View
To render the view, we can rename index.ts
to index.tsx
and import the component from Home.tsx
into it. Why the rename? We need this source code file to understand JSX syntax now, so it must have the .tsx
file extension. We can then use the renderToStaticMarkup
function from the react-dom
package to render the component as a string:
// src/index.tsx
import * as express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { Home } from './views/Home';
const app = express();
app.get('/', function (req, res) {
const resData = `<!doctype html>
<html>
<head>
<title>App</title>
</head>
<body>
${ReactDOMServer.renderToStaticMarkup(<Home />)}
</body>
</html>
`;
res.send(resData);
});
const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);
In this example, our Home
component represents the markup inside the body
element of our page, so we insert the rendered string into the body
element in the string template.
If we compile and run this (with the command node dist/index.js
), we will see the “Hello, World!” message rendered.
DTOs
Now we can begin to explore the power this work flow gives us as we build our app or when we’re maintaining it and our domain model changes. We’ll use a concept called DTOs (“data transfer objects”), also known as “view models” or “resource models”. It’s a useful design pattern. Rather than work directly with database access logic to build a view, you delegate to a middle layer, often called a “service”, tasked with getting the data you need for that specific operation. In a web app, these operations are HTTP request & response cycles.
For example, let’s consider GitHub. The operation might be loading the data needed to display the nav bar at the top of every page. This is isn’t going to include everything about your profile (no description, no links to your social media accounts) but it will probably include some data about the repositories you interact with. It will be able to display a different looking “notification” icon if you’ve got notifications to look at, perhaps a blue dot:
“You’ve got mail!” — GitHub, probably
In this example, the DTO doesn’t line up with just one table of data in an SQL database. This is normal. In the real world, you may have to get data from more than one table in an SQL database, or even fetch data from multiple databases, caches, remote web services, or a combination of sources like these. That’s why DTOs are so important. You can maintain a good understanding of the parts of your app by describing the data each operation needs.
With a strongly typed programming language, like TypeScript, this data can be checked by the compiler as it flows through your app into your views. And that’s exactly what we’re setting up here. The TypeScript compiler will refuse to compile the app if a front end developer adds a member to the DTO class - it would be a missing member in the controller or service layer. In the opposite situation, with a back end developer adding a member to the DTO class, the compiler won’t complain about the member being unused in the view, but at least now the front end developers will have code completion to help them use it.
In TypeScript, to create DTOs, we can use a class with public members instead of an interface for them since this allows them to have methods used for validation, and validation is important in the context of an HTTP request/response cycle.
Create a directory called dtos
in the src
directory of your app. Inside this directory, create the file HomeDto.tsx
, and inside, we’ll create a class called HomeDto
for the DTO. After creating this file, your directory structure will look like this:
node_modules/...
src/
dtos/
HomeDto.tsx
views/
Home.tsx
index.ts
package.json
tsconfig.json
We can use the following code for our HomeDto
class:
// src/dtos/HomeDto.tsx
import * as React from 'react';
export class HomeDto {
backgroundColor: string;
currentDate: Date;
}
Readers with previous ES6/TypeScript knowledge will note that we’re not using default exports, which is a bit less verbose on the import side and pretty common. We do this to allow our import statements to be checked by the TypeScript compiler. Later in the tutorial, we’ll see examples of where changes in the code base (like renaming a class but not renaming the import in this case) would be caught by the compiler as an error.
Since the goal of this tutorial is only to demonstrate using JSX to render views, we’ll skip databases, controllers, etc, and later in the tutorial we’ll simply pass example instances of this class to the views to render them.
Using DTOs as React Props
React uses the “props” (meaning “properties”) concept to pass data into components. This is similar to passing an argument into a function call, so that the data is available in the body of the function as a parameter. These properties can be hard-coded values or dynamic, based on runtime data. The values of the props can be any valid JavaScript value. You can read more about this in React’s official documentation. This ends up looking like this in JSX:
<MyComponent myFirstProp={'myFirstPropValue'} mySecondProp={2} />
With TypeScript available to us, we can make these props stongly typed, so that not making our component declaration match the props required would trigger a compiler error. We do this by making an interface to represent the data passed in as props and including the interface in the type that the component extends.
Due to TypeScript’s type system, we can also use a class for the props generic type. This enables us to use our DTO classes for the component’s props. After adding an import for HomeDto
and adding it as a type parameter, our Home
class (in src/views/Home.tsx
) will look like this:
// src/views/Home.tsx
import * as React from 'react';
import { HomeDto } from '../dtos/IndexDto';
export class Home extends React.Component<HomeDto> {
render() {
return <div>
<p>Hello, World!</p>
</div>;
}
}
Once we do this, we will have access to a member called props
inside the render
function, which will contain the members of the HomeDto
class. This is where we can get code completion help as we add the background color and current date data from our example:
When we’re finished adding to our render
method, it will look like this:
render() {
return <div style={{ backgroundColor: this.props.backgroundColor }}>
<p>Hello, World! The date is {this.props.currentDate.toLocaleDateString()}</p>
</div>;
}
Note that there are a few quirks with JSX compared to normal HTML. The styles can be inlined, but the style
attribute’s value must be an object. The names of the CSS properties are camelcased and used as properties of this object. Since we use braces to describe JavaScript expressions within JSX, we see this nested {{}}
brace syntax when using an object as a JSX attribute value.
To provide values for the props, our app’s index.tsx
file will be modified to the following:
// src/index.tsx
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { Home } from './views/Home';
const app = express();
app.get('/', function (req, res) {
const resData = `<!doctype html>
<html>
<head>
<title>App</title>
</head>
<body>
${ReactDOMServer.renderToStaticMarkup(<Home backgroundColor={'red'} currentDate={new Date()} />)}
</body>
</html>
`;
res.send(resData);
});
const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);
Shared Layouts via Nested React Components
Traditional view templating engines use the concept of “partial” or “shared” layouts to allow you to have a common shell for your views, with the contents of that shell unique for each particular view. In React, we accomplish this by creating a component for our shell and rendering the inner component relevant to the view inside it. We can also create components to use on many views (such as nav
elements) and render them as siblings to the view specific components where appropriate. The options for composability are very flexible.
Here’s an example that might work well for most apps. Create two additional views, Shell.tsx
and Nav.tsx
. Your directory structure will then look like this:
node_modules/...
src/
dtos/
HomeDto.tsx
views/
Home.tsx
Nav.tsx
Shell.tsx
index.ts
package.json
tsconfig.json
Use the following code for the Shell
component:
// src/views/Shell.tsx
import * as React from 'react';
// A component used to wrap our view components so that they form
// complete HTML pages.
export class Shell extends React.Component {
render() {
return <html>
<head>
<title>App</title>
</head>
<body>
{this.props.children}
</body>
</html>;
}
}
Note that there is no need to create a DTO class for the Shell
component, and therefore no need to import it into Shell.tsx
or use it as a type parameter as we extend React.Component
.
Also note how we use this.props.children
in the body
element. Every React component can have no children, one child, or many children. This optional property of the React.Component
class is available to the render
method. It will evaluate to any components that are nested as its children, which we’ll see in action shortly when we modify our Index view (using the Shell
component as its outer component).
Use the following code for the Nav
component:
// src/views/Nav.tsx
import * as React from 'react';
interface Props {
activeRoute: string;
}
export class Nav extends React.Component<Props> {
render() {
const msg = ' <- You are here! :)';
return <ul>
<li><a href="/">Home</a>{this.props.activeRoute === '/' ? msg : ''}</li>
<li><a href="/about">About</a>{this.props.activeRoute === '/about' ? msg : ''}</li>
</ul>;
}
}
Note that while this component doesn’t use a DTO (it has nothing to do with the domain model), it uses an interface for its props. This is because the component is meant to be used by more than one view for the app, and its props interface lets us strongly type its parameters (in this case, the name of the active route). We don’t expect this component to have children, so we omit this.props.children
from its render
method, just like we did for our Home
view component.
Now we can revisit our index.tsx
file and add a second route (/about
), using our Shell
component as a base for the views of each route. The Nav component is a sibling of the view-specific components, and those two components become the two child components of the Shell
component:
// src/index.tsx
import * as express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { Home } from './views/Home';
import { Nav } from './views/Nav';
import { Shell } from './views/Shell';
const app = express();
app.get('/', function (req, res) {
const resData = `<!doctype html>
${ReactDOMServer.renderToStaticMarkup(<Shell>
<Nav activeRoute={'/'} />
<Home backgroundColor={'red'} currentDate={new Date()} />
</Shell>)}
`;
res.send(resData);
});
app.get('/about', function (req, res) {
const resData = `<!doctype html>
${ReactDOMServer.renderToStaticMarkup(<Shell>
<Nav activeRoute={'/about'} />
<Home backgroundColor={'blue'} currentDate={new Date()} />
</Shell>)}
`;
res.send(resData);
});
const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);
For simplicity, we won’t create a new view for the About page. We’ll just reuse the Home page’s view. Therefore, the Home
component is present in both Shell
components.
Notice as you build the new complete views for each route in index.tsx
that linters, the compiler, Intellisense etc will kick in to warn you if you omit a JSX attribute, like omitting the activeRoute
attribute from the Nav
component:
“Computer says no.”
You’ll get a helpful error message like:
Property 'activeRoute' is missing in type '{}'.
Reduce Boilerplate with a Render Function
The only thing left to do to help us with our work flow adding new routes is to remove some of the boilerplate that we have. We can’t include the <!doctype html>
directive in our Shell
component. We can move this into a function with a shorter name. Create a file called utils.ts
in the project directory. The .tsx
extension isn’t mandatory because we won’t be using JSX syntax inside our helper function. Use the following code for utils.ts
:
// src/utils.ts
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
export function renderReactView(el: React.ReactElement<any>): string {
return `<!doctype html>${ReactDOMServer.renderToStaticMarkup(el)}`;
}
The renderReactView
function’s parameter el
will allow it to accept any React component. The function is exported so we can use it elsewhere in our app.
At this point, our directory structure will look like this:
node_modules/...
src/
dtos/
HomeDto.tsx
views/
Home.tsx
Nav.tsx
Shell.tsx
index.ts
utils.ts
package.json
tsconfig.json
After importing the helper function into index.tsx and using it where appropriate, its code will look like this:
// src/index.tsx
import * as express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';
import { Home } from './views/Home';
import { Nav } from './views/Nav';
import { renderReactView } from './utils';
import { Shell } from './views/Shell';
const app = express();
app.get('/', function (req, res) {
res.send(renderReactView(<Shell>
<Nav activeRoute={'/'} />
<Home backgroundColor={'red'} currentDate={new Date()} />
</Shell>));
});
app.get('/about', function (req, res) {
res.send(renderReactView(<Shell>
<Nav activeRoute={'/about'} />
<Home backgroundColor={'blue'} currentDate={new Date()} />
</Shell>));
});
const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);
That’s it! We now have the ability to use JSX to build our views on a server side app. And the TypeScript compiler and IDEs it is linked to will warn us about type issues with the data for the views. This means you don’t need traditional IDEs like Visual Studio or IntelliJ IDEA-based IDEs to benefit from code completion for your views.
Long Term View Code Completion
Here’s where this will help us as we continue to build and maintain our app.
New Data for DTO
Suddenly, we need to include more data in our DTO. Users now have profile photos, so we now have a profilePhotoUrl
property added to the HomeDto
class. It isn’t an optional property, meaning we expect our hypothetical service classes to fetch this for us and we’re expected to always display it in the view:
// src/dtos/HomeDto.tsx
export class HomeDto {
backgroundColor: string;
currentDate: Date;
profilePhotoUrl: string; // uh oh. Time to fix the views
}
Well, that was easier than expected. We’re warned about the missing data:
Renaming Source Code Files and Classes
Let’s examine another scenario that can cause issues - renaming things. All of the following situations will result in us being warned about the breakage at the import statement:
- Renaming the source code file. For example,
Home.tsx
toHomePage.tsx
. - Renaming a class. For example, renaming the
HomeDto
class toHomePageDto
. - Renaming a file, but with a typo or a simelar error.
Here, Home.tsx
was renamed HomePage.tsx
, and we’re told of this problem at compile time:
Fun and Profit
By linking to the compiler, we get a lot of functionality for free. There’s a big push for cross platform development tools, and I’ve discovered that this is a great way to succeed with that goal. A big thanks goes out to all the work the teams behind TypeScript, Visual Studio Code, and React have done to give us these great tools!
The code for this tutorial is available on GitHub.