Today I Learned
Short, practical notes on things I pick up while building. A micro-garden of raw discoveries.
10 entries
The React Hook Closure Trap
A useEffect callback captures the values from its render scope. If you reference state inside a setInterval without adding it to the dependency array, you see stale values — the closure "traps" the initial render's snapshot.
// Bug: count is always 0 inside the interval
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // stale!
}, 1000);
return () => clearInterval(id);
}, []); // missing `count` dependency
// Fix: use functional updater — no dependency needed
useEffect(() => {
const id = setInterval(() => {
setCount(prev => prev + 1); // always fresh
}, 1000);
return () => clearInterval(id);
}, []);
The functional updater prev => prev + 1 bypasses the closure entirely because React calls it with the latest state. This is the idiomatic fix for any "stale state in async callback" problem.
Rule of thumb: if you need current state inside an effect that shouldn't re-run on every state change, use a functional updater or a useRef to escape the closure.
Nash Equilibrium: The Intuition Behind the Math
A Nash Equilibrium is a strategy profile where no player can improve their payoff by unilaterally changing their strategy:
The key word is unilaterally. At equilibrium, every player is already doing the best they can given what everyone else is doing. It doesn't mean everyone is happy — it means no one has a profitable deviation.
The Prisoner's Dilemma makes this concrete: both players defecting is a Nash Equilibrium (neither can improve by switching alone), even though both cooperating would make everyone better off. The equilibrium is stable but not optimal.
Nash's Theorem (1950): Every finite game (finite players, finite strategies) has at least one Nash Equilibrium, possibly in mixed strategies. The proof uses Brouwer's fixed-point theorem — a beautiful connection between topology and strategic reasoning.
This is the same deterministic fairness guarantee I rely on in SecureExam-Generator: when the "game" (exam distribution) reaches equilibrium, no student can gain an advantage by attempting to predict the shuffle — because the ordering is cryptographically bound to their identity.
Graph Degree Centrality
In a graph , the degree centrality of a node is simply the fraction of other nodes it connects to:
where is the number of edges incident to . A node with is connected to every other node in the graph.
This is the simplest centrality measure, but it's surprisingly useful for digital gardens: the most connected node in my knowledge graph is likely the most "hub-like" concept — the one that ties disparate ideas together.
For this site's graph with nodes, a node with 10 connections has . The /stats page computes this live.
Variants:
- In-degree centrality (directed graphs): how many nodes point to you — a measure of authority
- Betweenness centrality: how often a node appears on shortest paths between other nodes — identifies bridges
- Eigenvector centrality: weights connections by the centrality of neighbors — "it's not just who you know, but who they know"
CRDTs need Lamport clocks, not wall clocks
When building NotePadIo's real-time sync, I learned that wall clocks (system time) are unreliable for ordering events across devices. Two clients may have different system times, making Date.now() useless for determining which edit came first.
Lamport clocks solve this: every operation carries a logical counter that increments on send and takes the max on receive. The ordering is deterministic regardless of actual time — which is exactly what CRDTs need for conflict-free merging.
let counter = 0;
function send(op: Operation) {
counter++;
broadcast({ ...op, timestamp: counter });
}
function receive(msg: Message) {
counter = Math.max(counter, msg.timestamp) + 1;
apply(msg);
}
The key insight: you don't need to know when something happened, just in what order relative to everything else.
10,287 hours
I pulled my total Steam playtime as a stat for this site. The number came back: 10,287 hours across 12 years.
That's roughly 4 hours a day, every day, since I was a kid.
The math doesn't lie — and for once, I'm grateful for it. Because sitting with that number long enough to feel it is what finally made it real. Not a lecture, not a warning. Just a number on a screen I built myself.
4 hours a day is:
- A consistent gym habit
- A full night of deep sleep instead of staying up
- A second skill, learned properly, compounded over a decade
- The exact time I spent building this entire website in two weeks
I'm not here to shame gaming. I'm here to acknowledge the tradeoff I was making without ever consciously choosing it.
The project that helped me see this — this website — took ~30 hours over 2 weeks. Imperfect, handmade, mine. And it taught me more about how I want to spend the next 10 years than 10,287 hours of anything passive ever did.
I'm not quitting cold turkey. I'm just finally paying attention to the exchange rate.
SHA-256 as a deterministic shuffle seed
In SecureExam-Generator, I needed each student to get a unique but reproducible question ordering. The trick: hash (student_id + exam_id + secret_salt) with SHA-256 and use the first 8 bytes as a PRNG seed.
import hashlib, random
def seeded_shuffle(questions, student_id, exam_id, salt):
digest = hashlib.sha256(
f"{student_id}{exam_id}{salt}".encode()
).digest()
seed = int.from_bytes(digest[:8], "big")
rng = random.Random(seed)
rng.shuffle(questions)
return questions
Same inputs always produce the same ordering — the shuffle is verifiable, not random noise. An examiner can recompute the exact sequence from the student ID alone, without storing any state.
View Transitions API works without JavaScript
The View Transitions API can create smooth page transitions with just CSS. The browser captures a screenshot of the old state, then cross-fades to the new state. I used this for the theme toggle on this site — the circular reveal effect is purely CSS-driven once the API is triggered.
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.4s;
}
Event sourcing: the log IS the database
In NotePadIo, a document is not a mutable blob — it's an append-only log of operations (insert, delete, move-block). The current state is always the deterministic result of replaying that log from the beginning.
This sounds expensive, but it buys you four things for free:
- Revision history — every past state is recoverable by replaying to a specific point
- Offline sync — queue operations locally, replay on reconnect
- Conflict resolution — operations commute, so order doesn't matter
- Audit logging — who changed what, when, automatically
The trade-off: replay time grows with document size. Fix: periodic snapshots (materialized state at log position N). Replay only from the latest snapshot forward.
Log: [op1, op2, op3, SNAPSHOT@3, op4, op5]
Replay: load SNAPSHOT@3 → apply op4 → apply op5
Next.js static export means zero runtime cost
With output: "export" in Next.js, the build produces plain HTML/CSS/JS files. No Node.js server runs in production. This means GitHub Pages can serve the site for free with no cold starts, no serverless functions, and no infrastructure to maintain. The trade-off: no API routes, no ISR, no middleware. Every dynamic feature must be client-side or pre-built.
react-force-graph-2d requires ssr: false
react-force-graph-2d uses HTML Canvas internally, which doesn't exist during server-side rendering. The fix is a dynamic import with ssr: false:
const ForceGraph2D = dynamic(
() => import("react-force-graph-2d"),
{ ssr: false }
);
This pattern applies to any library that touches the DOM or Canvas API at import time.
Mentioned by
Writing
Polished, long-form writing on software engineering, digital gardening, mathematics, and technical decision-making.
⟳ tended 1mo ago
SecureExam
Tamper-proof exam generation using SHA-256 content hashing, QR-embedded verification codes, and institutional filigree watermarks. Deterministic question shuffling ensures each student receives a unique, verifiable paper.
⟳ tended 1mo ago
NotePadIo
Real-time collaborative note-taking built on an event-sourced document model with CRDT-inspired conflict resolution. Operations carry Lamport clock timestamps for deterministic merge across concurrent editors.
⟳ tended 1mo ago
Game Theory
Intensive training at Ali Nesin Mathematics Village covering Nash equilibria, minimax theorem, Shapley value, and mechanism design. Formal proofs applied to real-world strategic reasoning.
⟳ tended 1mo ago