This is a tutorial to show how to create a lightweight Visual Studio 2022 ASP.NET Core SPA template with React style components supporting type checking, shared models, bundling and minification but no compile round.
Introduction
Few years ago, I needed a lightweight solution for small ASP.NET (Core) projects. My requirements were:
- Development done in full blown Visual Studio
- Type checking
- Reusable components
- ECMAScript module support
- No compile or very fast compile
- Bundling and minification for production
I tried Visual Studio included React templates with typescript and while it fulfilled most of my criteria not to mention access to all kind of fancy React libraries I could not bear the compile time needed to compile. And it was not lightweight at all. I tried number of other templates as well, for example Blazor which was not so mature at the time. It also was not at all lightweight, seemed to need quite a learning curve and creating bindings to JavaScript libraries I needed would have required time I could not bear. I also played with Visual Studio Code and while it would have greatly accelerated front end coding compared to full Visual Studio, I could not trade out C# of full Visual Studio. Also I did not want to break solution to multiple projects in multiple editors. Then I found great library Preact, a 3kb React clone with same API. They also had their own alternative to JSX, HTM (Hyperscript Tagged Markup), which runs directly in browser, hence precompilation is not needed. In this tutorial project template similar to Visual Studio’s React template is created step by step with ASP.NET Core, Preact, HTM and some other help libraries. Bootstrap will be uses for CSS as in original React template.
Here, I could also mention that prior to this template, I never tried to make anything like this myself but used only ready baked templates included in Visual Studio. I had checked what was inside, for example, Visual Studio's React template but didn't understand single line of it nor the tools used. That changed when I gave myself few days to dig into modern web development basics so this is also some kind of tutorial to those tools.
Result of this tutorial can also be downloaded from GitHub if you are in a hurry.
Creating Back End
Let’s start by creating a new project and choosing ASP.NET Core Web App and name it AspNetCorePreactHtm
with default settings. As we will use JavaScript to create DOM content, open Pages\Shared_Layout.cshtml and replace content with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AspNetCorePreactHtm</title>
</head>
<body>
@RenderBody()
@RenderSection("Scripts", required: false)
</body>
</html>
Open Pages\index.cshtml and replace content with:
@page
If you run the project, a blank page should open as there’s no visible html.
Creating Preact Front End with Virtual DOM that Compiles at Browser
First, delete all existing content in wwwroot/lib in solution explorer. Then right click empty wwwroot/lib folder and choose Add / Client-Side Library. In opening window, type bootstrap
in library textbox and hit enter. Latest Bootstrap version appears. Choose Choose specific files and select only css/bootstrap.min.css and js/bootstrap.bundle.min.js and click Install. Right click wwwroot/lib folder and choose again Add Client-Side Library. In opening window, type htm in library textbox and hit enter. Latest HTM library version appears. Choose Choose specific files and select only preact/standalone.module.js and click Install. Standalone module contains both Preact and HTM in one package. Right click wwwroot and choose Add / New Folder and name it src. Right click wwroot/src and choose Add / New Item / JavaScript file and name it App.js. Open created file wwwroot/src/App.js and paste the following code:
import { html, Component, render } from '../lib/htm/preact/standalone.module.js';
class App extends Component {
constructor() {
super();
}
render() {
return html`Hello World!`;
}
}
render(html`<${App} />`, document.body);
First line imports necessary JavaScript for Preact and HTM. Then follows Preact component that simply renders text "Hello world
". Last line then tells Preact to render component to html document's body
part.
Open again, _Layout.cshtml and replace content with the following snippet:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AspNetCorePreactHtm</title>
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<script defer type="module" src="~/src/App.js"></script>
</head>
<body>
@RenderBody()
@RenderSection("Scripts", required: false)
</body>
</html>
One line has been added and that is reference to App.js we just created. It has important definition, defer type="module"
, which tells browser that code contains modern ECMAScript code instead of common JavaScript. Run application again and hopefully, you will see Hello world
in opening web page. Preact's virtual DOM engine is wired up!
Adding WeatherForecast API to Back End
In Razor page applications, http APIs are defined by services. Next, we’ll create API that will return weather forecast like in default React template. Right click AspNetCorePreactHtm
application in solution explorer and choose Add / New Folder to root and name it Shared. Right click new Shared folder and choose Add / New Item / Code File and name it WeatherForecastSummary.cs. Paste the following code:
namespace AspNetCorePreactHtm.Shared
{
public enum WeatherForecastSummary
{
Freezing,
Bracing,
Chilly,
Cool,
Mild,
Warm,
Balmy,
Hot,
Sweltering,
Scorching
}
}
Add another code File to Shared folder and name it WeatherForecast.cs and paste the following code:
using System.ComponentModel.DataAnnotations;
namespace AspNetCorePreactHtm.Shared
{
public class WeatherForecast
{
public DateTime Date { get; set; } = DateTime.Now;
[Range(-50, 100)]
public int TemperatureC { get; set; } = 0;
[Range(-58, 212)]
public int TemperatureF { get; set; } = 32;
public WeatherForecastSummary Summary
{ get; set; } = WeatherForecastSummary.Cool;
}
}
There are a lot of comments we don’t need right now but we’ll come back to that later. Right click project as Solution Explorer and choose Add / New Item / Code File and name it WeatherForecastService.cs. Paste the following code:
namespace AspNetCorePreactHtm
{
using AspNetCorePreactHtm.Shared;
public interface IWeatherForecastService
{
string Get();
}
public class WeatherForecastService : IWeatherForecastService
{
public string Get()
{
var WeatherForecasts = new List<WeatherForecast>();
for (int i = 1; i <= 5; i++)
{
WeatherForecast wf = new WeatherForecast();
wf.Date = DateTime.Now.AddDays(i);
wf.TemperatureC = Random.Shared.Next(-20, 55);
wf.TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556);
wf.Summary = (WeatherForecastSummary)Random.Shared.Next
(0, Enum.GetNames(typeof(WeatherForecastSummary)).Length-1);
WeatherForecasts.Add(wf);
}
return System.Text.Json.JsonSerializer.Serialize(WeatherForecasts);
}
}
}
This defines our service of weather forecasts. To make it effective, we need to register it to web application builder. Open Program.cs and just after line builder.Services.AddRazorPages();
paste the following snippet:
builder.Services.AddSingleton<AspNetCorePreactHtm.IWeatherForecastService,
AspNetCorePreactHtm.WeatherForecastService>();
We still need to define relative path for http GET
request that responds with our new service. Just before the last line of file app.Run();
paste the following snippet:
app.MapGet("/api/weatherforecast",
(AspNetCorePreactHtm.IWeatherForecastService service) =>
{
return Results.Ok(service.Get());
});
Now WeatherForeCast
API is complete. You may notice that created WeatherForecast
API returns forecasts as json string instead of returning an array. Reason for this is that service first serializes send data to JSON. Default serialization converts property name to camel case (first letter always small). Calling System.Text.Json.JsonSerializer.Serialize
directly maintains Pascal case and property names will remain.
Creating Front End SPA
Now it’s time to create our actual front end SPA (Single Page Application). Each page will be defined in a separate file.
Home Page
Let’s start with the home page. Right click wwroot/src and choose Add / New Item / JavaScript file and name it Home.js. Paste the following code:
"use strict";
import { html, Component } from '../lib/htm/preact/standalone.module.js';
export class Home extends Component {
constructor(props) {
super(props);
}
render() {
return html`
<div>
<h1>Hello, world!</h1>
<p>Welcome to your new single-page application, built with:</p>
<ul>
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and
<a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'
target="_blank" rel="noopener noreferrer">C#</a>
for cross-platform server-side code</li>
<li><a href='https://preactjs.com/' target="_blank"
rel="noopener noreferrer">Preact</a> with
<a href='https://github.com/developit/htm'>HTM
(Hyperscript Tagged Markup)</a> rendering for client-side code</li>
<li><a href='http://getbootstrap.com/' target="_blank"
rel="noopener noreferrer">Bootstrap</a> for layout and styling</li>
</ul>
<p>To help you get started, we have also set up:</p>
<ul>
<li><strong>Client-side navigation</strong>.
For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
</ul>
</div>
`;
}
}
Home component is a plain component and renders just static HTML. Option use strict
at start of file will force us to write cleaner code, declare all used variables, etc.
Counter Page
Next we’ll create counter page. Right click wwroot/src and choose Add / New Item / JavaScript file and name it Counter.js. Paste the following code:
"use strict";
import { html, Component } from '../lib/htm/preact/standalone.module.js';
export class Counter extends Component {
constructor(props) {
super(props);
this.state = { currentCount: 0 };
}
incrementCounter() {
this.setState({
currentCount: this.state.currentCount + 1
});
}
render() {
return html`
<div>
<h1>Counter</h1>
<p>This is a simple example of a React component.</p>
<p aria-live="polite">Current count: <strong>${this.state.currentCount}</strong></p>
<button class="btn btn-primary" onClick=${() =>
this.incrementCounter()}>Increment</button>
</div>
`;
}
}
Counter component demonstrates how to wire DOM element to your own function inside template literal. Notice how state's value currentCount
is fetched on render as well as increment button's onClick
event is wired to component's internal incrementCounter()
function.
FetchData Page
Right click wwroot/src and choose Add / New Item / JavaScript file and name it FetchData.js. Paste the following code:
"use strict";
import { html, Component } from '../lib/htm/preact/standalone.module.js';
var feelsLike = ["Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
export class FetchData extends Component {
constructor(props) {
super(props);
this.state = { forecasts: [], loading: true };
}
componentDidMount() {
this.populateWeatherData();
}
async populateWeatherData() {
const response = await fetch('api/weatherforecast');
const json = await response.json();
this.state.forecasts = JSON.parse(json);
this.state.loading = false;
this.forceUpdate();
}
render() {
if (this.state.loading) {
return html`<p><em>Loading...</em></p>`;
}
else {
return html`
<div>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
${this.state.forecasts.map(f => html`
<tr>
<th scope="row">${f.Date.toLocaleString()}</th>
<td>${f.TemperatureC}</td>
<td>${f.TemperatureF}</td>
<td>${feelsLike[f.Summary]}</td>
</tr>
`)}
</tbody>
</table>
</div>
`;
}
}
}
FetchData
component demonstrates how to render list inside template literal with JavaScript map method. Fetching data by http call from server may be a lengthy process and therefore async function, that triggers render when fetch is done, is used for it.
Main Component
Now all pages are defined and it’s time to update application's main component, App component, so that it will route to and render created page components. Open wwwroot/src/App.js and replace the existing code with:
"use strict";
import { html, Component, render } from '../lib/htm/preact/standalone.module.js';
import { Home } from './Home.js';
import { Counter } from './Counter.js'
import { FetchData } from './FetchData.js'
var pages = { '#Home': Home, '#Counter': Counter, '#FetchData': FetchData };
class App extends Component {
constructor() {
super();
window.onpopstate = () => { this.Navigate(null); };
this.state = { navPage: '#Home' };
}
Navigate(toPage) {
this.setState({ navPage: toPage });
}
render() {
let page = this.state.navPage ? this.state.navPage :
window.location.hash ? window.location.hash : Object.entries(pages)[0][0];
if (window.location.hash !== page) {
window.history.pushState({}, page, window.location.origin + page);
}
let content = html`<${pages[page]} />`;
return html`
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<a class="navbar-brand" style="cursor: pointer"
onClick=${() => this.Navigate("#Home")}>PreactHtmStarter</a>
<button class="navbar-toggler" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" style="cursor: pointer"
onClick=${() => this.Navigate("#Home")}>Home</a>
</li>
<li class="nav-item">
<a class="nav-link" style="cursor: pointer"
onClick=${() => this.Navigate("#Counter")}>Counter</a>
</li>
<li class="nav-item">
<a class="nav-link" style="cursor: pointer"
onClick=${() => this.Navigate("#FetchData")}>Fetch data</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid body-content">
${content}
</div>
`;
}
}
render(html`<${App} />`, document.body);
Lot has changed in App.js but we'll dig into it a little later. Now run the project. Web page is similar to default React template opens. Use navigation bar links to browse between pages. Counter page count button increments count on page and Fetch Data page shows random weather forecasts from http api we created earlier. Browser’s back and forward buttons follow history of our earlier navigations within our single page application.
So How Does It All Work?
Pages
In React's or Preact's perspective, everything is a component: App
is our main component and Home
, Counter
and FetchData
are the page components. Because App
component imports page components from other JavaScript files they must be exported in their source code, for example, export class Counter
extends Component
in Counter.js. Each component has render function that renders its contents to screen. A component can contain other components like App
component contains all page components.
Router
Typically React and Preact use some external router library but here router is embedded in App component by super simple hash router implementation, which uses browser window history api to store navigations. It simply consist of dictionary containing page name as key and page component as value. #
character on name of each page tells browser to use JavaScript API instead of fetching data from server. When App component is created, constructor function is called first and there window back navigation handler function is defined. Also html navbar links do not contain any href link but mimics one by changing cursor on hover and wiring onClick
event to our own navigation function with page name as parameter. Navigate
function call then uses Preact component’s SetState
function which both mutates component’s internal state by merging any value in SetState
call to components internal state and triggers render to update view, in this case, navigated page component. When navigating to page, its name and virtual path are pushed to browsers navigation history to be popped later. Render
function then decides page to route: if state navPage
is null
page name is fetched from browser history. Page component is then red from dictionary and rendered to content section of App component. This is not my invention but from vanilla JavaScript web article I read years ago. I just ported it to Preact component. I'd really like to give credit to the original author, but have no recollection who it was.
Template Literals
Template literals look much like JSX templating language that React uses. JSX must be precompiled to JavaScript React nodes for browser to understand them. On the other hand, HTM library uses template literals to transpile them to Preact virtual DOM nodes in browser so precompile step can be skipped. While JSX cannot use some reserved words html like class and uses className
instead template literals don't have this restriction. Transpiling is also possible to do at server side but we’ll get back to that on minification.
Generic Type Checking
Now template is already fully functional. If project using it is kept small, this might just be enough. JavaScript is by nature non-typed language which means that any variable can have any value at all at any time and that is fine with JavaScript and cannot spot misspellings, etc. at editor. Therefore any increased complexity will benefit of write time type checking so that’s what we will do next. For that, we will use… TypeScript. That may sound funny as we are not compiling our front end code but run it as it is. Whether one likes TypeScript or not, it has an awesome feature – type checking without compiling. In fact, it is so clever that it can automatically fetch type script typings of client side libraries you use it they are definitely typed. Also, it is very effective at recognizing types of variables you use in your JavaScript. Right click AspNetCorePreactHtm
project in solution explorer and select Manage NuGet Packages. Choose Browse and type typescript
in search text box. From matches, select Microsoft.TypeScript.MSBuild
package and install the latest version. Right click AspNetCorePreactHtm
project in solution explorer and select Add New Item and select npm Configuration File. Keep default name package.json. Replace package.json file content with the following snippet:
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"devDependencies": {
"typescript-lit-html-plugin": "0.9.0"
}
}
To use npm
packages, you should have Node.js installed. Right click package.json and select Restore packages. This installs typescript-lit-html-plugin. It does not effect operation of solution in any way but highlights html inside template literals. So instead of just red string content color and also should bring some sort of intellisense writing html inside template literal string. By should I mean that it has not worked perfectly for me with Visual Studio 2022 but had no problem with previous versions of Visual Studio. At the time of writing this tutorial, I just don't know any better library for the job. Right click AspNetCorePreactHtm
project in solution explorer and select Add New Item and select TypeScript JSON Configuration File. Keep default name tsconfig.json. Replace tsconfig.json file content with the following snippet:
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"target": "ES2022",
"plugins": [
{
"name": "typescript-lit-html-plugin"
}
]
},
"exclude": [ "wwwroot/lib" ],
"include": [ "wwwroot/src" ]
}
This configuration tells TypeScript to check JavaScript files also while default it only checks TypeScript files (that we don’t have). Also it is not allowed to emit any transpiled code, only check. Latest ECMAScript specification is used for checking. Plugin we installed just before is declared in plugins. In your try to run project now, you will notice that it won’t compile anymore but gives number of error messages concerning Preact + HTM standalone package because TypeScript doesn’t find typings for the file. This is solved by opening installed package wwwroot/lib/htm/preact.standalone.module.js in editor. Add new line before JavaScript and type or paste:
This will notify TypeScript not to check its contents. Now the project should compile and run as before.
JSDoc Strongly Typed Type Checking
Now client end has some kind of type checking but it is not very strong as TypeScript can only try to guess types of our JavaScript variables. How can we define their typing without TypeScript code? Answer is JSDoc definitions that are defined as JavaScript comment annotations and do not modify JavaScript itself in any way. TypeScript understands these annotations and can use them for type checking. Let’s use App.js as simple example. At App.js file, place cursor at start of line Navigate(toPage) {
and type “/**
”. Editor will create JSDoc template like this for you automatically:
Replace it with the following snippet:
Where Navigates to page by name
is generic definition of the function and @param {string} toPage Page name to navigate
defines function parameter toPage
to be string
type with annotation comment. When writing JavaScript code intellisense will now serve these to you as you write. Try to hover mouse over this.Navigate
calls at template literal part of component and you will right away see how intellisense provides just annotated information about the function. Try to change this.Navigate(null);
call in components constructor section to this.Navigate(1);
. You will get warning as number 1 is not assignable to string
. Basically any class, structure or variable can be typed by JSDoc and you are free to type only the ones you see suitable.
Shared Models
There are number of libraries to convert C# models to TypeScript. I assumed that there would be ones to convert C# models to JSDoc annotated ECMAScript classes also and I was surprised when I did not find any. Getting to know Roslyn code analysis had been a long time in my to-do list so I made a small library with it to convert C# models to JSDoc annotated ECMAScript
classes. It is not very streamlined but if your models are not too complex, it should get the job done. You can download it from GitHub. Compile program to generate CSharpToES.exe. Once compiled, it can be used at the project (or any other project). Right click project root at Solution Explorer and choose Properties. Navigate to Build and Events. To pre-build event, add the following text:
<path to CSharpToES.exe> $(ProjectDir)Shared $(ProjectDir)wwwroot\src\shared
Where <path csharptoes.exe="" to="">
should be something like:
C:\CSharpToES\CSharpToES\bin\Release\net6.0\CSharpToES.exe
depending on where you downloaded it on your computer. Of course, you can also copy compiled content to easier path like C:\CSharpToES\CSharpToES.exe and reference it there. $(ProjectDir)Shared then tells CSharpToES relative source folder of shared C# models and $(ProjectDir)wwwroot\src\shared relative destination folder for compiled js class files as it reads C# models from files, not by reflection. Conversion must happen pre-build, otherwise compiler cannot catch compile errors for JavaScript code that uses compiled code. CSharpToES
follows source folder structure so that each .cs file is compiled to equivalent .js file. To demonstrate this is actually the reason why WeatherForecastSummary.cs and WeatherForecast.cs file are put in separate Shared folder and divided in two separated files. If you compile or run project, ECMAScript conversion will be done and equivalent WeatherForecastSummary.js and WeatherForecast.js files are created in wwwroot/src/shared folder. WeatherForecastSummary.js will look like this:
export const WeatherForecastSummary = {
Freezing: 0,
Bracing: 1,
Chilly: 2,
Cool: 3,
Mild: 4,
Warm: 5,
Balmy: 6,
Hot: 7,
Sweltering: 8,
Scorching: 9
}
JavaScript doesn’t have enum
type but this approach is pretty close it. Any comments in C# model are compiled to JSDoc annotations to help front end coding. WeatherForecast.js will look like this:
import { WeatherForecastSummary } from './WeatherForecastSummary.js';
export class WeatherForecast {
#Date;
#TemperatureC;
#TemperatureF;
#Summary;
constructor() {
this.#Date = new Date();
this.#TemperatureC = 0;
this.#TemperatureF = 32;
this.#Summary = WeatherForecastSummary.Cool;
}
get Date() {
return this.#Date;
}
set Date(val) {
if (val instanceof Date) {
this.#Date = val;
}
}
get TemperatureC() {
return this.#TemperatureC;
}
set TemperatureC(val) {
if (typeof val === 'number') {
this.#TemperatureC = (val < -50 ? -50 :
(val > 100 ? 100 : Math.round(val)))
}
}
get TemperatureF() {
return this.#TemperatureF;
}
set TemperatureF(val) {
if (typeof val === 'number') {
this.#TemperatureF = (val < -58 ? -58 :
(val > 212 ? 212 : Math.round(val)))
}
}
get Summary() {
return this.#Summary;
}
set Summary(val) {
if ([0,1,2,3,4,5,6,7,8,9].includes(val)) {
this.#Summary = val;
}
}
toJSON() {
return {
'Date': this.#Date,
'TemperatureC': this.#TemperatureC,
'TemperatureF': this.#TemperatureF,
'Summary': this.#Summary
}
}
static fromJSON(json) {
let o = JSON.parse(json);
return WeatherForecast.fromObject(o);
}
static fromObject(o) {
if (o != null) {
let val = new WeatherForecast();
if (o.hasOwnProperty('Date')) { val.Date = new Date(o.Date); }
if (o.hasOwnProperty('TemperatureC')) { val.TemperatureC = o.TemperatureC; }
if (o.hasOwnProperty('TemperatureF')) { val.TemperatureF = o.TemperatureF; }
if (o.hasOwnProperty('Summary')) { val.Summary = o.Summary; }
return val;
}
return null;
}
static fromJSONArray(json) {
let arr = JSON.parse(json);
return WeatherForecast.fromObjectArray(arr);
}
static fromObjectArray(arr) {
if (arr != null) {
let val = [];
arr.forEach(function (f) { val.push(WeatherForecast.fromObject(f)); });
return val;
}
return null;
}
}
This takes an opionated approach to models. Simplest approach would have been to declare properties in constructor. That way doesn’t protect properties as properties would simply become typed fields and bug in JavaScript could then write any value to them. I wanted protect properties so that they could not be for example nulled at client side if C# source property is not marked nullable. This is done by private #
fields, which contains actual property value. Private
field is then only accessed by getters and setters just like in C#. As C# has number of different numeric types and JavaScript basically only has one setters have code to limit value to limits of C# data type. Downside of this approach if increased complexity and increased number of JavaScript code. But as they are made automatically, it is not a big thing. Also, serialization and deserialization of ECMAScript
classes need extra work while plain JavaScript objects can be serialized simply by JSON.Stringify
and deserialized with JSON.parse
. JSON.parse
checks if object has toJSON
function defined and uses it so serializing these models work normally with JSON.Stringify
. Deserialization on the other hand is more tricky. For that, custom static
functions are provided: deserialization from JSON string, from plain JavaScript object or array of them. Library supports the following features:
- Automatic import export generation between files
- Dates are handled as date object instead of string
- C# Dictionaries are converted to JS Maps instead of plain objects because
Map
can be strongly types with JSDoc - Simple initialization of properties and fields is supported so that creating new object at JavaScript would have identical values than new object in C#
- Simple inheritance
If you hover f.Date
, f.TemperatureC
, f.TemperatureF
or f.Summary
variables at FetchData.js, you’ll notice that intellisense doesn’t have a clue what they are. Any misspelling could crash the application at run time. To demonstrate benefits of shared model, we’ll modify FetchData
component. Replace FetchData.js with the following code:
"use strict";
import { html, Component } from '../lib/htm/preact/standalone.module.js';
import { WeatherForecast } from './Shared/WeatherForecast.js';
var feelsLike = ["Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"];
export class FetchData extends Component {
constructor(props) {
super(props);
this.state = { forecasts: [], loading: true };
}
componentDidMount() {
this.populateWeatherData();
}
async populateWeatherData() {
const response = await fetch('api/weatherforecast');
const json = await response.json();
this.state.forecasts = WeatherForecast.fromJSONArray(json);
this.state.loading = false;
this.forceUpdate();
}
render() {
if (this.state.loading) {
return html`<p><em>Loading...</em></p>`;
}
else {
return html`
<div>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
${this.state.forecasts.map(f => html`
<tr>
<th scope="row">${f.Date.toLocaleString()}</th>
<td>${f.TemperatureC}</td>
<td>${f.TemperatureF}</td>
<td>${feelsLike[f.Summary]}</td>
</tr>
`)}
</tbody>
</table>
</div>
`;
}
}
}
Now page has types defined for WeatherForecast
and FetchData
components internal state. WeatherForecast
typing are imported from shared model. If you hover over f.Date
, f.TemperatureC
, f.TemperatureF
or f.Summary
variables, intellisense shows its data type and comments written in C# model. If you misspell, for example, f.Summary
to f.Summary
, the compiler detects that WeatherForecast
class does not have property Summary
and project will not compile. Also generated array deserializer is used in WeatherForecast.fromJSONArray(json)
call. If you want to define type of component’s internal state object type definition is probably easiest like here is done with FetchDataState
object definition. One thing to note is that using SetState
mutation to change state and trigger render is not recommended as intellisense does not understand that setState
mutation refers to internal state object. That is why here in populateWeatherData
function, state is altered directly and then by forceUpdate
call, Preact is notified that state has changed and render is needed.
Bundling and Minification
Now template is fully functional and supports type definitions. It can be published as it is and modern browsers, that support modules, can run it. There still are a few problems:
- JavaScript is not bundled. Browser has to fetch all files one by one by parsing import calls causing performance hit especially if there is lot of source files
- Files are send to browser unminified
- Your JavaScript source code is fully exposed
Lets start with bundling. Open package.json and replace content with the following:
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"scripts": {
"build-babel": "babel wwwroot/src -d wwwroot/babel",
"build-rollup": "rollup wwwroot/src/App.js --file wwwroot/bundle.js --format esm",
"build-rollup-babel": "rollup wwwroot/babel/App.js
--file wwwroot/bundle.js --format esm",
"build-terser": "terser --compress --mangle -- wwwroot/bundle.js >
wwwroot/bundle.min.js",
"trash-babel": "trash wwwroot/babel",
"build-purgecss": "purgecss
--css wwwroot/lib/bootstrap/css/bootstrap.min.css --content wwwroot/bundle.js
--output wwwroot/bundle.min.css",
"build-all": "npm run build-rollup &&
npm run build-terser && npm run build-purgecss",
"build-all-babel": "npm run build-babel &&
npm run build-rollup-babel && npm run build-terser &&
npm run build-purgecss && npm run trash-babel"
},
"devDependencies": {
"@babel/cli": "7.17.6",
"@babel/core": "7.17.5",
"purgecss": "4.1.3",
"rollup": "2.69.0",
"terser": "5.12.0",
"trash-cli": "5.0.0",
"typescript-lit-html-plugin": "^0.9.0"
},
"dependencies": {
"babel-plugin-htm": "3.0.0"
}
}
Then right click package.json and select Restore packages. Packages appear to new folder node_modules at root of project. There are number of packages added and here’s shortly what they do:
- Rollup bundles multiple ECMAScript files into one
- Terser minifies bundled ECMAScript files
- Babel converts with
babel-plugin-htm
template literals to Preact nodes as described at HTM project’s site - trash-cli is used to clean up temporary files created by Babel
- PurceCSS minifies CSS by parsing from JavaScript files which CSS styles are actually used in application and removes all the rest Scripts are the actual calls to libraries. Scripts can be also bundled into one call and they are then processed that order. Two such calls are defined:
build-all
and build-all-babel
. Let’s try build-all first
. Right click project at solution explorer and select Open In Terminal. In terminal, type or paste:
npm run build-all
and hit enter. Files bundle.js with bundle.min.js and bundle.min.css appear in wwwroot
. Script runs first subscript build-rollup
which creates bundle.js file Open bundle.js and you will notice that rollup has bundled all our code in one file and shortened variable names where possible. All comments, tabs and linebreaks are still there. Next script runs subscript build-terser
which minifies just created bundle.js. Open bundle.min.js and you will notice that all comments, tabs, line breaks and whitespaces are removed from JavaScript code parts. Template literals of components however are as they were before. This is because they are not code but string
s, Terser cannot know what we are using it for. build-purgecss
tells PurgeCSS
to extract all CSS from bootstrap.min.css that is actually used in created bundle bundle.js and write result to bundle.min.css file. Source code is now bundled and minified but one step further can be made. Babel with babel-plugin-htm
can compile component template literals to Preact nodes. If makes bundle slightly smaller thus it loads faster and browser JavaScript parser does not have to compile them as they are already compiled which accelerates code startup at browser. Before running babel, a config file needs to be defined. Right click project, Add / New Item / JSON file, name it babel.config.json and paste the following to file:
{
"presets": [],
"plugins": [
[
"babel-plugin-htm",
{
"tag": "html",
"import": "../lib/htm/preact/standalone.module.js"
}
]
]
}
This tells babel to use babel-plugin-htm
and where Preact standalone module is located. In terminal, type or paste:
npm run build-all-babel
and hit enter. This adds one step at the previous chain: before giving code for Rollup to bundle it runs build-babel
step which triggers babel to compile each of our JavaScript file to wwwroot/babel folder. Open wwwroot/babel/App.js and you will notice that Babel along with babel-plugin-htm
has compiled template literal in render
function to Preact h
node calls where h
corresponds to React’s createElement
. Rest of the script is the same as in build-all
except Rollup is directed to use these Babel compiled source files instead of the original ones. If you open bundle.min.js, you will notice that template literals have gone and all application JavaScript is on minified line. Bundling and minification is needed only prior to publish, it is not needed for debugging. You can run it from terminal when publishing but if you have memory like mine, it can be automated for example by adding it to project’s build events by right clicking project and selecting Properties. Then at build events section, add npm run
call to Post-build events. In that case, script will run each time you compile project.
Publishing
Now we have bundled and minified JavaScript and CSS for publishing. If we publish project now, they will not be used as html in Pages\Shared_Layout.cshtml still calls our source code at wwwroot/src and everything that is in wwwroot is published. Close project for a second and navigate to project definition file EsSpaTemplate.csproj and open it for example in Notepad (my favourite is Notepad++). After last PropertyGroup
definition, paste the following snippet, save and reopen project.
<ItemGroup>
<Content Update="wwwroot\src\**" CopyToPublishDirectory="Never" />
<Content Update="wwwroot\bundle.js" CopyToPublishDirectory="Never" />
<Content Update="wwwroot\babel\**" CopyToPublishDirectory="Never" />
</ItemGroup>
This will prevent publishing of any source code. Maybe this is possible inside Visual Studio also but I am not aware how. Next open Pages\Shared_Layout.cshtml. Replace content with the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PreactHtmStarter</title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="~/bundle.min.css" />
</environment>
<script src="~/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<environment include="Development">
<script defer type="module" src="~/src/App.js"
asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script type="module" src="~/bundle.min.js"
asp-append-version="true"></script>
</environment>
</head>
<body>
@RenderBody()
@RenderSection("Scripts", required: false)
</body>
</html>
Razor engine has a neat feature that it can inject different code to browser in debug mode than in published one. Directives <environment include="Development">
and <environment exclude="Development">
are added for that purpose. Simply, this directs to use the source code in wwwroot/src when debugging and to use minified code when published. Now template is feature ready against what was promised in preface.
I made a little test by publishing default React template to IIS and publishing this template to IIS with following results in Chrome browser.
Default React template needed to load:
192 kb + 8.2 kb ≈ 200 kb JavaScript 165 kb + 573 b ≈ 166 kb css
This template needed to load:
78.4 kb + 16.9 kb ≈ 96 kb JavaScript 10.3 kb css
Where 78.4 kb is bootstrap.bundle.min.js and 16.9 kb is minified code actually written in this tutorial including embedded Preact and HTM.
Summary
Basically, here’s what we get with this template:
- React components with Preact and HTM template literals in light weight template without need to precompile to JavaScript.
And what we don’t get:
- React libraries. There are tons of React libraries out there practically for any purpose you might think of. But they all use JSX and hence need compile round
So far, it has not been a problem to me to write necessary components for small web user interfaces I have used this template for. For same reason, I maybe would not use it in large and extensive projects.
Happy coding and thanks if you had stamina to read this far. This tutorial ended up being a lot longer than I expected.