A comparison of Blazor and React when it comes to modern web development—a review of the pros and cons. Does Blazor stack up?
Blazor WebAssembly (or Blazor Wasm, as all the cool kids are calling it!) has arrived, but how does it stack up compared to the other, more established options for building "modern" web applications?
Let's start with a comparison to React JS.
Note: Interested in how Blazor stacks up against Angular or Vue instead? Check out a comparison of Blazor and Angular here and a comparison of Blazor and Vue here.
Specifically we'll explore how the following aspects work (for both frameworks):
Before we dig in, it's worth noting this article focuses on Blazor Wasm, which runs in the browser using WebAssembly. That said, many of the points are equally valid if you're looking at using Blazor Server instead.
React is a UI library which helps you build responsive, interactive user interfaces for your web-based applications.
Unlike frameworks such as Angular, React focuses purely on the component-building aspects of your app and doesn't tackle other concerns (like routing), preferring instead to leave it up to you to choose the best tool for your specific use case.
When you create a new React app, it will automatically bring in various packages it needs to work (using npm or its alternative, Yarn) and you can then bring in additional packages to handle things like routing, handling forms, making network requests.
You write your application using HTML (well, actually JSX but we'll get to that), CSS, and JavaScript.
To publish your app, you can run a command to bundle it up (using a bundler such as webpack), take the static bundled files (HTML, JavaScript, etc.) and deploy them to a web server.
When someone visits the deployed site, the browser JavaScript engine kicks in and runs your application code, rendering elements via the browser's DOM and handling various browser events (users clicking buttons, entering text in an input, etc.).
If you need to fetch data from a database or similar, you can make HTTP requests from your application to a server, which will return the data for your app to display.
Blazor is a framework which also enables you to build client web applications that run in the browser, but using C# instead of JavaScript.
When you create a new Blazor app, it arrives with a few carefully selected packages (the essentials needed to make everything work) and you can install additional packages using NuGet.
From here, you build your app as a series of components, using the Razor markup language, with your UI logic written using C#.
To publish your app, you can use .NET's built-in publish
command, which bundles up your application into a number of files (HTML, CSS, JavaScript and DLLs) which can then be published to any web server that can serves static files.
When a user accesses your Blazor Wasm application, a Blazor JavaScript file takes over which downloads the .NET runtime, your application and its dependencies, before running your app using WebAssembly.
Blazor then takes care of updating the DOM, rendering elements and forwarding events (such as button clicks) to your application code.
By far the simplest way to spin up a new React application is to use "Create React App."
You need these commands:
npx create-react-app my-app
cd my-app
npm start
CRA also gives you easily accessed commands for bundling up your app ready for deployment.
For Blazor, you can use Visual Studio or spin up a new project via the command prompt.
dotnet new blazorwasm
cd blazorwasm
dotnet run
You have a few other options, like the ability to include infrastructure for authenticating users, and whether to host your Blazor app in an ASP.NET web application, but the command above is the simplest option to get started.
With React you build your applications as a series of components, using a declarative approach.
Here's an example:
import React, { useState} from 'react';
export default function Greeting() {
const [name, setName] = useState();
const nameChanged = (e) => {
setName(e.target.value)
}
return (<>
<label>What's your name?</label>
<input type="text" onChange={nameChanged} placeholder="Bob"/>
<span>Hello {name}</span>
</>)
}
If you're not familiar with React, there's a little to unpack here.
React components are written using JavaScript.
Here we export a JavaScript function called Greeting
.
This Greeting
function represents a React component, and exporting it means we can easily reference this elsewhere in our app (so we can render it wherever we want).
Our Greeting
function returns something which looks suspiciously like HTML but is actually JSX.
JSX is really JavaScript.
For example, when we declare span
as JSX in a component, React will turn this into JavaScript which will be invoked to render the relevant HTML markup in the browser.
In practice, you can usually just write your JSX as if it were HTML and everything "just works!"
If you look at the input
element, you'll notice we've declared an event handler for the onChange
event. When you type something into this input, it will invoke our nameChanged
function.
This will update the state of our component using something called hooks. Specifically we're using the UseState
hook to update the value of name
to whatever the user typed in.
Finally, in our span
element we render the value of name
resulting in a friendly personalized greeting for our user.
As we change the value in the input, the greeting instantly updates to reflect the new value.
Generally speaking, everything you build in React revolves around components, which you can then render wherever you want, like this:
<h1>
A brief introduction to React...
</h1>
<Greeting />
In summary, a React UI:
Blazor adopts a very similar approach to React in that you build your UI using components.
Where it differs is that you use Razor and C# (instead of JSX and JavaScript) to write your markup and UI logic.
<label>What's your name?</label>
<input type="text" @bind-value="Name" @bind-value:event="oninput" placeholder="Bob"/>
<span>Hello @Name</span>
@code {
public string Name { get; set; }
}
This operates exactly the same way as the React example when you run it in the browser.
We've got roughly the same markup, but this time we have used Blazor's @bind
syntax to bind our input to a property called Name
.
When the user enters their name, the Name
property will be updated with the value they enter.
By default, Blazor would update the value of Name
on blur (when we clicked out of the text input) so we've added @bind-value:event="oninput"
to make it update the property as soon as we start typing.
As with React you're now free to render this component wherever (and as often as) you like.
<h1>
A brief introduction to Blazor...
</h1>
<Greeting />
In summary, a Blazor UI:
There are two main "out of the box" approaches to handling state in your React components.
They can either interact with their own state (as we saw when we stored Name
in our example), or they can accept data via props:
export default function Greeting(props) {
const [name, setName] = useState();
const nameChanged = (e) => {
setName(e.target.value)
}
return (<>
<h2>{props.headline}</h2>
<label>What's your name?</label>
<input type="text" onChange={nameChanged} placeholder="Bob"/>
<span>Hello {name}</span>
</>)
}
Here we've added a props
argument to our Greeting
function, and then rendered its value in our component via {props.headline}
.
Now when we render this component, we can pass in a value for the headline.
<Greeting headline="Welcome, it's great to see you again"/>
As you use React for real applications, these options for storing state can become a little unwieldy, with more and more state to pass around and/or store. This is where alternatives like Redux come into play, giving you a centralized data store for your entire application.
But it's worth noting Redux is not directly tied to React, and is not something you would necessarily use from day one when you start building React applications.
Broadly speaking, Blazor has the same two primary options for managing state.
You can store data in the component itself using properties (as with Name
in our example) or take data in via parameters.
<h2>@Headline</h2>
<label>What's your name?</label>
<input type="text" @bind-value="Name" @bind-value:event="oninput" placeholder="Bob"/>
<span>Hello @Name</span>
@code {
[Parameter]
public string Headline { get; set; }
public string Name { get; set; }
}
As with the React example, when you render Greeting
you can pass in a headline and it will be rendered accordingly.
<Greeting Headline="Welcome, it's still great to see you..."/>
React doesn't ship with anything to specifically help you with forms.
This means you need to either "roll your own" or use a third-party npm package for the job.
Here's the markup for a simple contact form.
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" value={name} onChange={handleNameChange} />
</label>
<label>
Thoughts?:
<input type="text" value={comments} onChange={handleCommentsChange} />
</label>
<input type="submit" value="Submit"/>
</form>
)
Note how we're setting the value of our inputs from state in our component (name
and comments
) and forwarding the onChange events to our own functions (handleNameChange
and handleCommentsChange
).
Here's what the JavaScript for this function looks like:
const [name, setName] = useState();
const [comments, setComments] = useState();
function handleNameChange(event){
setName(event.target.value);
}
function handleCommentsChange(event){
setComments(event.target.value);
}
function handleSubmit(event){
// submit data to your server
alert(`${name}: ${comments}`)
event.preventDefault();
}
So we are effectively intercepting the normal behavior for the form to both read and update our own component state.
For validation you can either roll your own, writing logic to check the values set in the form and alerting the user if they are invalid, or lean on third-party libraries (typically brought in via npm).
By contrast, Blazor has built-in functionality to handle your form data and validation using something called EditForm
.
Here's the markup for an equivalent form to our React example.
@using System.ComponentModel.DataAnnotations
<EditForm Model="FormModel" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<label for="name">
Name:
<InputText id="name" @bind-Value="FormModel.Name"/>
</label>
<label for="slug">
Thoughts?:
<InputText id="comments" @bind-Value="FormModel.Comments"/>
</label>
<input type="submit" value="Submit"/>
<ValidationSummary />
</EditForm>
In many ways this is similar to React, except we're using Blazor's InputText
component for our input fields.
Technically it's entirely possible to use standard HTML elements and forms with Blazor, but using the built-in controls make certain things a lot easier (validation being one, as we'll see in a moment).
@bind-Value
takes care of both reading the value from a field and updating it when a user types in a new value.
The form itself is based on a model of our choosing (where the form values will live) and we've told it which method to invoke when the form is submitted (and is valid).
Note how we've included a DataAnnotationsValidator
and ValidationSummary
; these components wire up the form to automatically respect any validation rules we set up on our model.
Here's the rest of the code:
@code {
protected ContactUsModel FormModel { get; set; } = new ContactUsModel();
async Task HandleValidSubmit()
{
// post to your API
Console.WriteLine($"{FormModel.Name}: {FormModel.Comments}");
}
protected class ContactUsModel
{
[Required]
public string Name { get; set; }
public string Comments { get; set; }
}
}
The ContactUsModel
class could live anywhere in our project.
We have a FormModel
property and HandleValidSubmit
method.
When someone fills in the form, if they've met the validation rules (a Name
has been entered) then HandleValidSubmit
will be invoked.
Otherwise, the ValidationSummary
will be used to show which fields have validation errors.
As with forms, React (deliberately) doesn't ship with any built-in routing engine, leaving you free to choose which of various third-party routing options you want to use.
Notable options include "Reach Router" and "React Router."
Blazor does include routing and leans on ASP.NET's existing routing engine. If you want to make a component "routable" you can add a directive:
@page "/GreetMe"
<h1>
Welcome!
</h1>
Now any request to http://<your-web-site-here>/GreetMe
will render this component.
You can also pass data in via the route, like this:
@page "/GreetMe/{Name}"
<h1>
Welcome @Name!
</h1>
@code {
[Parameter]
public string Name { get; set; }
}
Now any request to http://<your-web-site-here>/GreetMe/Jon
will render a more personalized greeting (well, if your name's Jon according to this example!)
React leaves data fetching to your discretion.
You can use the native fetch
API:
let response = await fetch(url);
if(response.ok){
let json = await response.json;
}
Or you can employ one of many third-party libraries, such as "Axios" which offers more features and flexibility.
Typically you'd fetch data using something like React's useEffect
hook, then update your component state with the results.
useEffect(()=> {
async loadData(){
let response = await fetch('api/Tickets');
setTickets(await response.json);
}
loadData();
}, []);
UseEffect hooks are intended to allow your component to handle "side effects", in this case, fetching data.
From here you'd typically use JavaScript's map function to loop over your data and render something for each item.
{tickets.map(ticket=> <div>
{ticket.Title}
</div>}
With Blazor, you can use HttpClient
for all your data-fetching needs!
Under the hood, this defers to the native fetch
API, but you can generally just ignore that and use the abstraction.
Here's an example:
@using System.Net.Http
@inject HttpClient Http
@foreach(var ticket in _tickets){
<div>
@ticket.Title
</div>
}
@code {
private Tickets[] _tickets;
protected override async Task OnInitializedAsync(){
_tickets = await Http.GetFromJsonAsync<TicketSummary>("api/Tickets");
}
}
Blazor has a useful trick up its sleeve (compared to React, or any other existing frontend framework) when it comes to the interaction between your UI and API.
Because you're writing your web app in C#, you can use the same data models in your frontend and backend (API) code.
Let's say for example you need to retrieve a list of people...
The Person
model lives in a shared class library.
Both your Web API and Blazor Client projects reference this shared library.
Now your API can be strongly typed, returning (and accepting) data using the Person
model.
The data is still serialized and sent "over the wire" as JSON data, but your Blazor application can deserialize the JSON data using the exact same Person
model that was used to serialize it in the first place.
Now if you make breaking changes to your Person
model, you'll immediately see compilation errors if you've introduced any breaking changes which affect either the client or server use of the model.
So all this brings us to the obvious question: which one should you choose?
Well, naturally, this will depend on your own background, skills and preferences.
We've seen how there are a number of similarities between the two, but also a few key differences.
React is generally unopinionated when it comes to the things that aren't its primary focus, like routing, form handling, etc.
This could count as either a pro or a con depending on your perspective.
Also, if you're writing React components, you have little choice but to use JavaScript, and adopt everything that comes along for the ride (the ecosystem, npm, bundling, etc.).
Create React App offers a handy abstraction for some of the thornier details (like webpack), so you can focus on building your application.
React Pros
React Cons
Blazor offers a few advantages, especially if you're coming from a C# background.
You can bring your existing C# skills, experience and knowledge to the modern web application party!
You can stick to the ecosystem you already know (NuGet, the dotnet
tooling, Visual Studio or VS Code).
Shared models between client and backend API. This is a big deal and makes it much harder to inadvertently break your application.
Routing, form handling and validation are baked in.
You can still call out to JavaScript if you need to.
Blazor Pros
Blazor Cons
One thing to call out here is the initial download time.
When someone accesses your Blazor Wasm app for the first time, their browser will download a version of the .NET framework as well as your application's files.
Once they have these files they don't need to download them again, but it does mean you're likely to see a "loading..." indicator first time round.
The team at Microsoft has done a lot of work to get this initial download size down, but naturally this means Blazor is better suited to some web applications than others.
You probably wouldn't want to use it for things like product landing pages where there's little to no business logic and it's imperative that the page loads as quickly as possible.
But, for any Line of Business apps this initial download is unlikely to pose a major issue.
So, are you thinking of using Blazor? Is it a contender or your next project, or will you be sticking with React for now?
The choice is yours!
Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.