Blazor vs React is a question every .NET developer eventually asks. Blazor lets you build interactive web UIs with C# instead of JavaScript. If you're already a .NET developer, this means one language, one ecosystem, and far fewer existential crises about which state management library to use this quarter.

Table of Contents


I've been writing JavaScript since 2009, back when jQuery 1.3 felt like mass. I've watched frameworks rise and fall like empires. Backbone, Angular, React, and now I'm supposed to care about Signals? I've mass-deleted more node_modules folders than I've had hot dinners.

And somewhere around my third node_modules folder that exceeded 800MB, I started asking myself: what if there was another way?

Spoiler: there is. It's called Blazor, and it let me build web applications using a language that doesn't think "2" + 2 = "22" is acceptable behavior.

What Is Blazor, Anyway?

Blazor is Microsoft's framework for building interactive web UIs using C# instead of JavaScript. The name is a portmanteau of "Browser" and "Razor" (the .NET templating syntax), and it's been production-ready since 2020.

Instead of writing JavaScript that runs in the browser, you write C# that either:

  1. Runs on the server (Blazor Server): Your C# executes on the server, and UI updates flow to the browser via a SignalR WebSocket connection in real-time.
  2. Runs in the browser (Blazor WebAssembly): The .NET runtime itself runs in the browser via WebAssembly. Yes, actual compiled .NET code, in your browser.
  3. Both (Blazor United/.NET 8+): Pick your rendering mode per-component. Server-side for initial load speed, WebAssembly for offline capability. Best of both worlds.

// A Blazor component. Yes, that's C# in your UI.
@page "/counter"

<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

No webpack. No babel. No seventeen configuration files. Just C# that compiles and runs.

The Case Against React (Or: Why I Needed a Change)

React revolutionized how we think about component-based UIs. The Virtual DOM was genuinely clever. I'm not here to trash it.

But after years of building production React applications, I started noticing patterns that made me question my life choices.

The Framework Churn is Real

The React ecosystem has been described as "an ever-shifting maze of faddish churn". That's not my phrase, but I felt it in my soul when I read it.

Every year brings a new "right way" to do things:

Year

What We Were Told to Use

2016

Redux. Obviously.

2018

MobX because Redux boilerplate was killing us

2020

Context API (who needs libraries?)

2021

Zustand, after Context caused re-render hell on a dashboard I built

2022

Pick one: Jotai, Recoil, Valtio, or whatever your coworker found on Hacker News

2023

Whatever survived the layoffs

2024

Server Components. State lives on the server now, apparently?

One developer on DEV Community summed it up perfectly: "After years of jumping from React to Vue to Svelte to Solid (and back again), I realized I was constantly relearning how to build the same thing in a slightly different way."

Decision Fatigue is Exhausting

Starting a new React project in 2024 meant I spent two days just picking tools. Bundler? Webpack is "legacy" now, Vite is the default, but do I need Turbopack? State management? I went with Zustand after three hours of reading comparison posts. Styling? Tailwind, but half my team wanted Styled Components. Forms? React Hook Form, probably, unless we need something Formik does better.

That was before I'd written a single line of business logic.

The State of React 2025 notes that "the flexibility and variety of ecosystem options has been both a strength and a weakness... That leads to decision fatigue, variations in project codebases, and constant changes in what tools are commonly used."

The Type Safety Theater

Yes, TypeScript exists. Yes, it helps. But TypeScript is a bandage on a dynamically-typed wound. You're bolting a type system onto a language that fundamentally doesn't have one. The seams show.

// TypeScript: Types are suggestions, really
const user: User = JSON.parse(response); // No runtime validation!
// user could be anything. TypeScript just trusts you.

// Meanwhile, in C#:
var user = JsonSerializer.Deserialize<User>(response);
// Actual deserialization with type checking

I ran grep -r ': any' src/ | wc -l on a client project last year. 847 instances. At that point, you're just writing JavaScript with extra steps.

The npm Security Nightmare

Let's talk about something the JavaScript community doesn't like to discuss: your node_modules folder is a security liability.

September 2025: The Shai-Hulud Attack

One of the largest npm supply chain attacks in history compromised 18+ packages with over 2.6 billion weekly downloads. The attack started with a phishing email to a single maintainer. Within days, packages from Zapier, PostHog, and Postman were trojanized.

The kicker? Shai-Hulud 2.0 followed in November 2025, affecting over 25,000 GitHub repositories. This version included a "scorched earth" fallback-if the malware couldn't exfiltrate credentials, it would destroy the victim's entire home directory.

This isn't ancient history. This is this year.

The Greatest Hits of npm Disasters

Incident

Year

Impact

left-pad

2016

One developer unpublished an 11-line package. Babel, React, and thousands of projects broke instantly.

event-stream

2018

Malicious code injected by a "helpful" new maintainer. Went undetected for 2.5 months. Targeted Bitcoin wallets.

ua-parser-js

2021

8 million weekly downloads. Compromised to install cryptominers and password stealers.

everything

2024

A "joke" package that depended on every npm package. DOS'd anyone who installed it.

Shai-Hulud

2025

2.6B+ weekly downloads affected. AI-assisted attacks. Data destruction payloads.

Why This Keeps Happening

The npm ecosystem has structural problems:

  1. Massive dependency trees: A typical React app has hundreds of transitive dependencies. Each one is an attack surface.
  2. Volunteer maintainers: Critical infrastructure maintained by unpaid individuals who can be phished, burned out, or social-engineered.
  3. No build-time verification: npm installs whatever package.json says. There's no compile-time check that the code is safe.
  4. Trivial packages: The JavaScript ecosystem has normalized depending on packages for trivial functionality. is-oddis-evenis-number? These exist, and they have millions of downloads.
# A real npm audit from a "simple" React project
found 47 vulnerabilities (12 moderate, 28 high, 7 critical)

Meanwhile, in .NET Land

NuGet isn't perfect, but it's dramatically better:

When I run dotnet list package --vulnerable on my Blazor projects, I typically see zero results. When I run npm audit on React projects, I clear my afternoon.

Let the Code Speak for Itself: React vs. Blazor

Theory is nice. Code is better. Here's what the same functionality looks like in both frameworks.

A Simple Counter (The "Hello World" of Frameworks)

React (with hooks):

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Counter</h1>
      <p>Current count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Blazor:

@page "/counter"

<h1>Counter</h1>
<p>Current count: @count</p>
<button @onclick="() => count++">Click me</button>

@code {
    private int count = 0;
}

Similar line count, but notice: no imports, no useState hook, no setter function. Just a variable and an increment.

Two-Way Data Binding

This is where the ceremony difference gets real.

React:

import { useState } from 'react';

export default function SearchBox() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  const handleChange = (e) => {
    setQuery(e.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      <p>You typed: {query}</p>
    </div>
  );
}

Blazor:

<input type="text" @bind="query" @bind:event="oninput" placeholder="Search..." />
<p>You typed: @query</p>

@code {
    private string query = "";
}

@bind handles both directions. No onChange handler, no e.target.value, no setter function. The binding just works.

Form Handling with Validation

Here's where things get spicy. A user registration form with validation.

React (with React Hook Form + Zod - the "modern" approach):

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword']
});

export default function RegisterForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm({
    resolver: zodResolver(schema)
  });

  const onSubmit = async (data) => {
    await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input {...register('email')} placeholder="Email" />
        {errors.email && <span>{errors.email.message}</span>}
      </div>
      <div>
        <input {...register('password')} type="password" placeholder="Password" />
        {errors.password && <span>{errors.password.message}</span>}
      </div>
      <div>
        <input {...register('confirmPassword')} type="password" placeholder="Confirm" />
        {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
      </div>
      <button type="submit" disabled={isSubmitting}>Register</button>
    </form>
  );
}

That's three npm packages (react-hook-form@hookform/resolverszod), spread operators on inputs, and resolver configuration. It works, but there's a lot happening.

Blazor (with built-in DataAnnotations):

@page "/register"
@inject HttpClient Http

<EditForm Model="model" OnValidSubmit="HandleSubmit">
    <DataAnnotationsValidator />

    <div>
        <InputText @bind-Value="model.Email" placeholder="Email" />
        <ValidationMessage For="() => model.Email" />
    </div>
    <div>
        <InputText @bind-Value="model.Password" type="password" placeholder="Password" />
        <ValidationMessage For="() => model.Password" />
    </div>
    <div>
        <InputText @bind-Value="model.ConfirmPassword" type="password" placeholder="Confirm" />
        <ValidationMessage For="() => model.ConfirmPassword" />
    </div>

    <button type="submit" disabled="@isSubmitting">Register</button>
</EditForm>

@code {
    private RegisterModel model = new();
    private bool isSubmitting = false;

    private async Task HandleSubmit()
    {
        isSubmitting = true;
        await Http.PostAsJsonAsync("/api/register", model);
        isSubmitting = false;
    }

    public class RegisterModel
    {
        [Required, EmailAddress]
        public string Email { get; set; } = "";

        [Required, MinLength(8, ErrorMessage = "Password must be at least 8 characters")]
        public string Password { get; set; } = "";

        [Required, Compare(nameof(Password), ErrorMessage = "Passwords don't match")]
        public string ConfirmPassword { get; set; } = "";
    }
}

Zero additional packages. The validation attributes are built into .NET. EditForm handles the submit flow. DataAnnotationsValidator wires up validation automatically. The model class is reusable on your API too.

Data Fetching

React (with useEffect - the footgun everyone steps on):

import { useState, useEffect } from 'react';

export default function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;  // Cleanup flag to prevent state updates after unmount

    const fetchUsers = async () => {
      try {
        const response = await fetch('/api/users');
        if (!response.ok) throw new Error('Failed to fetch');
        const data = await response.json();
        if (isMounted) {
          setUsers(data);
          setLoading(false);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
          setLoading(false);
        }
      }
    };

    fetchUsers();

    return () => { isMounted = false; };  // Cleanup
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Notice the cleanup pattern with isMounted. I spent two hours last year hunting down a memory leak because a junior dev forgot this pattern. The error message? "Can't perform a React state update on an unmounted component." Helpful.

Blazor:

@page "/users"
@inject HttpClient Http

@if (users == null)
{
    <div>Loading...</div>
}
else if (error != null)
{
    <div>Error: @error</div>
}
else
{
    <ul>
        @foreach (var user in users)
        {
            <li>@user.Name</li>
        }
    </ul>
}

@code {
    private List<User>? users;
    private string? error;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            users = await Http.GetFromJsonAsync<List<User>>("/api/users");
        }
        catch (Exception ex)
        {
            error = ex.Message;
        }
    }
}

No cleanup needed. Blazor handles component lifecycle properly. No dependency array to forget. OnInitializedAsync runs once when the component loads.

Component Communication (Parent to Child, Child to Parent)

React:

// Parent
function Parent() {
  const [selectedItem, setSelectedItem] = useState(null);

  return (
    <div>
      <ItemList
        onItemSelected={setSelectedItem}
        highlightedId={selectedItem?.id}
      />
      {selectedItem && <ItemDetail item={selectedItem} />}
    </div>
  );
}

// Child
function ItemList({ onItemSelected, highlightedId }) {
  const items = [...];

  return (
    <ul>
      {items.map(item => (
        <li
          key={item.id}
          className={item.id === highlightedId ? 'highlighted' : ''}
          onClick={() => onItemSelected(item)}
        >
          {item.name}
        </li>
      ))}
    </ul>
  );
}

Blazor:

@* Parent *@
<ItemList @bind-SelectedItem="selectedItem" />

@if (selectedItem != null)
{
    <ItemDetail Item="selectedItem" />
}

@code {
    private Item? selectedItem;
}
@* ItemList.razor *@
<ul>
    @foreach (var item in items)
    {
        <li class="@(item.Id == SelectedItem?.Id ? "highlighted" : "")"
            @onclick="() => SelectedItem = item">
            @item.Name
        </li>
    }
</ul>

@code {
    private List<Item> items = [...];

    [Parameter]
    public Item? SelectedItem { get; set; }

    [Parameter]
    public EventCallback<Item?> SelectedItemChanged { get; set; }
}

The @bind-SelectedItem syntax automatically wires up two-way binding between parent and child. Convention over configuration.

The Ceremony Comparison

Task

React

Blazor

Two-way binding

value + onChange (every. single. field.)

@bind

Form validation

Pick a library, write a schema, pray

Built-in DataAnnotationsValidator

Data fetching

useEffect + cleanup + deps array (good luck)

OnInitializedAsync

State management

This week's favorite: Redux? Zustand? Jotai?

Class fields. Call StateHasChanged() when needed

DI

Roll your own with Context or install a library

@inject (it just works)

Parent-child binding

Props down, callbacks up

@bind-PropertyName

See the pattern? Blazor does the same thing with less code and fewer choices to agonize over.

Why Blazor Actually Works for Me

One Language to Rule Them All

This is the killer feature. With Blazor, I write C# on the frontend and C# on the backend. Same language. Same type system. Same patterns.

// Shared model used by both frontend and backend
public record CreateOrderRequest(
    string CustomerId,
    List<OrderItem> Items,
    ShippingAddress Address
);

// API endpoint
[HttpPost]
public async Task<ActionResult<Order>> CreateOrder(CreateOrderRequest request)
{
    // Validate, process, return
}

// Blazor component calling the API
var order = await Http.PostAsJsonAsync<Order>("api/orders", request);

No serialization mismatches. No "I updated the API but forgot to update the TypeScript types." The compiler catches everything.

Type Safety That Actually Means Something

C# is statically typed. Not "we added types later and hope you use them" typed. The compiler is your friend, and it will tell you about problems before your users find them.

// This won't compile. Period.
int count = "five"; // Error CS0029

// Pattern matching catches nulls at compile time
if (user is { Email: var email })
{
    SendWelcomeEmail(email); // email is guaranteed non-null here
}

As Emergent Software notes, "combining these tools with the type safety of the language allows developers to write code with a lot more confidence up-front, not having to worry about runtime errors like unexpected nulls."

SOLID Principles Are First-Class Citizens

.NET was designed with dependency injection and interface-based programming in mind. Implementing SOLID principles isn't fighting the framework-it's using it as intended.

// Program.cs - Clean DI registration
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IPaymentService, StripePaymentService>();
builder.Services.AddScoped<OrderService>();

// Component receives dependencies automatically
@inject OrderService Orders
@inject NavigationManager Navigation

@code {
    private async Task SubmitOrder()
    {
        var result = await Orders.CreateAsync(currentOrder);
        if (result.IsSuccess)
        {
            Navigation.NavigateTo($"/orders/{result.Value.Id}");
        }
    }
}

Try achieving this level of clean dependency injection in React without a third-party library and a configuration ceremony.

Debugging That Doesn't Make You Cry

Visual Studio's debugger with Blazor is genuinely excellent. Breakpoints work. Step-through works. Watch expressions work. The call stack actually makes sense.

I can set a breakpoint in my Blazor component, step through my business logic, and trace execution all the way to the database and back. Same debugger, same tools, same experience as any other C# application.

private async Task LoadOrders()
{
    // Set a breakpoint here
    var orders = await OrderService.GetRecentAsync(customerId);

    // Inspect 'orders' in the debugger
    // See the actual types, not 'object Object'

    this.orders = orders;
}

Compare this to debugging React where you're juggling browser DevTools, React DevTools, and Redux DevTools all at once. Last year I spent 45 minutes on a bug that turned out to be a missing dependency in a useEffect array. The symptom was silent infinite re-renders. Blazor's debugger would have caught that in seconds.

The .NET Ecosystem is Stable (Boringly So)

The .NET ecosystem moves slowly. Deliberately. I consider this a feature, not a bug.

NuGet packages tend to have long-term support. When Newtonsoft.Json decided to stay on version 13, nobody panicked. It still works. Compare that to the npm ecosystem where "latest" is a lifestyle.

When I upgraded from .NET 7.0.14 to .NET 8.0.0 last November, I got a documented list of breaking changes. Took me an afternoon to migrate a medium-sized app. When React moved from class components to hooks, the community spent months rewriting tutorials and arguing on Twitter.

Real-World Advantages

Code Sharing is Trivial

Got validation logic? Write it once in a shared library.

// Shared.Validation/OrderValidator.cs
public class OrderValidator : AbstractValidator<Order>
{
    public OrderValidator()
    {
        RuleFor(x => x.Items).NotEmpty();
        RuleFor(x => x.Total).GreaterThan(0);
        RuleFor(x => x.ShippingAddress).NotNull();
    }
}

// Use in Blazor component
// Use in API controller
// Use in background service
// Same validation, everywhere

The Component Model is Familiar

If you know Razor syntax from MVC or Razor Pages, Blazor components will feel natural. The learning curve for .NET developers is measured in days, not months.

// Component with parameters - looks like any other C# class
@code {
    [Parameter]
    public Order Order { get; set; } = default!;

    [Parameter]
    public EventCallback<Order> OnOrderUpdated { get; set; }

    [CascadingParameter]
    public UserContext? User { get; set; }
}

Performance Has Improved Dramatically

Early Blazor WebAssembly had legitimate performance concerns. Those days are largely behind us-each .NET release has brought major improvements.

.NET 8 introduced:

.NET 9 added:

.NET 10 delivers:

So: Static SSR outperforms React on initial load because you're serving pre-rendered HTML with zero JavaScript hydration overhead. AOT compilation gets WebAssembly apps to near-native speed. You pick the right mode for each page.

Blazor's Rapid Evolution: .NET 8, 9, and 10

One thing that impressed me about Blazor is how aggressively Microsoft has been improving it. This isn't a side project-it's a first-class citizen in the .NET ecosystem.

.NET 8: The "Blazor United" Revolution

.NET 8 fundamentally changed how Blazor works with the introduction of unified rendering modes. Before .NET 8, you had to choose: Blazor Server or Blazor WebAssembly. Now you can mix and match per-component.

Static Server-Side Rendering (SSR) changed everything. Your components render as plain HTML on the server. No SignalR connection, no WebAssembly download. Just fast, SEO-friendly HTML.

// A static SSR page - renders as pure HTML
@page "/about"
@attribute [StreamRendering]

<PageTitle>About Us</PageTitle>

<article>
    <h1>About Our Company</h1>
    <p>This renders as static HTML. Search engines love it.</p>
</article>

The new Blazor Web App template defaults to SSR, with interactivity as an opt-in enhancement. This is a fundamental shift in philosophy-you start with the fastest, most SEO-friendly option and add interactivity only where needed.

.NET 9: Mixing Modes with Precision

.NET 9 doubled down on flexibility. The new [ExcludeFromInteractiveRouting] attribute lets you mark specific pages that must use static SSR-useful for pages that depend on HTTP cookies or the request/response cycle.

@page "/privacy-settings"
@attribute [ExcludeFromInteractiveRouting]

// This page always renders statically, even in an otherwise interactive app

Other .NET 9 improvements:

.NET 10: Performance and Polish

.NET 10 shipped with significant Blazor improvements:

Blazor script optimization: The Blazor script is now served as a static web asset with automatic compression and fingerprinting, significantly reducing payload size and improving caching.

Persistent component state: The new [PersistentState] attribute simplifies sharing data between pre-rendering and interactive renders:

@code {
    [PersistentState]
    public List<Movie>? Movies { get; set; }

    protected override async Task OnInitializedAsync()
    {
        // State is automatically restored from prerendering
        Movies ??= await MovieService.GetMoviesAsync();
    }
}

Better 404 handlingNavigationManager.NotFound() now works seamlessly across all render modes. New project templates include a default NotFound.razor page.

Improved diagnostics: Server circuits now expose traces as top-level activities, making observability in Application Insights much cleaner.

The trajectory is clear: Blazor is getting faster, more flexible, and more production-ready with every release.

When React Still Makes Sense

I'm not here to tell you React is bad. It's not. Pick React if your team already thinks in JavaScript and nobody wants to learn C#. Pick it if you need some niche npm component that doesn't exist in the Blazor ecosystem (there are still gaps). Pick it for a quick startup MVP where you'll probably throw away the code in six months anyway.

React's ecosystem is massive. If you need an obscure charting library or a drag-and-drop grid that does exactly one weird thing, someone's probably built it. That's genuinely valuable.

But if you're a .NET shop building internal tools, line-of-business apps, or anything that needs to run for more than three years? Blazor is worth a serious look.

Getting Started with Blazor

If you're curious, here's the quickest path to a running Blazor app:

# Create a new Blazor Web App (.NET 8+)
dotnet new blazor -o MyBlazorApp
cd MyBlazorApp
dotnet run

That's it. No npm install that spins for three minutes downloading half the internet. No node_modules folder larger than your actual code. No .babelrc or webpack.config.js to fight with.

The official Blazor documentation is comprehensive and actually well-written. Microsoft's docs have come a long way since the MSDN days.

FAQ: Common Concerns About Blazor

I've heard every objection in the Blazor vs React debate. Some are valid. Most aren't.

Is Blazor WebAssembly bundle size too large?

In 2020? Yeah, it was rough. First load could hit 2-3MB.

Now? .NET 8+ changed the math:

But honestly, the smarter answer is: don't ship WebAssembly to everyone. Use Static SSR for landing pages (zero WASM). Use Interactive Server for authenticated sections. Save WebAssembly for offline scenarios or heavy client-side computation. You pick per-page now.

Are JavaScript developers easier to hire than Blazor developers?

Depends what you're actually hiring for.

A senior C# developer picks up Blazor in a week. The component model clicks immediately, the syntax is just Razor, and they already know the language. Compare that to hiring a "React developer" who learned it three years ago and now needs to catch up on Server Components, the use hook, and whichever meta-framework your team picked.

The .NET talent pool is large, stable, and full-stack capable. Your backend developer can review frontend PRs without context-switching languages.

Does React have more third-party components than Blazor?

Yes. Full stop.

But quantity isn't quality. For every polished React component library, there are dozens of abandoned packages with security holes, incompatible peer dependencies, and READMEs that say "TODO: add documentation."

Blazor's ecosystem is smaller but you're not digging through npm hoping the package you found is still maintained.

MudBlazor is the one I reach for first. Material Design, free, and the maintainers actually respond to issues. Radzen and Syncfusion both offer free community licenses if you need more. Enterprise shops usually go Telerik for the support contracts.

And when you absolutely need a JavaScript library? IJSRuntime lets you interop cleanly:

@inject IJSRuntime JS

@code {
    private async Task InitializeChart()
    {
        await JS.InvokeVoidAsync("Chart.bindings.init", chartElement, chartData);
    }
}

You're not locked out of the JavaScript ecosystem. You just don't depend on it.

Is Blazor good for SEO?

Static SSR renders pure HTML. Google sees it, indexes it, done. Same as Next.js SSR but in C#. This objection made sense in 2020 when Blazor was WebAssembly-only. It hasn't been true for years.

Can Blazor be used for mobile apps?

Blazor Hybrid exists. Write your components once, run them in MAUI (iOS/Android/Windows/macOS), WPF, or even Windows Forms.

Same C# components, native app shell. Not quite "write once, run anywhere" but close enough for most use cases.

Is Blazor tooling as good as VS Code with React?

I'd argue it's better, but that's subjective.

Visual Studio with Blazor: actual debugging with breakpoints and step-through, IntelliSense that understands your whole codebase, refactoring that works across components and services, Hot Reload that mostly works, integrated testing and profiling.

VS Code with React: syntax highlighting, IntelliSense that works if TypeScript is configured right, hoping your launch.json is correct, browser extensions that may or may not conflict with each other.

The debugging alone sold me. Setting a breakpoint in a Blazor component and stepping through to the database, in one IDE, one language? That's just pleasant.

Does Blazor create Microsoft vendor lock-in?

.NET is open source (MIT license). Blazor is open source. You can run it on Linux, in Docker, on AWS, on Azure, wherever.

Is there ecosystem gravity toward Azure? Sure. But I've deployed Blazor apps to AWS, DigitalOcean, and bare metal servers without issue.

Compare this to React, which is technically open source but practically controlled by Meta's priorities. When Facebook decided class components were out, the entire ecosystem pivoted. When they pushed Server Components, everyone scrambled.

At least Microsoft publishes a roadmap.

Does Blazor Hot Reload work properly?

It was rough in .NET 5. By .NET 8 it was usable. .NET 10 finally made it good. Edit a .razor file, save, see changes. Method bodies, Razor markup, CSS, all instant.

Is it as fast as Vite? No. Is it fast enough that I don't think about it? Yes.

Final Thoughts

Switching to Blazor hasn't solved all my problems. I still write bugs. I still make architecture mistakes. I still occasionally spend too long debugging something that turns out to be a typo.

But I've stopped spending mental energy on which state management library is "correct" this month. I don't wonder whether my TypeScript types match runtime data anymore (spoiler: they often didn't). I'm not updating seventeen npm packages every time Dependabot yells at me.

One language. One ecosystem. Patterns I've refined over a decade of .NET work. That's the trade-off, and for me it's worth it.

Your mileage may vary. But if you're a .NET developer who's ever looked at your package.json with existential dread-Blazor might be worth an afternoon of your time.

What's Next?

If you want to try it: dotnet new blazor -o MyFirstBlazorApp && cd MyFirstBlazorApp && dotnet run. That's the whole setup.

Microsoft's Blazor tutorials are solid, and r/Blazor is one of the friendlier tech subreddits I've found.

Questions? Drop a comment or find me on social. I like talking about this stuff.

Sources:


About the Author

I'm a Systems Architect who is passionate about distributed systems, .NET clean code, logging, performance, and production debugging. I've mass-deleted node_modules folders more times than I'd like to admit.