Using React, Redux and Webpack with Dotnet Core to build a modern web frontend
This is part of a series in my exploration of a modern web architecture, RROD (React, Redux, Orleans and Dotnet). For the introduction, see here.
What Framework to use?
React is actually not a complete framework, it is just a View engine. For data handling and logic, most React projects add some additional library. There are several good options, but Redux has become mainstream, after its creator Dan Abramov introduced it with a brilliant presentation. Redux uses an event-sourcing mechanism, where events (called Actions in Redux) flow through the state object tree where they are handled by updating that state, after which React will re-render the View. The difference with traditional event sourcing architectures is that, in Redux, the event handler is a reducer function [(state, action) => state] operating on immutable data, which makes it cheap to keep references to all previous states. This, in turn, allows for “time travel”: rewind events, update code, replay the events on the updated code. It’s pretty cool.
The RROD Implementation
What you get, on top of a normal Dotnet Core MVC project are:
ConfigureServicesfunction in Startup.cs.
- A ClientApp folder with a starter website, written in typescript. In my case, this is an Isomorphic web application with entry points for both server and client, using webpack to drive the compilation and bundling process. The application references the
- A razor viewpage
Index.cshtmlthat “boots” the ClientApp.
- Build targets in the .csproj project file to run npm install and webpack as part of the Dotnet build and publish commands.
Typewriter — generating typescript from C# code
To assist in synchronizing C# and Typescript code, specifically Redux State and Actions, and data passed via Web API models, I used Typewriter. Typewriter works as a Visual Studio plugin, and the generated typescript code is exported at development time in a “server” folder under ClientApp (where all client code is), so it can be referenced by the rest of the typescript client code.
I created two templates:
- Models.tst — for all classes with a name ending in “Model” or “Result”, this template generates a typescript interface.
- Redux.tst — For all classes with a name ending in “Action” this template converts a Redux Action on the server (where the Type is build-in) to the equivalent typescript interface, with the type as a string property on the object. I also implemented the equivalent, reflection based back-and-forth conversion in .NET. It also generates a string constant for the action name, and generates an interface for classes with a name ending in “State”.
With this in place, I can be sure that models are always correctly translated between server and client, without duplicate work. This works very well, and the type safety of typescript avoids a lot of problems before they happen.
React Components in Typescript
I think that is very clean. It’s like ASP Web Forms, made cool again :-)
AddNodeServices() is optional, starting it with
LaunchWithDebugging = true makes it possible to attach a debugger. To do that with VS Code, configure a Node.js launch task using “Legacy Protocol” to attach to the node debugger on port 5858 (which is the default). Debugging transpiled ES2015 is not perfect, but I was able to set breakpoints and understand how node was executing my typescript code server-side.
The Controller Action for a JavascripServices web application does nothing unusual. The magic happens in the View
<div id=”react-app” asp-prerender-module=”ClientApp/dist/main-server”>Loading…</div><script src="~/dist/main-client.js" asp-append-version="true"></script>
webpack.HotModuleReplacementPlugin that makes Hot Module Reloading (HMR) work. This means it is important to NOT add those modules in your webpack.config.js yourself, as having them twice results in hard-to-understand conflicts.
Warning: danger ahead
It’s important to know that the webpack, babel and npm configurations are all very finicky. You can very easily break things by changing something in the webpack configuration or in package.json. Things may even break without any participation on your part: when minor new versions of dependencies are published, a new typescript version comes out, you run it on another machine that has another version of node, anything basically…
Also, there are many ways to specify a webpack configuration: loaders can be specified as a string, as an array of strings, as an object, as an array of objects, in the (deprecated)
.loader section or in the (new)
If you manage to get all this to work without breaking anything you get rewarded with Hot Module Reloading. That means that modifying any typescript (.ts or .tsx) file while running in Development mode results in an automatic refresh of the browser screen, usually without even losing state. You can install Chrome plugins for React and Redux that can show you all the Actions that have happened in the past and lets you change them. You can use a slider to undo Actions, change code, and replay those Actions on the updated code. It is very cool.
This turns out to be straigtforward. Just pass the data from the Controller into the serverside View
Still another way to pass initial data to the client is by sending it in a client-readable cookie.
The average page size on the internet currently stands at 3Mb. That is terrible, but when building a web application, using npm and webpack, adding a few cool libraries to your project, it’s easy to see how pages can grow to that size.
You can get the
CompressedStaticFilesMiddleware from NuGet. This simple method reduces bundle size by a factor of 3, without adding any load to the CPU, because the files are pre-compressed.