This is Part 2 of the React for C#/.NET Developers
series. Read Part 1 here
If you read my last article about React concepts mapped to C#, you probably nodded along to most of it.
Arrow functions? Same as lambdas. Got it. .map() is just .Select()? Makes sense. useState is like INotifyPropertyChanged? Okay, I can work with that.
And then you hit useEffect. And everything fell apart.
Don't worry — it's not you. useEffect is the one React concept that genuinely has no clean C# equivalent. It trips up every experienced developer coming from a server-side background, and the reason is simple:
It requires you to think about code execution in a completely different way.
This article is going to fix that. For good.
First, let's understand WHY it's confusing
In C#, you control exactly when code runs. You call a method, it executes, it returns. You know the order. You trust the sequence.
// C# — you are in control
public void LoadData()
{
var data = _repository.GetTransactions(); // runs now
DisplayData(data); // runs after
UpdateStatus("Done"); // runs last
}
React doesn't work like that. React's job is to render UI — and it will re-render your component whenever it feels like it needs to. State changes, parent re-renders, context updates — React decides when your component runs, not you.
So the question becomes: how do you run code at a specific moment if you're not in control of when your component renders?
The answer is useEffect.
What useEffect actually is
Forget the word "effect" for a moment. It's a terrible name that means nothing to a C# developer.
Think of useEffect as a "run this code when something specific happens" hook. You tell React two things:
What code to run
When to run it
That's it. That's useEffect.
useEffect(() => {
// 1. What to run
console.log("This ran!");
}, []); // 2. When — [] means once, on mount
The dependency array — the part everyone gets wrong
The second argument to useEffect — the [] — is called the dependency array. This is where most C# developers get confused because there's nothing like it in C#.
Here's the simplest way to understand it:
Dependency array
When useEffect runs
C# equivalent
[] empty
Once — when component first appears
Form_Load / OnInitialized
[value]
On mount + every time value changes
INotifyPropertyChanged
nothing (no [])
After every single render
No direct equivalent
Pattern 1: Run once on mount — fetch your data
This is what you'll write 80% of the time. Fetch data from your API when the component first loads — exactly like Form_Load in WinForms or OnInitialized in Blazor.
useEffect(() => {
// Runs ONCE when component first appears
fetch("/api/transactions", {
headers: {
Authorization: `Bearer ${localStorage.getItem("jwt_token")}`
}
})
.then(response => response.json())
.then(data => {
setTransactions(data);
setIsLoading(false);
});
}, []); // ← empty array = run once
C# mental model:
// Closest C# equivalent
protected override async Task OnInitializedAsync()
{
transactions = await _api.GetTransactionsAsync();
isLoading = false;
}
Pattern 2: Run when a value changes — reactive fetching
Re-fetch data whenever a filter or ID changes — without writing a bunch of event handlers.
function TransactionDetail({ accountId }) {
const [account, setAccount] = useState(null);
useEffect(() => {
// Runs on mount AND every time accountId changes
fetch(`/api/accounts/${accountId}`)
.then(r => r.json())
.then(setAccount);
}, [accountId]); // ← re-run when accountId changes
if (!account) return <p>Loading...</p>;
return <h2>{account.accountNumber}</h2>;
}
Pattern 3: The cleanup function — often forgotten, always important
useEffect can return a function. That function runs when the component is removed from the screen. It's your cleanup code.
useEffect(() => {
// Setup
const timer = setInterval(() => {
console.log("Checking for new transactions...");
}, 5000);
// Cleanup — runs when component unmounts
return () => {
clearInterval(timer);
};
}, []);
C# equivalent — think IDisposable:
public void Dispose()
{
_timer?.Stop();
_timer?.Dispose();
}
The mistake every C# developer makes at least once
WARNING
This useEffect looks perfectly reasonable and will cause you hours of debugging pain.
// WRONG — infinite loop!
useEffect(() => {
fetch("/api/data")
.then(r => r.json())
.then(result => setData(result)); // updates state
}, [data]); // ← data is the dependency
// What happens:
// 1. Renders → useEffect runs → fetches data
// 2. setData updates state → triggers re-render
// 3. Re-render → useEffect runs again (data changed!)
// 4. Fetch again → setData → re-render → infinite loop 🔥
The fix:
// CORRECT — run once, don't watch data
useEffect(() => {
fetch("/api/data")
.then(r => r.json())
.then(result => setData(result));
}, []); // ← empty array, runs once
GOLDEN RULE
Never put state in the dependency array if that same useEffect updates that state. You will create an infinite loop.
Putting it all together — a real component
Here's a complete TransactionDashboard component that uses every pattern we covered — plus concepts from the first article:
function TransactionDashboard({ accountId }) {
const [transactions, setTransactions] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState("All");
// Fetch when accountId changes + cleanup
useEffect(() => {
setIsLoading(true);
const controller = new AbortController();
fetch(`/api/accounts/${accountId}/transactions`, {
headers: { Authorization: `Bearer ${localStorage.getItem("jwt_token")}` },
signal: controller.signal
})
.then(r => r.json())
.then(data => { setTransactions(data); setIsLoading(false); })
.catch(err => { if (err.name !== "AbortError") setIsLoading(false); });
return () => controller.abort(); // cleanup
}, [accountId]);
// useMemo — from Article 1
const totals = useMemo(() => ({
deposits: transactions.filter(t => t.type === "Deposit")
.reduce((sum, t) => sum + t.amount, 0),
withdrawals: transactions.filter(t => t.type === "Withdraw")
.reduce((sum, t) => sum + t.amount, 0),
}), [transactions]);
// .filter() — from Article 1
const displayed = filter === "All"
? transactions
: transactions.filter(t => t.type === filter);
if (isLoading) return <p>Loading...</p>;
return (
<div>
<p>Deposits: ₱{totals.deposits.toLocaleString()}</p>
<p>Withdrawals: ₱{totals.withdrawals.toLocaleString()}</p>
<button onClick={() => setFilter("All")}>All</button>
<button onClick={() => setFilter("Deposit")}>Deposits</button>
<button onClick={() => setFilter("Withdraw")}>Withdrawals</button>
<ul>
{displayed.map(t => (
<li key={t.id}>{t.accountNumber} — {t.type} — ₱{t.amount.toLocaleString()}</li>
))}
</ul>
</div>
);
}
The mental model that finally makes it click
Stop thinking about when code runs and start thinking about what your component depends on.
C# developer thinks
React developer thinks
I need to call this method when X happens
My component needs Y data when Z changes
I control the execution order
I declare dependencies, React handles timing
Methods run when I call them
Effects run when their dependencies change
Once you internalize that shift, useEffect stops being confusing and starts feeling like the cleanest reactive system you've ever worked with.
Quick reference — useEffect cheat sheet
// 1. Run once on mount (most common — data fetching)
useEffect(() => { fetchData(); }, []);
// 2. Run when value changes (reactive)
useEffect(() => { fetchAccountData(accountId); }, [accountId]);
// 3. Run with cleanup (timers, subscriptions)
useEffect(() => {
const timer = setInterval(checkForUpdates, 5000);
return () => clearInterval(timer);
}, []);
// 4. NEVER do this unless you know exactly why
useEffect(() => { /* runs after every render */ });
What's next
In the next article we'll cover Context and the JWT authentication flow — how to store your token once after login and make it available to every component in your app without prop drilling.
If you missed Article 1 where we covered arrow functions, .map(), useState and components — go back and read that first, then come back here.
Want to go deeper?
I put together a complete 8-hour React training guide built specifically for C#/.NET developers — JWT auth, AG Grid, Recharts, and a full SecureBank dashboard build with exercises and solutions included.
React for C#/.NET Developers — Complete Training Guide
What's the React concept still tripping you up? Drop it in the comments — your question might become the next article in this series.
Top comments (0)