_When my team said we were adding a React frontend to our .NET API, I did what any self-respecting senior developer does.
I Googled it. Watched a YouTube tutorial for 20 minutes. Closed the tab. Opened a new tab. Googled "React for C# developers." Got results that assumed I was either a complete beginner who didn't know what a variable was, or a JavaScript wizard who already knew what a closure was._
I was neither. I was a C# developer. I knew exactly what I was doing — just not in this language.
So I figured it out the hard way. And now I'm going to save you the weeks I wasted by mapping every React concept directly to what you already know in C#.
No "what is a variable" explanations. No JavaScript history lessons. Just: here's what you know, here's what it's called in React, here's why it works the same way.
First, the mindset shift
In C#, the server does the work. Your controller fetches data, builds a model, passes it to a Razor view, and the browser gets back finished HTML. The browser is basically a dumb terminal receiving rendered output.
React flips this completely. You ship JavaScript to the browser, and the browser builds the UI itself. Your .NET API becomes just a data supplier — it hands over JSON and React figures out the rest.
C#/.NET world: Browser → Server → Server renders HTML → Browser displays it
React world: Browser → Server → Server returns JSON → Browser renders it
That's the whole paradigm shift. Everything else is just syntax.
Variables: You already know this
This one's genuinely easy. JavaScript has three ways to declare variables and they map almost perfectly to what you already know.
| JavaScript | C# Equivalent | When to use |
|---|---|---|
const name = "John" |
readonly string name = "John" |
Value won't change — use this by default |
let count = 0 |
int count = 0 |
Value will change (loop counters, etc.) |
var name = "John" |
var name = "John" |
Old style — avoid in modern JS |
The mental model: use const for everything until you need to reassign it, then use let. Forget var exists. Seriously.
// JavaScript
const apiUrl = "https://api.myapp.com"; // won't change
let currentPage = 1; // will change as user navigates
// Compare to C#
readonly string apiUrl = "https://api.myapp.com";
int currentPage = 1;
Arrow Functions: You've already been writing these
Here's something that'll make you smile. You've been writing arrow functions in C# for years. You just called them lambdas.
// C# lambda
var double = (int x) => x * 2;
double(5); // returns 10
// JavaScript arrow function — almost identical!
const double = (x) => x * 2;
double(5); // returns 10
The syntax is nearly the same. The behavior is nearly the same. The only real difference is types — JavaScript doesn't have them (unless you use TypeScript, which is a conversation for another day).
Multi-line works the same way too:
// Single line — implicit return, no braces needed
const add = (a, b) => a + b;
// Multi-line — use braces, explicit return required
const getTotal = (transactions) => {
const sum = transactions.reduce((acc, t) => acc + t.amount, 0);
return sum;
};
Array Methods: LINQ with different names
This is where C# developers get an instant superpower in JavaScript. You already know how to think in LINQ. JavaScript's array methods are the same operations with different names.
| JavaScript | LINQ Equivalent | What it does |
|---|---|---|
.map(fn) |
.Select(fn) |
Transforms each item — returns new array |
.filter(fn) |
.Where(fn) |
Returns items matching a condition |
.reduce(fn, init) |
.Aggregate(init, fn) |
Reduces to a single value (totals, sums) |
.find(fn) |
.FirstOrDefault(fn) |
Returns first match or undefined |
.some(fn) |
.Any(fn) |
Returns true if any item matches |
.every(fn) |
.All(fn) |
Returns true if all items match |
👉 React for C#/.NET Developers — Complete Training Guide
// C# LINQ
var amounts = transactions.Select(t => t.Amount).ToList();
var deposits = transactions.Where(t => t.Type == "Deposit").ToList();
var total = transactions.Aggregate(0m, (sum, t) => sum + t.Amount);
// JavaScript — same logic, different syntax
const amounts = transactions.map(t => t.amount);
const deposits = transactions.filter(t => t.type === "Deposit");
const total = transactions.reduce((sum, t) => sum + t.amount, 0);
Why this matters for React: .map() is how you render lists. Instead of returning transformed values, you return JSX elements. This single pattern is responsible for roughly 40% of the React code you'll ever write.
// Rendering a list in React — it's just .map() returning JSX
return (
<ul>
{transactions.map(t => (
<li key={t.id}>{t.accountNumber}: ${t.amount}</li>
))}
</ul>
);
Components: Partial Views with superpowers
A React component is a JavaScript function that returns HTML-like syntax (called JSX). Think of it as a Razor Partial View that also contains its own controller logic.
// C# — ViewComponent (closest equivalent)
public class TransactionCardViewComponent : ViewComponent
{
public IViewComponentResult Invoke(string accountNumber, decimal amount, string type)
{
return View(new TransactionCardViewModel { ... });
}
}
// React — same idea, dramatically less ceremony
function TransactionCard({ accountNumber, amount, type }) {
const color = type === "Deposit" ? "green" : "red";
return (
<div>
<h3>{accountNumber}</h3>
<p style={{ color }}>₱{amount.toLocaleString()}</p>
</div>
);
}
// Use it like a custom HTML tag anywhere in your app
<TransactionCard accountNumber="ACC-001" amount={5000} type="Deposit" />
The JSX syntax looks weird at first — HTML inside JavaScript? — but you get used to it within a day. The key rules:
-
classbecomesclassName(becauseclassis a reserved word in JavaScript) -
forbecomeshtmlForon labels - Every tag must be closed —
<br />not<br> - Curly braces
{}embed JavaScript expressions, exactly like@()in Razor
Props: Method parameters, but for components
Props are how you pass data into a component. They work exactly like parameters to a C# method — the caller provides values, the component uses them, and they're read-only inside the component.
// C# method — you pass parameters
public string FormatTransaction(string accountNumber, decimal amount)
{
return $"{accountNumber}: ₱{amount:N0}";
}
// React component — you pass props (same idea)
function TransactionRow({ accountNumber, amount }) {
return <p>{accountNumber}: ₱{amount.toLocaleString()}</p>;
}
// Calling it — like calling a method, but in JSX
<TransactionRow accountNumber="ACC-001" amount={5000} />
The destructuring syntax { accountNumber, amount } in the function parameter is just JavaScript's way of pulling named properties out of an object. It's sugar for props.accountNumber, props.amount.
State: The concept that makes React click
This is where React gets genuinely different from anything in server-side C# — and once it clicks, everything makes sense.
State is data that belongs to a component and can change over time. When state changes, React automatically re-renders the component with the new value. No manual DOM manipulation, no page reload. The UI just updates.
import { useState } from "react";
function TransactionFilter() {
// const [currentValue, setterFunction] = useState(initialValue)
const [filter, setFilter] = useState("All");
return (
<div>
<button onClick={() => setFilter("All")}>All</button>
<button onClick={() => setFilter("Deposit")}>Deposits</button>
<button onClick={() => setFilter("Withdraw")}>Withdrawals</button>
<p>Currently showing: {filter}</p>
</div>
);
}
The closest C# mental model: think of useState like INotifyPropertyChanged — when the value changes, something automatically updates in response. Except React handles the "update the UI" part for you automatically.
The golden rule that trips up every C# developer at least once:
// WRONG — this does nothing to the UI
filter = "Deposit";
// CORRECT — this triggers a re-render
setFilter("Deposit");
React only knows to re-render the component when you use the setter function. Direct assignment is invisible to React. Burn this into your brain on day one.
useEffect: The Form_Load event you always wanted
useEffect is how you run code at specific moments in a component's life — most commonly to fetch data when the component first appears on screen.
import { useState, useEffect } from "react";
function TransactionList() {
const [transactions, setTransactions] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// This runs once when the component first renders
fetch("/api/transactions", {
headers: { Authorization: `Bearer ${localStorage.getItem("jwt_token")}` }
})
.then(r => r.json())
.then(data => {
setTransactions(data);
setIsLoading(false);
});
}, []); // ← the empty [] means "run once on mount"
if (isLoading) return <p>Loading...</p>;
return (
<ul>
{transactions.map(t => <li key={t.id}>{t.accountNumber}</li>)}
</ul>
);
}
The second argument to useEffect is the dependency array:
| Syntax | When it runs | C# equivalent |
|---|---|---|
useEffect(() => {}, []) |
Once on mount | Form_Load |
useEffect(() => {}, [id]) |
On mount + every time id changes |
INotifyPropertyChanged |
useEffect(() => {}) |
After every render | No direct equivalent |
Context: Dependency Injection without the ceremony
If you've ever needed to pass a value (like a JWT token or the current user) through five levels of components, you've hit "prop drilling" — passing props down through components that don't even need them just to get them to a component that does.
Context solves this exactly the way DI solves it in C# — register it once, consume it anywhere.
// 1. Create the context (like registering in DI container)
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [token, setToken] = useState(localStorage.getItem("jwt_token"));
return (
<AuthContext.Provider value={{ token, setToken }}>
{children}
</AuthContext.Provider>
);
}
// 2. Wrap your app — once
<AuthProvider>
<App />
</AuthProvider>
// 3. Consume anywhere — no prop drilling needed
function NavBar() {
const { token } = useContext(AuthContext);
return <p>Logged in: {token ? "Yes" : "No"}</p>;
}
// C# equivalent mindset
// 1. Register in DI: services.AddScoped<IAuthService, AuthService>();
// 2. Inject anywhere: public NavController(IAuthService auth) { }
Same problem, same solution, different syntax.
The one thing that takes the longest to unlearn
Coming from C#, your instinct is to think about when things happen. You're used to sequential execution — line 1 runs, then line 2, then line 3.
React is declarative. You describe what the UI should look like given the current state, and React figures out when and how to make it happen. You stop thinking "do this when the user clicks" and start thinking "the UI looks like X when the filter is Y."
This is the shift that takes a few days to internalize. Be patient with yourself on this one. It's not a syntax problem — it's a mindset problem. And once it clicks, you'll wonder how you ever thought the other way.
Where to go from here
If this mapped well to how you think, I put together a complete 8-hour React training guide built specifically for C#/.NET developers — every concept extended further than this article, with hands-on exercises, full solutions, and a complete SecureBank dashboard build using JWT auth, AG Grid, and Recharts.
👉 React for C#/.NET Developers — Complete Training Guide
It's the guide I wish existed when I started. Hope it saves you the weeks it took me to figure this out.
Got questions? Drop them in the comments — especially if something here didn't map the way you expected. Those are usually the most interesting discussions.
Top comments (0)