Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

React Fundamentals for .NET Engineers

For .NET engineers who know: Blazor components, Razor syntax, and component-based UI patterns You’ll learn: How React’s component model, JSX, and one-way data flow map to what you already know from Blazor, and where the mental model diverges Time: 18 min read

The .NET Way (What You Already Know)

Blazor gives you a component model that should feel familiar: each component is a .razor file combining a template (HTML with Razor syntax) and logic (C# code block). Components receive data via [Parameter] attributes, communicate up via EventCallback, and React to state changes automatically through the framework’s rendering cycle. The file structure is the component — one file, one component.

// Counter.razor — a complete Blazor component
@page "/counter"

<h1>Count: @currentCount</h1>
<button @onclick="Increment">Click me</button>

@code {
    [Parameter]
    public int InitialCount { get; set; } = 0;

    private int currentCount;

    protected override void OnInitialized()
    {
        currentCount = InitialCount;
    }

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

You know this pattern cold. The React equivalent is structurally identical in intent and noticeably different in mechanics. The mapping is close enough that you can transfer your architectural thinking directly; the syntax and some behavioral details are where you need to pay attention.

The React Way

JSX: HTML-in-TypeScript

React components return JSX — a syntax extension that lets you write what looks like HTML directly inside a TypeScript function. This is not a template language parsed separately (like Razor); it is syntactic sugar that the TypeScript compiler (via Babel or esbuild) transforms into plain function calls.

// What you write
const element = <h1 className="title">Hello, world</h1>;

// What the compiler produces
const element = React.createElement("h1", { className: "title" }, "Hello, world");

That transformation is the entire magic of JSX. Once you internalize that JSX is just function calls that return objects describing UI, most of React’s behavior becomes obvious.

Key JSX rules that differ from HTML and Razor:

  • class is className (because class is a reserved word in JavaScript)
  • for on labels is htmlFor
  • All tags must be closed: <br />, not <br>
  • Self-closing tags need the slash: <input />, not <input>
  • Curly braces {} are the escape hatch to TypeScript — equivalent to @ in Razor
  • Style is an object, not a string: style={{ color: 'red', fontSize: 16 }}
  • JSX expressions must return a single root element (or use <>...</> fragment syntax)
// Razor equivalent
// <p>@user.Name is @user.Age years old</p>

// JSX equivalent
const greeting = (
  <p>{user.name} is {user.age} years old</p>
);

Functional Components: The Only Kind You Need

React has two ways to define components: class components (the original model) and functional components (the current model). Class components are legacy. Every new component you write will be a function that accepts a props object and returns JSX.

// The complete anatomy of a functional component

interface GreetingProps {
  name: string;
  role?: string;
}

function Greeting({ name, role = "engineer" }: GreetingProps): JSX.Element {
  return (
    <div>
      <h2>Hello, {name}</h2>
      <p>Role: {role}</p>
    </div>
  );
}

export default Greeting;

The function name is the component name. Capitalization is not optional — React uses it to distinguish HTML elements (lowercase) from components (PascalCase). <div> is an HTML element; <Greeting> is a component call.

Props: Component Parameters

Props are the equivalent of Blazor’s [Parameter] attributes. They flow one direction — parent to child — and the child must not mutate them. This is the “one-way data flow” React is known for, and it is the biggest conceptual shift from two-way binding.

// Blazor: parameters flow in, EventCallback flows up
// UserCard.razor
[Parameter] public string Name { get; set; }
[Parameter] public int Age { get; set; }
[Parameter] public EventCallback<string> OnSelect { get; set; }
// React: props flow in, callback functions flow up
interface UserCardProps {
  name: string;
  age: number;
  onSelect: (name: string) => void;
}

function UserCard({ name, age, onSelect }: UserCardProps) {
  return (
    <div onClick={() => onSelect(name)}>
      <strong>{name}</strong>, age {age}
    </div>
  );
}

The structural pattern is identical: data in, events out. The difference is that React’s “events out” mechanism is just a callback prop — a plain function. There is no EventCallback<T> wrapper or special invocation syntax.

State and Re-rendering

React re-renders a component whenever its state changes. State in functional components is managed via hooks — useState being the fundamental one. When you call the state setter, React schedules a re-render of that component and all its descendants.

import { useState } from "react";

function Counter({ initialCount = 0 }: { initialCount?: number }) {
  const [count, setCount] = useState(initialCount);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

useState returns a tuple: the current value and a setter function. The setter triggers the re-render. You never mutate the value directly — count++ will not trigger a re-render.

This is the INotifyPropertyChanged pattern, but the notification mechanism is the setter function rather than a property change event. The effect is the same: update state, UI updates.

The Component Lifecycle

Blazor has explicit lifecycle methods: OnInitialized, OnParametersSet, OnAfterRender, Dispose. React’s functional component model consolidates these into two mechanisms: rendering (the function itself) and effects (useEffect, covered in Article 3.2).

The render phase is simple: React calls your component function, you return JSX, React reconciles that JSX with the current DOM. Your component function must be a pure function of its props and state — given the same inputs, it must return the same output. Side effects (API calls, subscriptions, timers) do not belong in the render body.

// This is the "render" phase — pure, no side effects
function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);

  // Side effects go in useEffect (Article 3.2)
  // The render body just describes what to show

  if (user === null) {
    return <p>Loading...</p>;
  }

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

The rough lifecycle equivalence:

BlazorReact (functional)
Constructor / OnInitializeduseEffect(() => { ... }, []) — runs once after first render
OnParametersSetuseEffect(() => { ... }, [prop]) — runs when prop changes
OnAfterRenderuseEffect(() => { ... }) — runs after every render
IDisposable.DisposeReturn value of useEffect callback (cleanup function)
StateHasChanged()setState(...) setter call

Conditional Rendering

Razor uses @if and @switch blocks. JSX uses TypeScript expressions, which means you use ternary operators and logical && for inline conditionals.

// Razor
@if (isLoading) {
    <Spinner />
} else {
    <Content model="@model" />
}
// React — ternary for if/else
{isLoading ? <Spinner /> : <Content model={model} />}

// React — && for "render only if true"
{isAuthenticated && <AdminPanel />}

// React — for complex conditions, extract to a variable
const content = (() => {
  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage message={error} />;
  return <Content model={model} />;
})();

return <div>{content}</div>;

The && shorthand has a gotcha: if the left side evaluates to 0, React renders 0 (not nothing). Use !!value && or a ternary when the value could be zero.

Rendering Lists

In Razor you use @foreach. In React you map over arrays and return JSX. The key prop is mandatory and must be unique and stable.

// Razor
<ul>
    @foreach (var item in items)
    {
        <li>@item.Name</li>
    }
</ul>
// React
<ul>
  {items.map((item) => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>

The key prop is not accessible inside the component (you cannot read props.key). Its only purpose is to let React’s reconciler track which list items have moved, been added, or been removed between renders. Using array index as a key (key={index}) is acceptable only for static, non-reorderable lists — for anything that can change, use a stable ID.

Event Handling: Synthetic Events

React wraps native DOM events in a synthetic event system. The API mirrors the DOM Event interface, so .preventDefault(), .stopPropagation(), and .target all work as expected. The difference from native DOM events is that React pools event objects for performance — in practice this matters only if you access the event asynchronously after the handler returns.

function SearchForm() {
  const [query, setQuery] = useState("");

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();          // Same as you'd expect
    console.log("Searching for:", query);
  }

  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setQuery(event.target.value);    // Controlled input pattern
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

Event handlers in JSX are camelCase: onClick, onChange, onSubmit, onKeyDown. They accept a function reference, not a string — never onClick="handleClick()".

Component Composition

React has no concept of content projection sections the way Blazor does with @ChildContent and @Body. The equivalent mechanism is the children prop.

// Blazor — content projection
// Card.razor
<div class="card">
    @ChildContent
</div>

@code {
    [Parameter] public RenderFragment ChildContent { get; set; }
}
// React — children prop
interface CardProps {
  children: React.ReactNode;
  title?: string;
}

function Card({ children, title }: CardProps) {
  return (
    <div className="card">
      {title && <h3 className="card-title">{title}</h3>}
      <div className="card-body">{children}</div>
    </div>
  );
}

// Usage
<Card title="User Details">
  <p>Name: Chris</p>
  <p>Role: Engineer</p>
</Card>

For more complex slot patterns (equivalent to multiple named @ChildContent sections), pass JSX as named props:

interface LayoutProps {
  sidebar: React.ReactNode;
  main: React.ReactNode;
}

function Layout({ sidebar, main }: LayoutProps) {
  return (
    <div className="layout">
      <aside>{sidebar}</aside>
      <main>{main}</main>
    </div>
  );
}

// Usage
<Layout
  sidebar={<NavigationMenu />}
  main={<ContentArea />}
/>

A Complete Component: Annotated for .NET Engineers

Here is a realistic component combining all the above concepts. Read the comments as the bridge between what you know and what you are learning.

// UserList.tsx
// Equivalent to a Blazor component with [Parameter], foreach, and EventCallback

import { useState } from "react";

// TypeScript interface — equivalent to a C# record or DTO
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// Props interface — equivalent to [Parameter] declarations in Blazor @code block
interface UserListProps {
  users: User[];
  title?: string;
  onUserSelect: (user: User) => void;   // Equivalent to EventCallback<User>
}

// The component: a function, not a class
// Return type JSX.Element is optional (inferred), but explicit is cleaner
function UserList({ users, title = "Users", onUserSelect }: UserListProps): JSX.Element {
  // useState — equivalent to a private field that triggers StateHasChanged when set
  const [filter, setFilter] = useState<"all" | "active" | "inactive">("all");
  const [selectedId, setSelectedId] = useState<number | null>(null);

  // Derived data — computed from state, no extra hooks needed
  // Equivalent to a computed property in your Blazor component
  const visibleUsers = users.filter((u) => {
    if (filter === "active") return u.isActive;
    if (filter === "inactive") return !u.isActive;
    return true;
  });

  // Event handler — a plain function, not an EventCallback invocation
  function handleSelect(user: User): void {
    setSelectedId(user.id);
    onUserSelect(user);               // "Invoke" the callback — equivalent to EventCallback.InvokeAsync
  }

  // The render output — equivalent to the Razor markup portion of a .razor file
  // Note: this is the return of the function, not a separate template file
  return (
    <div className="user-list">
      <h2>{title}</h2>

      {/* Filter controls — JSX comments use this syntax */}
      <div className="filters">
        {/* onClick receives a function, not a string */}
        <button
          onClick={() => setFilter("all")}
          className={filter === "all" ? "active" : ""}   // className, not class
        >
          All ({users.length})
        </button>
        <button
          onClick={() => setFilter("active")}
          className={filter === "active" ? "active" : ""}
        >
          Active
        </button>
        <button
          onClick={() => setFilter("inactive")}
          className={filter === "inactive" ? "active" : ""}
        >
          Inactive
        </button>
      </div>

      {/* Conditional rendering — ternary, not @if */}
      {visibleUsers.length === 0 ? (
        <p className="empty-state">No users match the current filter.</p>
      ) : (
        // List rendering — .map(), not @foreach
        // key prop is mandatory and must be stable
        <ul className="user-items">
          {visibleUsers.map((user) => (
            <li
              key={user.id}    // Stable unique ID — not the array index
              className={`user-item ${selectedId === user.id ? "selected" : ""}`}
              onClick={() => handleSelect(user)}
            >
              <span className="user-name">{user.name}</span>
              <span className="user-email">{user.email}</span>
              {/* && short-circuit — renders only when true */}
              {user.isActive && (
                <span className="badge active">Active</span>
              )}
            </li>
          ))}
        </ul>
      )}

      <p className="summary">
        Showing {visibleUsers.length} of {users.length} users
      </p>
    </div>
  );
}

export default UserList;
// App.tsx — consuming the component
// Equivalent to placing <UserList> in a parent .razor file

import { useState } from "react";
import UserList from "./UserList";

const SAMPLE_USERS = [
  { id: 1, name: "Alice Chen", email: "alice@example.com", isActive: true },
  { id: 2, name: "Bob Perez", email: "bob@example.com", isActive: false },
  { id: 3, name: "Carol Smith", email: "carol@example.com", isActive: true },
];

function App() {
  const [selectedUser, setSelectedUser] = useState<string | null>(null);

  return (
    <div>
      <UserList
        users={SAMPLE_USERS}
        title="Engineering Team"
        onUserSelect={(user) => setSelectedUser(user.name)}
      />
      {selectedUser && <p>Selected: {selectedUser}</p>}
    </div>
  );
}

export default App;

Key Differences

ConceptBlazor (.NET)React (TypeScript)
Template languageRazor syntax (.razor files, @ prefix)JSX (.tsx files, {} escape)
Component definitionClass or partial class with markupPlain TypeScript function
Component parameters[Parameter] attribute on propertiesProps object (destructured function argument)
HTML attribute for CSS classclassclassName
Event callbacksEventCallback<T>, InvokeAsync()Plain function: (value: T) => void
State that triggers re-renderprivate field + StateHasChanged()useState hook — setter triggers re-render
Two-way binding@bind directiveControlled input: value + onChange
Child content projectionRenderFragment ChildContentchildren: React.ReactNode prop
Named slotsMultiple RenderFragment parametersJSX passed as named props
Iteration@foreach.map() returning JSX with key prop
Conditionals@if, @switchTernary ? :, logical &&, extracted variables
Code-behind separation.razor + .razor.csSingle .tsx file (logic and markup together)
Component lifecycleOnInitialized, OnAfterRender, DisposeuseEffect (Article 3.2)
Global state/servicesDI container, injected servicesContext API, state management libraries

Gotchas for .NET Engineers

Gotcha 1: JSX is not HTML, and attribute casing matters.

Coming from Razor, it is natural to write class="..." and not get immediate feedback that it is wrong — the code may still compile. React will print a console warning and apply no class whatsoever. The corrected form is className. Similarly, tabindex is tabIndex, readonly is readOnly, maxlength is maxLength. The pattern is: DOM attributes that are multi-word become camelCase in JSX. Memorize className and htmlFor; the rest you can look up.

Gotcha 2: Mutation does not trigger re-renders — ever.

In C#, you might do user.Name = "Updated" and call StateHasChanged(). In React, modifying a state variable’s internal properties does nothing to trigger a re-render. React compares state values by reference for objects and arrays. If you mutate in place, the reference does not change, and React sees no update.

// WRONG — mutates in place, React does not see a change
const [user, setUser] = useState({ name: "Alice", age: 30 });

function updateName() {
  user.name = "Bob";  // Mutates the object — no re-render
  setUser(user);      // Same reference — React bails out
}

// CORRECT — create a new object
function updateName() {
  setUser({ ...user, name: "Bob" });  // New object reference — triggers re-render
}

// WRONG — mutating an array
const [items, setItems] = useState([1, 2, 3]);

function addItem() {
  items.push(4);      // Mutates the array — no re-render
  setItems(items);    // Same reference — React bails out
}

// CORRECT — create a new array
function addItem() {
  setItems([...items, 4]);   // New array reference — triggers re-render
}

This is the single most common source of “my state changed but the UI didn’t update” bugs for .NET engineers learning React.

Gotcha 3: && with falsy values renders 0.

The idiom {count && <Thing />} is common in React code. It works when count is a boolean. When count is a number and its value is 0, the expression short-circuits and the value of the expression is 0 — which React renders as the text “0” in the DOM.

// WRONG — renders "0" when items.length is 0
{items.length && <ItemList items={items} />}

// CORRECT — use a boolean explicitly
{items.length > 0 && <ItemList items={items} />}

// ALSO CORRECT — ternary avoids the issue entirely
{items.length > 0 ? <ItemList items={items} /> : null}

Gotcha 4: The component function re-runs on every render — functions are not expensive.

.NET engineers sometimes avoid defining functions inside render because it looks like they are creating new delegate instances on every call. In React, this is normal and expected — the component function body runs on every render, including any nested function definitions. In practice this is not a performance problem; JavaScript function creation is cheap. You optimize only when profiling shows a real bottleneck, typically using useCallback (covered in Article 3.2).

Gotcha 5: Class components exist in the codebase — do not write them, but you need to read them.

Any React codebase older than 2019 likely has class components. You will encounter extends React.Component, render() methods, this.state, this.setState, and lifecycle methods like componentDidMount. These are not wrong, but they are the old model. Do not write new class components. When refactoring, convert to functional components unless the scope is too large to justify it.

// Legacy class component — READ, do not WRITE
class OldCounter extends React.Component<{}, { count: number }> {
  state = { count: 0 };

  componentDidMount() {
    console.log("Mounted — equivalent to OnInitialized");
  }

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Count: {this.state.count}
      </button>
    );
  }
}

// Modern functional equivalent
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>Count: {count}</button>
  );
}

Gotcha 6: Props are read-only — TypeScript may not catch every violation.

React’s contract is that you do not mutate props. TypeScript can enforce this if you mark props readonly, but in practice most codebases do not do this. React will not throw an error if you mutate a prop object’s nested properties, but the behavior is undefined and will produce subtle bugs. Treat props as immutable values. If you need to derive a modified version, copy it.

Hands-On Exercise

Build a filterable data table component from scratch. The component should:

  1. Accept a users prop of type User[], where User has id, name, department, startDate, and isActive.
  2. Render the list as an HTML table with column headers.
  3. Include a text input that filters by name (case-insensitive, as the user types).
  4. Include buttons to filter by department (collect unique departments from the data).
  5. Include a checkbox to show only active users.
  6. Show a count: “Showing X of Y users”.
  7. Emit a onSelect callback when a row is clicked.

Requirements:

  • Use functional components and TypeScript interfaces throughout.
  • All derived data (filtering, counting) computed inline from state — no useEffect for this exercise.
  • No external libraries — this is a JSX and props exercise.
  • Wire it into a parent App component that provides sample data and displays the selected user’s name.

This exercise forces you to handle: controlled inputs, list rendering with keys, conditional rendering, event handlers, and component composition — all in one context.

Quick Reference

JSX Syntax

HTML / RazorJSX
class="..."className="..."
for="..."htmlFor="..."
<br><br />
<input><input />
style="color: red"style={{ color: 'red' }}
<!-- comment -->{/* comment */}
@value{value}
@if (x) { ... }{x ? ... : null} or {x && ...}
@foreach (var x in list){list.map(x => <li key={x.id}>...</li>)}

Component Anatomy

// Import hooks and types at the top
import { useState } from "react";

// Define props interface before the component
interface MyComponentProps {
  requiredProp: string;
  optionalProp?: number;              // ? = optional, like C# optional parameter
  onEvent: (value: string) => void;  // Callback prop — equivalent to EventCallback<string>
}

// Component is a named function, exported at the bottom
function MyComponent({ requiredProp, optionalProp = 0, onEvent }: MyComponentProps) {
  const [localState, setLocalState] = useState<string>("");

  return (
    <div>...</div>
  );
}

export default MyComponent;

.NET to React Concept Map

.NET / BlazorReact
[Parameter] attributeProp in the props interface
EventCallback<T>(value: T) => void function prop
StateHasChanged()State setter from useState
@ChildContent (RenderFragment)children: React.ReactNode
Named RenderFragment slotsJSX passed as named props
@foreacharray.map() with key prop
@if / @switchTernary ? : / && / extracted variable
OnInitialized / DisposeuseEffect (Article 3.2)
@bind (two-way)value + onChange (controlled input)
private field + StateHasChanged()useState — value + setter
Partial class code-behindSame .tsx file — logic above the return

Common React TypeScript Types

Use CaseTypeScript Type
Child elementsReact.ReactNode
Click handlerReact.MouseEvent<HTMLButtonElement>
Input change handlerReact.ChangeEvent<HTMLInputElement>
Form submit handlerReact.FormEvent<HTMLFormElement>
Any JSX elementJSX.Element or React.ReactElement
Ref to DOM elementReact.RefObject<HTMLDivElement>
Style objectReact.CSSProperties

Further Reading