<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maksym Kuzmitskyi (MaximusFT)</title>
    <description>The latest articles on DEV Community by Maksym Kuzmitskyi (MaximusFT) (@maximusft).</description>
    <link>https://dev.to/maximusft</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1819104%2Fd158bf22-3ff5-498a-bab8-91ce4b684bc1.jpg</url>
      <title>DEV Community: Maksym Kuzmitskyi (MaximusFT)</title>
      <link>https://dev.to/maximusft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maximusft"/>
    <language>en</language>
    <item>
      <title>Guardrails for a Runaway Agent</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Fri, 14 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/guardrails-for-a-runaway-agent-8h5</link>
      <guid>https://dev.to/maximusft/guardrails-for-a-runaway-agent-8h5</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbahuquz44tgvkojjeylp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbahuquz44tgvkojjeylp.png" alt="Guardrails for a Runaway Agent" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything in this cluster has circled the same failure: an agent that runs longer, spends more, and drifts further than you intended, with nothing to stop it. The &lt;a href="https://ma-x.im/blog/agent-playbook-you-dont-need-multi-agents" rel="noopener noreferrer"&gt;multi-agent orchestra&lt;/a&gt; fractures context, the &lt;a href="https://ma-x.im/blog/agent-playbook-token-bill" rel="noopener noreferrer"&gt;token bill&lt;/a&gt; balloons, the &lt;a href="https://ma-x.im/blog/agent-playbook-test-on-change" rel="noopener noreferrer"&gt;scheduled loop&lt;/a&gt; burns all night. Underneath all of them is one uncomfortable property, and Anthropic name it directly: agents are stateful, and their errors &lt;em&gt;compound&lt;/em&gt;. One bad step doesn't just produce one bad result — it changes the state the next step reasons from, so the agent quietly walks off in a completely different direction and has no idea it's lost.&lt;/p&gt;

&lt;p&gt;That's the thing traditional software doesn't really do. A bug usually breaks a feature and stops. An agent's bug can send it on a confident, expensive journey to nowhere. So the design question isn't "how do I make the agent never fail?" — you can't. It's "when it fails, how do I make sure it fails &lt;em&gt;small&lt;/em&gt;?"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't grant an agent autonomy by trusting it. You grant it autonomy by bounding what it can do when you're wrong to trust it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Cap the loop
&lt;/h2&gt;

&lt;p&gt;The first guardrail is the dumbest one and the most important: a hard ceiling on how many steps or iterations a run can take. Anthropic build exactly this into their agents — a maximum number of iterations as a stopping condition, so a task that can't converge &lt;em&gt;terminates&lt;/em&gt; instead of spinning. This is the single line that turns the overnight-burn story into a non-event: the agent tries twenty times, hits the cap, stops, and tells you it's stuck. "Burned all night" becomes "gave up before breakfast."&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget the spend
&lt;/h2&gt;

&lt;p&gt;The step cap bounds &lt;em&gt;count&lt;/em&gt;; a spend cap bounds &lt;em&gt;cost&lt;/em&gt; directly. Give a run a token or dollar budget and a circuit breaker that trips when it's exceeded. Even if you got the iteration ceiling wrong, the budget is a second, independent wall between a stuck agent and a catastrophic invoice. Two different limits guarding the same failure is not redundancy — it's the point. The whole philosophy here is that any single guardrail might be the one you misconfigured.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notice when it stops making progress
&lt;/h2&gt;

&lt;p&gt;A cap stops an agent eventually; progress-detection stops it &lt;em&gt;sooner&lt;/em&gt;. If the agent takes the same action, gets the same result, and tries again — that's not work, that's a loop, and you can detect it. Repeated identical tool calls, the same error over and over, no change in state across steps: these are all signals that the run has stopped advancing and should be cut short. It's the difference between letting it exhaust twenty iterations and killing it at the third pointless repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put a human at the sharp edges
&lt;/h2&gt;

&lt;p&gt;Not every action deserves the same freedom. Reading a ticket, running a test, opening a draft PR — low consequence, let it run. Deleting data, pushing to a shared branch, spending real money, touching production — these are where autonomy should pause and check with a person. This isn't timidity; it's the &lt;a href="https://ma-x.im/blog/agent-playbook-do-it-as-usual" rel="noopener noreferrer"&gt;same negotiated boundary&lt;/a&gt; that makes "do it as usual" safe to run unattended. The agent owns the reversible actions and stops at the irreversible ones. You decide where that line sits per action, deliberately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shrink the blast radius
&lt;/h2&gt;

&lt;p&gt;The guardrails above assume the agent &lt;em&gt;tries&lt;/em&gt; something bad; sandboxing assumes it &lt;em&gt;succeeds&lt;/em&gt; and makes that survivable. Least privilege is the whole idea: an agent that physically cannot reach production can't take it down, no matter how confused it gets. Run in an environment where the worst thing it can do is still recoverable — a scratch branch, a staging database, scoped credentials. Then a runaway is an annoyance, not an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  See why it ran away
&lt;/h2&gt;

&lt;p&gt;You can't tighten a guardrail you can't see through. Anthropic lean hard on tracing agent decisions for exactly this reason — when an agent "can't find obvious information" or spirals, the only way to fix it systematically is to look at what it actually did, step by step. Observability isn't a nice-to-have bolted on at the end; it's how you learn which guardrail was missing. Every runaway you can inspect becomes a rule you can add.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;There's a temptation to read all of this as distrust — as clipping the agent's wings. I see it the opposite way. Guardrails are what let me hand an agent &lt;em&gt;more&lt;/em&gt; autonomy, not less, because I know the failure modes are bounded. The reason I can say "do it as usual" and walk away is precisely that a stuck run caps out, a runaway can't reach anything that matters, and I can see afterward what went wrong. The bounds are what make the trust affordable.&lt;/p&gt;

&lt;p&gt;An agent without guardrails isn't a bold, high-trust setup. It's an unbounded process with a credit card and access to your systems, and the only thing standing between it and a bad night is luck. Design the blast radius first, and autonomy stops being a gamble. That's the through-line of this whole cluster: the goal was never a more impressive agent. It was a &lt;em&gt;sane&lt;/em&gt; one — one you can actually let run, because you decided in advance how it's allowed to fail.&lt;/p&gt;

&lt;p&gt;Take one agent you run unattended and write down what happens if it gets stuck right now: does it stop on its own, and what can it reach while it's confused? If either answer is uncomfortable, you've found your next guardrail. Tell me which one — I'd bet it's the iteration cap, because it almost always is.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>Test on Change, Not on a Timer</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Wed, 12 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/test-on-change-not-on-a-timer-11hn</link>
      <guid>https://dev.to/maximusft/test-on-change-not-on-a-timer-11hn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysski64r34ysbzvr936t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysski64r34ysbzvr936t.png" alt="Test on Change, Not on a Timer" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go back to the &lt;a href="https://ma-x.im/blog/agent-playbook-token-bill" rel="noopener noreferrer"&gt;overnight-burn story&lt;/a&gt; for a second, because the detail that matters most is the one everyone skips past. The agent was on a &lt;em&gt;schedule&lt;/em&gt;. It woke up on a timer to check the environment, over and over, whether or not anything had actually changed. The loop was the accident. The schedule was the design flaw that made the accident possible.&lt;/p&gt;

&lt;p&gt;And it's such a natural design flaw. When you want an agent to keep something healthy, the first thing your brain reaches for is a cron job: "every N minutes, go check." It feels responsible — always watching, always on. But a timer is a fundamentally lazy trigger. It fires on the passage of time, which is almost never the thing you actually care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timer runs when nothing happened
&lt;/h2&gt;

&lt;p&gt;Think about what a scheduled agent actually does most of the time: it wakes up, looks at an environment that is &lt;em&gt;exactly&lt;/em&gt; as it left it, does a bunch of work to confirm nothing changed, and goes back to sleep. You're paying full agent price — remember, &lt;a href="https://ma-x.im/blog/agent-playbook-token-bill" rel="noopener noreferrer"&gt;4× a chat and up&lt;/a&gt; — to repeatedly discover that nothing is different.&lt;/p&gt;

&lt;p&gt;And on the rare occasion something &lt;em&gt;is&lt;/em&gt; wrong, the timer is no help there either. If the environment breaks thirty seconds after a run, your schedule sits on that failure until the next tick. So you tighten the interval to catch problems faster, which means even more empty runs, which means more cost and more surface area for exactly the kind of loop that burned all night. The timer is bad at both jobs: it's wasteful when things are fine and slow when they aren't.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A clock is not an event. Waking an agent on a schedule means waking it for no reason, most of the time — and paying for the privilege.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wake it on a reason
&lt;/h2&gt;

&lt;p&gt;The alternative is almost obvious once you name it: run the agent when something &lt;em&gt;changes&lt;/em&gt;. Not "every five minutes," but "when a commit lands," "when a deploy finishes," "when a check goes red," "when this file is updated." The trigger is an event that carries actual meaning, and the agent's run is a response to that meaning.&lt;/p&gt;

&lt;p&gt;This flips every problem the timer had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No empty runs.&lt;/strong&gt; No change, no event, no run. You stop paying to confirm the status quo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immediate response.&lt;/strong&gt; The agent reacts the moment the thing it cares about happens, not on the next tick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Naturally bounded work.&lt;/strong&gt; One event produces one run with a clear scope: "here's what changed, deal with it." That's a far easier task to reason about — and to cap — than an open-ended "go check everything."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggable history.&lt;/strong&gt; Every run traces back to a specific event. When something misbehaves, you know exactly what set it off, instead of squinting at a timeline of identical scheduled wake-ups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Testing is the cleanest example. Running your whole suite on a timer is nonsense — nothing changed, so nothing new can break. Running it when code changes is the entire point. The event &lt;em&gt;is&lt;/em&gt; the reason the test is worth running. That principle generalizes: the right moment to act is the moment the world became different, because that's the only moment new information exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest exceptions
&lt;/h2&gt;

&lt;p&gt;I don't want to pretend timers never have a place. Some work genuinely is periodic: a nightly summary, a daily digest, a report that's tied to the calendar rather than to a change. If the &lt;em&gt;business meaning&lt;/em&gt; is "once a day," then a schedule is correct — the time itself is the event.&lt;/p&gt;

&lt;p&gt;But even then, two rules hold. First, it should still be bounded hard, because "runs on a schedule" and "can run forever once started" are independent properties, and the second one is what bites you. Second, be honest about whether the task is &lt;em&gt;actually&lt;/em&gt; periodic or whether you reached for a timer because it was the easy primitive. Most "check the environment every N minutes" jobs are change-driven work wearing a schedule's clothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;The shift from timer to event is small in code — a webhook instead of a cron entry — and large in consequences. It's the difference between an agent that runs because a clock ticked and an agent that runs because something happened that it should respond to. The first is a machine idling in your driveway, burning fuel to go nowhere. The second only starts when there's somewhere to go.&lt;/p&gt;

&lt;p&gt;That overnight bill wasn't really caused by a loop. The loop was the spark. The schedule was the pile of dry wood that let a single spark burn until morning. Trigger the agent on change instead, and most of that wood is never there to catch — the agent simply isn't running when there's nothing to do. What you still need is a firebreak for the runs that &lt;em&gt;do&lt;/em&gt; go wrong, and that's the last piece: &lt;a href="https://ma-x.im/blog/agent-playbook-guardrails" rel="noopener noreferrer"&gt;the guardrails that cap the damage&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Find one scheduled agent job you're running and ask what event it's really waiting for. If you can name the change it's checking for, you can trigger on that change directly — and delete the clock. Tell me what the event turned out to be; naming it is usually the moment the cron job stops making sense.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>You Only Get to See It From the Outside Once</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/you-only-get-to-see-it-from-the-outside-once-4a05</link>
      <guid>https://dev.to/maximusft/you-only-get-to-see-it-from-the-outside-once-4a05</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1ipv7akxxpww293d2mz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu1ipv7akxxpww293d2mz.png" alt="You Only Get to See It From the Outside Once" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was in the middle of writing this article when my wife started giving me notes on it. And I did the thing I apparently do — I pushed back. I explained that engineering is different on the inside, that the processes don't work the way she was picturing, that she was missing context. I had reasons. I always have reasons.&lt;/p&gt;

&lt;p&gt;She let me finish. Then she pointed out, pretty gently, that I had just acted out the entire premise of the article I was writing. She was the outsider looking at my argument with fresh eyes. I was the one who had already climbed inside it and could no longer see it straight. On an article that is &lt;em&gt;specifically about that&lt;/em&gt;. She was out of the box. I was in it. And I couldn't tell until she said so.&lt;/p&gt;

&lt;p&gt;That small, slightly embarrassing moment is the whole thing I want to talk about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asset nobody writes on the offer letter
&lt;/h2&gt;

&lt;p&gt;When an experienced person — an architect, a staff engineer, a senior hire — joins a project, they show up with something that has nothing to do with their résumé. For a short window, they can see the project &lt;em&gt;from the outside&lt;/em&gt;. They notice the thing that makes no sense. They ask "why is it like this?" about the part everyone else stopped seeing a year ago. They feel the friction a new user feels, because right now they basically &lt;em&gt;are&lt;/em&gt; a new user.&lt;/p&gt;

&lt;p&gt;That vantage point is not a bonus on top of the "real" work. For a lot of senior hires, it &lt;em&gt;is&lt;/em&gt; the work — or it should be. You're not paying a premium for another pair of hands to type features. You're paying for someone who can still look at the whole thing and tell you, honestly, what's wrong with it before they've been anesthetized into thinking it's normal.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The outside view is the most valuable thing an experienced engineer brings — and it's the first thing a team destroys without meaning to.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  It has a shelf life
&lt;/h2&gt;

&lt;p&gt;Here's the cruel part. That view is perishable. It doesn't last. Six months in, you've internalized the map. You know where everything lives, which means you've stopped noticing that nothing &lt;em&gt;should&lt;/em&gt; live there. You know why the weird thing is weird — there's a Slack thread, a migration, a reason — and knowing the reason is exactly what makes you stop seeing that it's a problem. You've learned where the bodies are buried, so you no longer notice you're standing in a graveyard.&lt;/p&gt;

&lt;p&gt;Keeping the outside view alive past that honeymoon is genuinely hard. It takes deliberate effort from the person, and — much more — a culture that protects it. Most places have neither. So the clarity that walked in the door on day one quietly evaporates, and in a year that same brilliant hire is defending the exact weirdness they were hired to question.&lt;/p&gt;

&lt;h2&gt;
  
  
  How teams burn it on day one
&lt;/h2&gt;

&lt;p&gt;The way companies destroy this asset is almost always the same, and it's well-intentioned. They hire a senior person because there's a hot new feature, or a new branch of the product, or a fire. And then, on day one, they submerge them: &lt;em&gt;"Great, let us walk you through how everything works here."&lt;/em&gt; Onboarding as immersion. The clock starts, the deadlines are already there, and the expectation is feature output by roughly week two.&lt;/p&gt;

&lt;p&gt;Think about what that does. You took the one person who could still see the project from the outside, and you rushed them inside as fast as possible. You spent their most valuable, most perishable asset on getting them productive on tickets — and you never once asked them the question only they could answer for a few short weeks: &lt;em&gt;what looks wrong to you right now, before you get used to it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What they actually needed was a little room. Time to pick the thing up, turn it over, feel how it behaves, and notice. Time to reflect. That's not slacking; that's the senior work. But reflection has no line in the sprint, so it's the first thing the deadline eats.&lt;/p&gt;

&lt;h2&gt;
  
  
  I hit this wall myself
&lt;/h2&gt;

&lt;p&gt;Let me be specific, because I lived the other side of this. A few years back I joined a Canadian health-tech company in a good, senior role. I was there about six months, and then we parted ways — not dramatically, but because what they wanted and what I was doing had quietly stopped being the same thing.&lt;/p&gt;

&lt;p&gt;I did a lot in those months. I set up practices, fixed how a bunch of things were used. Their setup was wild in places — a designer could change a button through an AI tool more or less directly. There was review, technically. But there was essentially &lt;em&gt;zero&lt;/em&gt; consistency in the code. And every time I actually picked the project up to work in it, it came apart in my hands. Long legacy, dressed in modern libraries, with no architecture underneath — you genuinely could not tell where anything lived or why.&lt;/p&gt;

&lt;p&gt;Here's the mismatch. What I was offering them was the outside view: I'm looking at this thing fresh, and I'm telling you it doesn't hold together. That was the most useful thing I had. But what they wanted, increasingly, was for me to stop looking and start shipping — hands inside the box, features out the door. They'd hired a senior and then asked him to stop doing the senior part. I couldn't unsee what I was seeing, and they didn't want to act on it. So it ended.&lt;/p&gt;

&lt;p&gt;I'm not telling that story to settle a score. Honestly, I get it — the pressure to ship is real and it's not villainy. I'm telling it because it's the clearest example I have of a company holding a valuable asset and spending it on the wrong thing. They paid for an outside view and then treated it as an obstacle to velocity.&lt;/p&gt;

&lt;h2&gt;
  
  
  For the manager, and for the senior
&lt;/h2&gt;

&lt;p&gt;So this cuts two ways, and I want to be clear about both, because the two audiences own different halves of the fix.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;manage&lt;/em&gt; senior hires: the outside view is an asset with an expiry date, and you are almost certainly burning it. The window where a new architect or staff engineer can tell you what's actually wrong is measured in weeks, not quarters. Protect it. Before you submerge them in "how we do things here," ask them what looks broken while they can still see it — and write it down, because in three months they won't be able to tell you anymore. And then think about the harder, cultural problem: how do you keep &lt;em&gt;anyone&lt;/em&gt; on the team seeing the product from the outside once the honeymoon ends? That's the real work of engineering culture, and it doesn't happen by accident.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;are&lt;/em&gt; the senior: your value was never the volume of code you produce. Plenty of people can produce code. Your rare, decaying advantage is that right now you can still see the box from the outside. Guard it. In your first weeks somewhere new, write down everything that looks wrong, everything that makes you go "wait, why?" — because that document is worth more than the features you'll ship in that same period, and you will not be able to write it later. You'll have gone native.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;The thing that still gets me about the moment with my wife is how fast it happened, and how invisible it was to me. I wasn't being stubborn on some project I'd spent years on. I'd been inside &lt;em&gt;my own article&lt;/em&gt; for a couple of days, and that was already enough to make me defend it against exactly the kind of outside perspective the article is arguing you should protect. If it happens that quickly to me, on a piece of writing, imagine what six months of deadlines does to a person's ability to see the system they work in.&lt;/p&gt;

&lt;p&gt;The outside view isn't a personality trait. It's a position — you're either standing outside the thing or you're inside it — and you lose it the moment you step in and get comfortable. Which means the whole game, for teams and for individuals, is noticing that the window is open right now, and using it before it quietly closes.&lt;/p&gt;

&lt;p&gt;Think about the last time someone from outside your project — a new hire, a designer, someone who doesn't live in the code — told you something was wrong, and your first instinct was to explain why they didn't understand. That instinct is the tell. I'd genuinely like to know what they were seeing that you couldn't — reach out and tell me, because odds are they were standing exactly where you used to.&lt;/p&gt;

</description>
      <category>engineeringculture</category>
      <category>leadership</category>
      <category>career</category>
    </item>
    <item>
      <title>The Token Bill Is Part of the Architecture</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Mon, 10 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/the-token-bill-is-part-of-the-architecture-39g8</link>
      <guid>https://dev.to/maximusft/the-token-bill-is-part-of-the-architecture-39g8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7141xbyedji9qjkj858b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7141xbyedji9qjkj858b.png" alt="The Token Bill Is Part of the Architecture" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's a story that made the rounds a while back, and I keep thinking about it. Someone set up an agent to run on a schedule — every so often it would wake up and check their environment, run some tests, make sure things were healthy. Reasonable idea. Then one night something went wrong, the agent got stuck in a loop, and instead of running for the couple of hours it was supposed to, it churned all night. By morning it had burned through an eye-watering pile of tokens doing nothing useful.&lt;/p&gt;

&lt;p&gt;It's easy to read that as "lol, watch your usage." I read it differently. That bill wasn't an accident of carelessness — it was a design that never treated cost as real. The agent had no sense that its own runtime cost money, and nothing in the system did either. And that's the norm, not the exception. We design agent workflows around capability and correctness, and we bolt cost on afterward as a thing to monitor. I think that's backwards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cost isn't something you check after the fact. With agents, it's a first-class architectural constraint — as much a part of the design as latency or correctness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Agents are expensive in a way chat isn't
&lt;/h2&gt;

&lt;p&gt;Let me put real numbers on it, because the gap is bigger than people expect. Anthropic published figures from their own systems: an agent uses roughly &lt;strong&gt;4× the tokens&lt;/strong&gt; of a normal chat interaction, and a multi-agent system about &lt;strong&gt;15×&lt;/strong&gt;. Every autonomous loop, every "let me check that again," every re-read of a big context window — it all adds up per step, and agents take a &lt;em&gt;lot&lt;/em&gt; of steps.&lt;/p&gt;

&lt;p&gt;This is why Anthropic's own top-line advice for building with LLMs is to find the simplest thing that works and only add complexity when it demonstrably pays. That's not aesthetic minimalism. It's economics. Agentic autonomy trades latency and money for flexibility, and if the task didn't need the flexibility, you just paid a premium for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the money actually leaks
&lt;/h2&gt;

&lt;p&gt;Once you start looking at cost as architecture, you start seeing where it drains:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unbounded loops.&lt;/strong&gt; The overnight-burn story is the pure form. An agent that can retry forever will, eventually, retry forever. Without a hard ceiling, one stuck state is an open tap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents pinging agents.&lt;/strong&gt; The multi-agent orchestras I complained about in &lt;a href="https://ma-x.im/blog/agent-playbook-you-dont-need-multi-agents" rel="noopener noreferrer"&gt;the first article&lt;/a&gt; don't just fracture context — they multiply token spend. One agent asking another "are you done yet?" is a paid API call every time it asks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-sending the world every turn.&lt;/strong&gt; If every step drags the entire history back through the model, your cost grows with the square of the conversation. Long-running agents that don't compress or summarize their context pay for their whole past on every single step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reaching for a big model on easy work.&lt;/strong&gt; Routing a trivial classification through your most capable, most expensive model is money set on fire. Anthropic explicitly route easy, common cases to a small cheap model and reserve the expensive one for the hard cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing like the meter is running
&lt;/h2&gt;

&lt;p&gt;None of these are exotic to fix. They just require treating the token meter as a signal you design against, the same way you'd design against a latency budget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Put a ceiling on every loop.&lt;/strong&gt; A hard cap on iterations or steps turns "burned all night" into "gave up after twenty tries and told me." This is the first guardrail, and it's the subject of &lt;a href="https://ma-x.im/blog/agent-playbook-guardrails" rel="noopener noreferrer"&gt;its own article&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give long tasks a memory, not a longer transcript.&lt;/strong&gt; Summarize completed phases and store the essentials, so each step reasons over a compact context instead of re-paying for the entire history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Match the model to the job.&lt;/strong&gt; Cheap model for routing and simple calls, expensive model where the reasoning actually earns it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer a workflow to an autonomous loop when the task is well-defined.&lt;/strong&gt; A fixed pipeline of LLM calls is predictable in both behavior &lt;em&gt;and&lt;/em&gt; cost. You reach for open-ended autonomy when you truly can't predict the steps — not by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;The overnight-burn story gets told as a cautionary tale about monitoring, but monitoring is the weakest possible response — it tells you about the fire after it's out. The real fix is upstream, in the design: an agent whose cost is bounded by construction can't run up that bill in the first place, because the ceiling, the compaction, and the stop conditions were part of the architecture, not a dashboard you forgot to check.&lt;/p&gt;

&lt;p&gt;So I've started treating the token bill the way I treat a slow render or a memory leak: as a property of the system I'm responsible for shaping, not an invoice that shows up later. When I sketch an agent workflow now, "what does one run cost, and what stops it from costing a hundred times that?" is one of the first questions, not the last. The story that made the rounds is only a horror story if cost was never in the design. Put it in, and the worst case is a bounded disappointment instead of a night's worth of tokens gone.&lt;/p&gt;

&lt;p&gt;The next two articles follow this thread straight down: &lt;a href="https://ma-x.im/blog/agent-playbook-test-on-change" rel="noopener noreferrer"&gt;when to actually run the agent&lt;/a&gt; — because that scheduled loop was the root of the burn — and &lt;a href="https://ma-x.im/blog/agent-playbook-guardrails" rel="noopener noreferrer"&gt;the guardrails&lt;/a&gt; that cap the damage when a run goes wrong anyway.&lt;/p&gt;

&lt;p&gt;Go look at one agent workflow you run and answer a single question: what's the most it could spend if it got stuck right now? If you don't know, that's the number an attacker on your patience — a loop — already knows. Tell me what you find; the not-knowing is the whole problem.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>Do It As Usual: Teaching One Agent Your Whole Workflow</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sat, 08 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/do-it-as-usual-teaching-one-agent-your-whole-workflow-4gkn</link>
      <guid>https://dev.to/maximusft/do-it-as-usual-teaching-one-agent-your-whole-workflow-4gkn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5osbcor7uuq6kaws7hh6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5osbcor7uuq6kaws7hh6.png" alt="Do It As Usual: Teaching One Agent Your Whole Workflow" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a thing I can do that sounds like a party trick. I open a brand-new session with my agent and type, almost word for word: "take ticket 123 and do it." And it does — the whole thing, end to end, without me hovering. Not because I found a magic prompt. Because I spent months teaching it what "as usual" means.&lt;/p&gt;

&lt;p&gt;I bring this up because of a complaint I keep hearing, some version of: &lt;em&gt;"I can't get my agent to just do a task. I set it loose and then I have to walk behind it fixing everything."&lt;/em&gt; And look — if you started last week, that's completely fair. I couldn't either at first. But when someone's been at it for months and still can't get the agent to carry a task cleanly, I get suspicious. Not of the agent. Of how they're working with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "As usual" is a lot of things
&lt;/h2&gt;

&lt;p&gt;Let me show you what actually happens when I say "do it as usual," because the phrase is hiding an entire workflow the agent has internalized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It takes the ticket and assigns it to me.&lt;/li&gt;
&lt;li&gt;It moves it to In Progress.&lt;/li&gt;
&lt;li&gt;If the ticket was sitting in the backlog, it pulls it into the current sprint.&lt;/li&gt;
&lt;li&gt;If it has no story points, it estimates it — and if it genuinely can't tell, it asks me, the way we agreed it would.&lt;/li&gt;
&lt;li&gt;It creates a branch by our naming convention, after first updating main so it's not branching off something stale.&lt;/li&gt;
&lt;li&gt;It makes the changes, committing as it goes with human-readable messages that say what was actually done.&lt;/li&gt;
&lt;li&gt;It opens the pull request, writes a real description, adds the necessary context and links, mentions the people who need to see it, and links the PR back to the ticket.&lt;/li&gt;
&lt;li&gt;Then it waits for the pipelines, checks whether anything went red, and fixes it if it did.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that from four words. And none of it was true on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody wants to hear
&lt;/h2&gt;

&lt;p&gt;That list didn't arrive as a single clever configuration. It arrived one failure at a time. The agent would branch off stale main — so I taught it to update first. It would write commit messages like "fix stuff" — so we worked out what a human-readable message looks like. It would guess a story-point estimate confidently when it had no business guessing — so we agreed that when it's unsure, it stops and asks me instead of inventing a number.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every step in "as usual" is a scar. It's there because something once went wrong, and I turned the fix into a rule instead of re-explaining it every session.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the whole method, honestly. It's not prompt cleverness. It's accumulation. Each time the agent got something wrong, I didn't just correct it for that one run — I wrote the correction down somewhere durable, so the next session started from the corrected baseline. Do that for a few months across a dozen steps, and "as usual" stops being a phrase and becomes a reflex the agent actually has.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give it real context, not guesses
&lt;/h2&gt;

&lt;p&gt;The second half of this is tooling. An agent working from guesses is an agent you'll always be correcting. So I connected the things it needs to &lt;em&gt;know&lt;/em&gt; the state of the world instead of hallucinating it: an MCP server to read Jira, another for Confluence, one for our docs, one for Postman — the tools the work actually runs on.&lt;/p&gt;

&lt;p&gt;The difference is night and day. When the agent can read the ticket, it doesn't misunderstand the task. When it can see the docs and the API collection, it doesn't invent an endpoint. The tools aren't there to make the agent look impressive — they're there so its decisions are grounded in what's real. This is the &lt;a href="https://ma-x.im/blog/agent-playbook-you-dont-need-multi-agents" rel="noopener noreferrer"&gt;augmented-LLM idea&lt;/a&gt; done properly: one agent, made capable by good tools, is worth more than a crowd of agents guessing at each other's intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  One agent, not one per repo
&lt;/h2&gt;

&lt;p&gt;I told this story in &lt;a href="https://ma-x.im/blog/agent-playbook-you-dont-need-multi-agents" rel="noopener noreferrer"&gt;the multi-agents piece&lt;/a&gt;, but it belongs here too, because it's the same instinct. My project spans six or seven repositories. My first move was to set up an agent in each one. Then I noticed my tasks almost never span repos — they live in one, and multi-repo changes have come up maybe twice. So instead of maintaining seven configurations, I put every repo in one workspace and taught one agent. Every hour I &lt;em&gt;didn't&lt;/em&gt; spend duplicating setup across seven repos, I spent making the single one better. That's why it can do "as usual" at all — the investment was concentrated, not scattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision points matter most
&lt;/h2&gt;

&lt;p&gt;The subtle part — the thing that separates an agent that helps from one you babysit — is deciding where it should act and where it should stop and ask. I didn't tell it to just do everything autonomously. We agreed on the seams. Estimation it can't confidently do? Ask. Convention it knows cold? Just do it. That negotiated boundary is what makes "as usual" safe to run unattended. The agent isn't guessing about my intent at the risky moments, because we already talked through those moments and it knows the rule.&lt;/p&gt;

&lt;p&gt;This is why "just be more autonomous" is bad advice. Autonomy isn't a slider you crank to the top. It's a map of which decisions the agent owns and which ones come back to you — and you build that map the same way you build everything else here, one correction at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;So when I hear "I can't get my agent to do a task in one shot," what I mostly hear is a missing investment. People want the "do it as usual" outcome without the months of turning mistakes into durable rules and wiring in the tools that ground the agent in reality. The agent that runs my whole ticket workflow from four words isn't smarter than yours. It's just been &lt;em&gt;taught&lt;/em&gt; more, and the teaching stuck because I wrote it down instead of repeating it.&lt;/p&gt;

&lt;p&gt;The uncomfortable truth is that this looks a lot less like prompting and a lot more like onboarding a junior engineer who happens to have perfect recall. You explain the convention once, correctly, and it never forgets it — but only if you actually captured it instead of hand-waving it in a single session that evaporates when the context does.&lt;/p&gt;

&lt;p&gt;Pick the one task you do most often with your agent and write down every correction you find yourself repeating this week. That list is the start of your own "as usual." Send it to me if you want a second pair of eyes — I'll tell you which corrections are rules and which are you doing the agent's thinking for it.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>You Probably Don't Need Multi-Agents</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/you-probably-dont-need-multi-agents-83a</link>
      <guid>https://dev.to/maximusft/you-probably-dont-need-multi-agents-83a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgnhosylqm7wpsg1z1ho5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgnhosylqm7wpsg1z1ho5.png" alt="You Probably Don't Need Multi-Agents" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I work across six or seven repositories on one project — a big hybrid thing, part microfrontend, part backend, several apps that all talk to each other. When I started bringing an AI agent into that work, my first instinct was the obvious one: set up an agent in each repo. Seven repos, seven configured agents. It felt right. Each repo is its own world, so each gets its own helper.&lt;/p&gt;

&lt;p&gt;I got a couple of repos in before I stopped and asked myself what I was actually doing. My tasks almost never live in seven repos at once. They live in &lt;em&gt;one&lt;/em&gt;. In months of work I've had maybe two tasks that touched multiple repos. So I was about to build and maintain seven separate agent setups to serve a reality where the work is overwhelmingly single-repo. I threw it out, put every repo into one workspace, and configured one agent, once. That agent has been better than any swarm I could have wired together — and it cost me a fraction of the effort.&lt;/p&gt;

&lt;p&gt;That small decision is the whole argument of this article, scaled up. The industry's default reflex right now is &lt;em&gt;more agents&lt;/em&gt;. I think the default should be &lt;em&gt;one agent, taught well&lt;/em&gt; — and you should have to justify every agent you add past the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The orchestra is a status symbol
&lt;/h2&gt;

&lt;p&gt;Here's the thing that bugs me about most "how I use AI" posts. They read like a flex. "I built an orchestrator that spawns a planner, which hands off to a coder, which pings a reviewer, which triggers a tester." Eight agents, a diagram with lots of arrows, and the unspoken message: look how sophisticated my setup is.&lt;/p&gt;

&lt;p&gt;And honestly, some of it &lt;em&gt;would&lt;/em&gt; work fine — if the coordination between the pieces were as good as the diagram implies. But that's exactly the part nobody shows you. The arrows in the diagram are doing an enormous amount of invisible work. In practice, an agent that pings another agent — "are the tasks ready yet?", "go run the tests now" — is a coordination protocol you now own, built out of the least reliable component you have: one model trying to communicate intent to another model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The number of agents in your system is not a measure of its sophistication. It's a measure of how many places your context can fracture.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What a second agent actually costs
&lt;/h2&gt;

&lt;p&gt;Let's talk about the bill, because it's the part the impressive diagrams leave out. Anthropic published numbers from their own multi-agent research system, and they're worth sitting with: a single agent burns roughly &lt;strong&gt;4× the tokens&lt;/strong&gt; of a normal chat, and a multi-agent system burns about &lt;strong&gt;15×&lt;/strong&gt;. That's not a rounding error. That's your token budget, tripled and tripled again, in exchange for coordination overhead.&lt;/p&gt;

&lt;p&gt;Sometimes that trade is worth it. Anthropic's own finding is that multi-agent setups genuinely win at &lt;em&gt;breadth-first&lt;/em&gt; research — questions where you can fan out into many independent directions at once, each subagent exploring its own slice with its own context window, then compressing what it found. If the task is "find every board member across these 400 companies," parallel agents crush a single agent that has to plod through sequentially. That's a real, honest use case.&lt;/p&gt;

&lt;p&gt;But look closely at what makes it work: the subtasks are &lt;em&gt;independent&lt;/em&gt;. One agent's findings don't change what another agent should do. That's the property that makes parallelism safe. And it's exactly the property most of our work doesn't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coding is the wrong shape for a swarm
&lt;/h2&gt;

&lt;p&gt;Anthropic say it plainly, and it matches everything I see: most coding tasks involve &lt;em&gt;fewer&lt;/em&gt; truly parallelizable subtasks than research, and today's models aren't good at coordinating and delegating to each other in real time. Code is a web of dependencies. The change you make in one file constrains the change in the next. Split that across agents and you don't get parallelism — you get two agents making conflicting assumptions about the same system.&lt;/p&gt;

&lt;p&gt;The team behind Devin wrote a whole piece called "Don't Build Multi-Agents," and their core point is sharper than the cost argument. Every action an agent takes carries an &lt;em&gt;implicit decision&lt;/em&gt;. When you split work across parallel subagents, each one makes its own implicit decisions, and those decisions quietly conflict. Their example is perfect: ask two subagents to build a "Flappy Bird clone" in parallel, and one builds a Super Mario background while the other builds a bird that moves nothing like Flappy Bird. Now your final agent has to reconcile two subtly incompatible worlds. Nobody was wrong on purpose. The context just couldn't be shared thoroughly enough for them to agree.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Actions carry implicit decisions, and conflicting decisions carry bad results.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's why even Claude Code, when it spawns a subtask, usually only asks it to &lt;em&gt;answer a question&lt;/em&gt; — never to go off and write code in parallel. The subagent investigates, reports back, and the main agent keeps making the actual decisions in one continuous line of context. It's a deliberately simple design, and the simplicity is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default should be one continuous context
&lt;/h2&gt;

&lt;p&gt;Strip away the fashion and the reliable pattern underneath is almost boringly plain: a single agent, working in one continuous context, making decisions in sequence. It sees everything it did earlier because it never handed off. There's no telephone game, no reconciliation step, no second model guessing at the first one's intent.&lt;/p&gt;

&lt;p&gt;Yes, you eventually hit a ceiling — a task so large the context window overflows. But the answer to that is rarely "spawn a committee." It's better context management: summarizing completed phases, storing decisions in memory, spinning up a &lt;em&gt;fresh&lt;/em&gt; context with a clean handoff when you truly need one. You reach for that when the single agent stops fitting, not before — and even then you're managing context, not orchestrating a crowd.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real lever isn't more agents
&lt;/h2&gt;

&lt;p&gt;Here's what actually moved the needle for me, and it wasn't a second agent. It was teaching the &lt;em&gt;one&lt;/em&gt; agent my workflow properly and giving it the right tools. I wired in the MCP servers it needs — reading Jira, Confluence, docs, Postman — so it works with real context instead of guessing. And I invested the months it took to teach it how I actually work, step by step.&lt;/p&gt;

&lt;p&gt;The payoff is that I can now start a fresh session and type "take task 123 and do it as usual," and it knows what that means: assign the ticket to me, move it to In Progress, pull it into the sprint if it's sitting in the backlog, estimate it if it has no points, branch off an updated main by our convention, commit with human-readable messages, open the PR with a real description and the right people tagged, link it to the ticket, and watch the pipeline — fixing it if it goes red. One agent. One phrase. No orchestra.&lt;/p&gt;

&lt;p&gt;That's a whole article of its own — &lt;a href="https://ma-x.im/blog/agent-playbook-do-it-as-usual" rel="noopener noreferrer"&gt;how I taught one agent to "do it as usual"&lt;/a&gt; — and it's the honest alternative to the swarm. The effort I &lt;em&gt;didn't&lt;/em&gt; spend maintaining seven setups, I spent deepening one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;The multi-agent orchestra is seductive because it looks like engineering. Boxes, arrows, roles, a system. But most of the time it's complexity cosplaying as capability — three times the token bill, a coordination protocol built on the flakiest possible substrate, and results that are &lt;em&gt;worse&lt;/em&gt; because the context keeps fracturing between the players.&lt;/p&gt;

&lt;p&gt;So my rule is simple: reach for the second agent only when you can name the work it's doing — work that is genuinely independent, genuinely parallel, and genuinely worth 15× the tokens. Research that fans out? Maybe. Almost everything else? One agent, one context, taught well. The sophistication was never in the number of agents. It was always in how much of your actual work you managed to hand off cleanly to a single one.&lt;/p&gt;

&lt;p&gt;If you're running a multi-agent setup right now, do one honest audit: for each agent past the first, write down the independent, parallel task it owns and what it costs you. If you can't name that task without hand-waving, you don't have an architecture — you have a diagram. Tell me which agent you'd delete first.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>architecture</category>
      <category>theagentplaybook</category>
    </item>
    <item>
      <title>Code-Splitting Is a Boundary Decision, Not a Bundle Trick</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/code-splitting-is-a-boundary-decision-not-a-bundle-trick-3l24</link>
      <guid>https://dev.to/maximusft/code-splitting-is-a-boundary-decision-not-a-bundle-trick-3l24</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswli0cucn6gj874j9e51.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fswli0cucn6gj874j9e51.png" alt="Code-Splitting Is a Boundary Decision, Not a Bundle Trick" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ma-x.im/blog/react-playbook-command-palette" rel="noopener noreferrer"&gt;command palette article&lt;/a&gt; closed by promising the last piece of this look-and-feel stretch, and it's the one that looks the most like a solved problem: code-splitting. Wrap a component in &lt;code&gt;React.lazy&lt;/code&gt;, drop a &lt;code&gt;Suspense&lt;/code&gt; boundary around it, watch the bundle shrink. Done, right?&lt;/p&gt;

&lt;p&gt;Here's the thing that bugs me about most bundle-size advice. It's all about the &lt;em&gt;how&lt;/em&gt; — &lt;code&gt;lazy&lt;/code&gt;, dynamic &lt;code&gt;import()&lt;/code&gt;, the router's lazy routes — and almost none of it is about the &lt;em&gt;where&lt;/em&gt;. And the where is the entire problem. Splitting code is one line. Deciding what to cut, so the app loads fast without shattering into a hundred chunks you can't reason about, is an architecture decision. Get the how right and the where wrong, and you don't get a faster app. You get a slower one with more moving parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A split is not free
&lt;/h2&gt;

&lt;p&gt;Start with what a split actually costs, because the mental model everyone skips is that lazy-loading isn't "free performance." Every split you introduce is a boundary, and every boundary buys you three things you didn't have before: a separate network request, a loading state you now have to design, and the risk of a waterfall if that request can't start until another one finishes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A code-split isn't a size optimization. It's a boundary you're adding to the app — and boundaries have a cost on both sides.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the real question stops being "can I split this?" (you almost always can) and becomes "is there a real seam here worth paying a boundary for?" That reframe is the whole article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default that quietly backfires
&lt;/h2&gt;

&lt;p&gt;The advice you'll read first is "split at the route level," and honestly, that advice is correct. Route boundaries are the best seams in the entire app: the user is already waiting for a navigation, they don't expect the next screen to be instant, and each route pulls in a genuinely different slice of the app. With a type-safe router like the one from the &lt;a href="https://ma-x.im/blog/react-playbook-tanstack-router" rel="noopener noreferrer"&gt;routing article&lt;/a&gt;, lazy routes are the natural unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// each route is its own chunk — the user is already navigating, a boundary here is invisible&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFileRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lazyRouteComponent&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ReportsPage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where it backfires is the next step people take. Route-level splitting works, so they conclude "more splitting is more better" and start wrapping individual components — a &lt;code&gt;UserAvatar&lt;/code&gt; here, a &lt;code&gt;Badge&lt;/code&gt; there — in &lt;code&gt;lazy&lt;/code&gt;. Now every one of those is a network round-trip and a spinner. You've turned a single fast download into a cascade of tiny requests, each with its own loading flicker, and the page assembles itself in front of the user like a slideshow. That's not a faster app. That's the same bytes, delivered worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the seams actually are
&lt;/h2&gt;

&lt;p&gt;So if "everywhere" is wrong and "routes" is the safe default, what else genuinely earns a split? Three shapes, and they all have the same property: the code is &lt;em&gt;heavy&lt;/em&gt; and &lt;em&gt;not needed for the first meaningful paint&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heavy, rarely-used features.&lt;/strong&gt; A rich text editor, a video player, a PDF viewer, the &lt;a href="https://ma-x.im/blog/react-playbook-chart-libraries" rel="noopener noreferrer"&gt;chart library&lt;/a&gt; you only render on the analytics tab. These can each be hundreds of kilobytes, and most sessions never touch them. Splitting here is pure win — the cost of the boundary is paid only by the users who actually open the feature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the editor is 300kb of the bundle and only the 5% who click "edit" ever need it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RichTextEditor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./RichTextEditor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;isEditing&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;isEditing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isEditing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NotePreview&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EditorSkeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RichTextEditor&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Suspense&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Behind an interaction.&lt;/strong&gt; Anything that only appears after a click — a settings modal, a command palette, an export dialog. The boundary hides behind the click the user already made, so the tiny load feels like part of the action, not a delay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Below the fold, and truly optional.&lt;/strong&gt; The stuff a user might scroll to but often won't.&lt;/p&gt;

&lt;p&gt;Notice what's &lt;em&gt;not&lt;/em&gt; on this list: anything needed for the first render, anything small, and anything that's split purely because it lives in its own folder. A 4kb component doesn't deserve a network request. The download of that request costs more than the bytes you saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The waterfall you'll create by accident
&lt;/h2&gt;

&lt;p&gt;The nastiest failure mode isn't too many chunks — it's chunks that can't load in parallel. This is the same waterfall problem the &lt;a href="https://ma-x.im/blog/react-playbook-data-fetching" rel="noopener noreferrer"&gt;data-fetching article&lt;/a&gt; warned about, just moved from data to code. A lazy component that, once loaded, immediately lazy-loads its own child, which lazy-loads &lt;em&gt;its&lt;/em&gt; child — the browser can't start request two until request one lands. You've serialized what should have been parallel.&lt;/p&gt;

&lt;p&gt;The fix is intent-based prefetching: start fetching the chunk when the user &lt;em&gt;signals&lt;/em&gt; they're heading somewhere, not when they arrive. Hovering a link, focusing a button, opening the parent — all of these are the browser's chance to fetch ahead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// kick off the chunk on hover, so by the time they click it's already in cache&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/reports"&lt;/span&gt;
  &lt;span class="na"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ReportsPage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Reports
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this and the boundary all but disappears: the request overlaps with the user's own reaction time instead of stacking behind another request. Good routers do a version of this for you. The point is that &lt;em&gt;when&lt;/em&gt; a chunk starts loading matters as much as whether you split it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chunk you can't reason about
&lt;/h2&gt;

&lt;p&gt;Here's where splitting collides with the rest of the Playbook. Remember the &lt;a href="https://ma-x.im/blog/react-playbook-barrel-files" rel="noopener noreferrer"&gt;barrel-file article&lt;/a&gt; — the &lt;code&gt;index.ts&lt;/code&gt; that re-exports everything in a folder? Lazy-load a single component &lt;em&gt;through&lt;/em&gt; a barrel, and you don't get that component. You get the whole barrel: every sibling it re-exports, dragged into the chunk because the import graph can't tell them apart. Your carefully-placed &lt;code&gt;lazy&lt;/code&gt; boundary quietly pulls in half the folder, and the chunk you thought was small isn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// looks like one small component...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./widgets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Chart&lt;/span&gt; &lt;span class="p"&gt;})));&lt;/span&gt;
&lt;span class="c1"&gt;// ...but ./widgets/index.ts re-exports Chart, Table, Map, Calendar — all now in this chunk&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why I keep coming back to the idea that these topics aren't separate. A split is only as clean as the &lt;a href="https://ma-x.im/blog/react-playbook-module-dependencies" rel="noopener noreferrer"&gt;module boundaries&lt;/a&gt; underneath it. If your imports reach sideways through barrels and shared grab-bags, your chunks will be blurry no matter how many &lt;code&gt;lazy&lt;/code&gt; calls you sprinkle in. Import the file directly, and the split is exactly as big as you think it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split where it hurts, not where the folders are
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://ma-x.im/blog/react-playbook-rerenders-and-architecture" rel="noopener noreferrer"&gt;re-renders article&lt;/a&gt; made an argument I want to borrow directly: optimize where it actually hurts, and don't let the optimization wreck the structure. Code-splitting is the same discipline one layer down. The temptation is to split on the shape of your file tree — one chunk per feature folder, because it's tidy. But the file tree is your convenience, not the user's journey. The seams that matter are the ones in &lt;em&gt;their&lt;/em&gt; experience: the route they navigate to, the heavy tool they occasionally open, the dialog behind a click.&lt;/p&gt;

&lt;p&gt;Before adding a &lt;code&gt;lazy&lt;/code&gt;, I ask two questions. Is this code actually heavy enough that its bytes show up in the load? And is there a real moment in the user's flow where paying for a separate request is invisible? If both answers aren't yes, the split is making the app more complex for a number that doesn't move.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;Code-splitting reads like a bundle trick and behaves like an architecture decision — which makes it a fitting end to this look-and-feel cluster, because so did theming, and so did the command palette. Each one looks like polish and turns out to be a boundary in disguise. The visible artifact is trivial: a smaller bundle, a color toggle, a search modal. The value, every time, is in where you draw the line and whether the rest of the structure lets that line stay clean.&lt;/p&gt;

&lt;p&gt;So the honest heuristic isn't "split more." It's split &lt;em&gt;rarely and deliberately&lt;/em&gt;, at the seams the user can feel, over module boundaries clean enough that a chunk means what its name says. Everything else is just trading one problem you can see — a big bundle — for a dozen you can't.&lt;/p&gt;

&lt;p&gt;If you've got a build with more than a handful of chunks, pull up your bundle analyzer and find the smallest lazy chunk you have. If it's a few kilobytes, that's a boundary you're paying for and getting nothing back — tell me what it is, because I'd bet it snuck in on the "more splitting is better" reflex, and it's the first thing I'd delete.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>performance</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>The Command Palette Is an Architecture, Not a Widget</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sun, 02 Aug 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/the-command-palette-is-an-architecture-not-a-widget-5697</link>
      <guid>https://dev.to/maximusft/the-command-palette-is-an-architecture-not-a-widget-5697</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzkovf7zw8cia0bc2ia1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzkovf7zw8cia0bc2ia1.png" alt="The Command Palette Is an Architecture, Not a Widget" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ma-x.im/blog/react-playbook-theming-dark-mode" rel="noopener noreferrer"&gt;theming article&lt;/a&gt; ended on a move from how an app looks to how power users drive it. The command palette — the &lt;code&gt;Cmd+K&lt;/code&gt; menu that fuzzy-searches everything you can do — is the poster child for a piece of UI that people build as a widget and should build as an architecture. The difference shows up fast, and it's the difference between a palette that stays useful and one that quietly lies about what the app can do.&lt;/p&gt;

&lt;p&gt;Here's the trap. You install a nice palette component, drop a modal in, and fill it with a hardcoded list of actions: "New file," "Toggle dark mode," "Go to settings." It demos beautifully. Then someone adds a feature with its own toolbar button, forgets to also add it to the palette, and now the palette is missing things. Someone renames an action in a menu but not in the palette. Six months in, the palette is a stale, hand-maintained duplicate of logic that already exists elsewhere in the app. The modal was never the hard part. The hard part is that the palette needs to know everything the app can do — and so do the menus, and the keyboard shortcuts, and the toolbar.&lt;/p&gt;

&lt;h2&gt;
  
  
  A command is a first-class object
&lt;/h2&gt;

&lt;p&gt;The reframe is to stop thinking about a search modal and start thinking about &lt;em&gt;commands&lt;/em&gt; as data. A command is a real object: an id, a title, some keywords, the thing it does, and optionally whether it's currently allowed and what shortcut triggers it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// for fuzzy search: "theme", "appearance"...&lt;/span&gt;
  &lt;span class="nl"&gt;group&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "File", "View", "Navigation"&lt;/span&gt;
  &lt;span class="nl"&gt;shortcut&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "mod+k"&lt;/span&gt;
  &lt;span class="nl"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// context-aware availability&lt;/span&gt;
  &lt;span class="nl"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the actual behavior&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a command is data, the interesting shift happens: the palette stops &lt;em&gt;owning&lt;/em&gt; actions and starts &lt;em&gt;rendering&lt;/em&gt; them. This is the same instinct as &lt;a href="https://ma-x.im/blog/react-playbook-headless-components" rel="noopener noreferrer"&gt;headless components&lt;/a&gt; — separate the behavior (what the command does) from the presentation (how it's listed) — and the same instinct as the &lt;a href="https://ma-x.im/blog/react-playbook-analytics-as-a-layer" rel="noopener noreferrer"&gt;typed event catalog&lt;/a&gt; — one declared source of truth instead of scattered strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The registry is the single source of truth
&lt;/h2&gt;

&lt;p&gt;The center of the whole design is a registry: one place every command is registered, and every surface that needs to know "what can the app do?" reads from it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandRegistry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// features clean up their own&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isEnabled&lt;/span&gt;&lt;span class="p"&gt;?.()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CommandRegistry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now features &lt;em&gt;contribute&lt;/em&gt; commands instead of the palette knowing about features:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// in the editor feature — it announces what it can do, and to whom it doesn't care&lt;/span&gt;
&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file.new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New File&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;File&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;shortcut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mod+n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createFile&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the direction of the dependency. The editor feature depends on the registry (a stable, shared thing); the palette depends on the registry; the two features never depend on each other. That's the &lt;a href="https://ma-x.im/blog/react-playbook-module-dependencies" rel="noopener noreferrer"&gt;dependencies-point-the-right-way&lt;/a&gt; rule holding: everything points at a shared abstraction, nothing reaches sideways. Add a feature, it registers its commands, and the palette shows them with zero changes to the palette. Remove the feature, its cleanup runs, the commands vanish. Nothing drifts because nothing was duplicated.&lt;/p&gt;

&lt;h2&gt;
  
  
  One dispatcher, not scattered key handlers
&lt;/h2&gt;

&lt;p&gt;The keyboard shortcuts fall out of the same registry, and this is where you avoid another mess. The naive approach sprinkles &lt;code&gt;onKeyDown&lt;/code&gt; handlers across a dozen components, each checking for its own combo — the same scatter problem the &lt;a href="https://ma-x.im/blog/react-playbook-event-bus" rel="noopener noreferrer"&gt;event bus&lt;/a&gt; and analytics articles warned about, just with keystrokes. Instead, one global listener resolves keys against the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onKeyDown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;combo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toCombo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "mod+n", "mod+k", etc.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shortcut&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;combo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onKeyDown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onKeyDown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One place owns the mapping from keys to commands. Shortcut conflicts become detectable (two commands claiming &lt;code&gt;mod+k&lt;/code&gt; is now a thing you can check for, instead of a mystery). And because &lt;code&gt;isEnabled&lt;/code&gt; gates &lt;code&gt;all()&lt;/code&gt;, a shortcut simply does nothing when its command isn't valid in the current context — no scattered guard clauses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The palette is just a view over the registry
&lt;/h2&gt;

&lt;p&gt;And now the part everyone thought was the whole feature is the easy part. The palette is a thin view: read the registry, fuzzy-match the query against titles and keywords, render the list, run the chosen command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CommandPalette&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;fuzzyFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// search titles + keywords&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Type a command…"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shortcut&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;kbd&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shortcut&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;kbd&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Dialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because commands are data, you get things for free that would be painful to hand-maintain: search across everything, a "keyboard shortcuts" help screen (just render the registry grouped by &lt;code&gt;group&lt;/code&gt;), and an honest answer to "what can I do here?" that's always current because it's generated, never authored twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;The command palette is a systems problem wearing a UI costume — the same shape as theming. The visible artifact (a search modal, a color toggle) is trivial; the value is entirely in the architecture behind it (a command registry, a token system). Build either one as a widget and it rots, because you end up maintaining the same truth in several places and they drift apart. Build it as a single source of truth that many surfaces read from, and the palette, the menus, and the shortcuts can never disagree — because there's only one thing for them to agree with.&lt;/p&gt;

&lt;p&gt;That's the through-line of this whole look-and-feel stretch: the parts of an app that seem like polish are usually boundaries in disguise, and the teams that treat them as boundaries end up with UI that scales instead of UI that quietly lies. Next the series turns to the last piece of this cluster — code-splitting, and the deceptively hard question of what to actually cut so the app loads fast without fracturing into a thousand lazy chunks you can't reason about.&lt;/p&gt;

&lt;p&gt;If your app has a command palette, open it and then open your main menu side by side. If they don't list the same capabilities, you've got two sources of truth drifting apart — tell me how far they've diverged, because that gap is the whole argument for a registry.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Theming and Dark Mode Without the Flash: A Systems Problem in CSS Clothing</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/theming-and-dark-mode-without-the-flash-a-systems-problem-in-css-clothing-59id</link>
      <guid>https://dev.to/maximusft/theming-and-dark-mode-without-the-flash-a-systems-problem-in-css-clothing-59id</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk5vrh3cbmoy9lsgn1xse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk5vrh3cbmoy9lsgn1xse.png" alt="Theming and Dark Mode Without the Flash: A Systems Problem in CSS Clothing" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ma-x.im/blog/react-playbook-rerenders-and-architecture" rel="noopener noreferrer"&gt;last article&lt;/a&gt; argued that a lot of React "optimization" is really architecture damage in disguise. Theming is a great place to keep that lens on, because it's the feature that looks the most like a trivial CSS toggle and is actually a small systems-design problem — one where the naive version fails in three specific, visible ways.&lt;/p&gt;

&lt;p&gt;You start simple. A &lt;code&gt;dark&lt;/code&gt; class on the body, some overrides, a bit of React state to flip it. Ship it. Then the reports come in. The page flashes white before going dark on every load. A few components didn't get the memo and stayed light. And your theme toggle mysteriously re-renders half the app. None of these is a CSS bug exactly — they're all consequences of putting theming in the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens are the foundation, not the paint
&lt;/h2&gt;

&lt;p&gt;The first mistake is letting every component decide its own colors. &lt;code&gt;color: #1a1a1a&lt;/code&gt; here, &lt;code&gt;background: #fff&lt;/code&gt; there, a slightly different grey in a third place. Now "dark mode" means hunting down every hardcoded color in the codebase, and "the greys drifted" is inevitable because there was never one grey to begin with.&lt;/p&gt;

&lt;p&gt;The fix is design tokens expressed as CSS custom properties: one place that owns the actual color decisions, and components that only ever refer to them &lt;em&gt;semantically&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* primitive palette — raw values, referenced by nobody directly */&lt;/span&gt;
  &lt;span class="py"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f9fafb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--gray-900&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111827&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--green-500&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#22c55e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* semantic tokens — what components actually use */&lt;/span&gt;
  &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--green-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;'dark'&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-900&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gray-50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--green-500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* accent survives the theme */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two-layer split matters more than it looks. Primitives (&lt;code&gt;--gray-900&lt;/code&gt;) are the paint box; semantic tokens (&lt;code&gt;--color-surface&lt;/code&gt;) are the meaning. Components consume only the semantic layer — &lt;code&gt;background: var(--color-surface)&lt;/code&gt; — so they never know or care what actual color that is in a given theme. Switching themes is now just swapping what the semantic tokens point to. And critically, a component that says &lt;code&gt;var(--color-surface)&lt;/code&gt; cannot drift, because there's exactly one definition of surface per theme. This is the same "one source of truth, referenced by many" idea from &lt;a href="https://ma-x.im/blog/react-playbook-data-normalization" rel="noopener noreferrer"&gt;data normalization&lt;/a&gt;, applied to color.&lt;/p&gt;

&lt;h2&gt;
  
  
  The switch doesn't belong in React state
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprises people, and it ties straight back to the last article. Flipping the theme should not be a React re-render at all. You change one attribute on the root element — &lt;code&gt;data-theme="dark"&lt;/code&gt; — and the CSS cascade does the entire rest of the work, instantly, for every element on the page, with zero components re-rendering.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// persist, as a side effect&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contrast that with the common mistake: putting the theme in a React context that wraps the whole app, so that toggling it re-renders every consumer to pass down new colors. That's taking a job CSS does for free — cascading a variable change to the whole tree — and turning it into a render storm, the exact anti-pattern from the &lt;a href="https://ma-x.im/blog/react-playbook-rerenders-and-architecture" rel="noopener noreferrer"&gt;re-render article&lt;/a&gt;. Let CSS variables do what they're built for. You can still keep a &lt;em&gt;small&lt;/em&gt; piece of React state for the toggle's UI (which icon to show), but the actual theming mechanism is a DOM attribute and a cascade, not a re-render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flash is a rendering-order problem
&lt;/h2&gt;

&lt;p&gt;Now the ugliest one: the flash of the wrong theme (FOUC). On a statically exported, server-rendered site — which is &lt;a href="https://ma-x.im/blog/nextjs-ssg-github-pages" rel="noopener noreferrer"&gt;exactly this stack&lt;/a&gt; — the server has no idea what theme the user picked. It renders the default, ships the HTML, the browser paints it, and only &lt;em&gt;then&lt;/em&gt; does your React code boot, read localStorage, and correct the theme. That gap between first paint and correction is the flash, and no amount of React can remove it, because React runs too late by definition.&lt;/p&gt;

&lt;p&gt;The fix has to happen &lt;em&gt;before&lt;/em&gt; first paint, which means a tiny blocking script in the document &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, before any content renders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// injected into &amp;lt;head&amp;gt;, runs synchronously before the body paints&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;themeScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
  (function () {
    try {
      var stored = localStorage.getItem('theme');
      var theme = stored || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
      document.documentElement.setAttribute('data-theme', theme);
    } catch (e) {}
  })();
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// in the App Router root layout&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;suppressHydrationWarning&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;themeScript&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;head&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It feels a little dirty to hand-inject a script, but it's the correct tool: it sets &lt;code&gt;data-theme&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; synchronously, before the browser paints a single pixel, so the very first frame is already the right theme. This is also the disciplined version of the &lt;a href="https://ma-x.im/blog/react-playbook-local-storage-and-state" rel="noopener noreferrer"&gt;localStorage lesson&lt;/a&gt; — read persisted state in the right place at the right time, not during React's render where it causes hydration mismatches. Note &lt;code&gt;suppressHydrationWarning&lt;/code&gt; on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;: the attribute the script sets won't match the server's markup, and that's expected, so we tell React not to complain about this one specific, intentional divergence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Respect the system, but let people override
&lt;/h2&gt;

&lt;p&gt;A complete theming system has three inputs, in priority order: the user's explicit choice, then the OS preference, then a default. &lt;code&gt;prefers-color-scheme&lt;/code&gt; gives you the OS signal, and &lt;code&gt;matchMedia&lt;/code&gt; lets you react to it changing live (someone flips their laptop to dark at sunset):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// only follow the system if the user hasn't set an explicit preference&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;applyTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule that keeps this sane: an explicit user choice always wins and always persists; the system preference is only the fallback when the user hasn't decided. Conflating those two — following the OS even after the user picked something — is how you get the maddening bug where a user's chosen theme keeps getting overridden.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;Theming teaches the same lesson this stretch of the series keeps circling: the thing that looks like a surface detail is usually a boundary decision. Where do color decisions live? (In tokens, once.) What actually performs the switch? (A DOM attribute and the cascade, not React.) When does the theme get resolved? (Before first paint, not after hydration.) Get those three boundaries right and dark mode is almost anticlimactic — a few dozen lines, no flash, no drift, no render storm. Get them wrong and you're chasing flashes and mismatched components forever, patching symptoms of a structure that put theming in the wrong place.&lt;/p&gt;

&lt;p&gt;Next, the series stays on architectural UI but moves from how the app &lt;em&gt;looks&lt;/em&gt; to how power users &lt;em&gt;drive&lt;/em&gt; it. The command palette — that &lt;code&gt;Cmd+K&lt;/code&gt; menu — looks like a search modal and is really a command registry: one source of truth for everything the app can do, feeding the palette, the menus, and the keyboard shortcuts at once. That's where we go.&lt;/p&gt;

&lt;p&gt;If your app has dark mode, do the two-second test: hard-refresh it a dozen times on a throttled connection and watch the first frame. If you catch a flash of the wrong theme, your theming is resolving too late — and I'd love to hear whether it was the flash, the drift, or the render storm that got you first.&lt;/p&gt;

</description>
      <category>react</category>
      <category>css</category>
      <category>architecture</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Re-Renders Are React's Superpower, Not a Disease to Cure</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/re-renders-are-reacts-superpower-not-a-disease-to-cure-b24</link>
      <guid>https://dev.to/maximusft/re-renders-are-reacts-superpower-not-a-disease-to-cure-b24</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9imdv8c917ftvwhu24e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9imdv8c917ftvwhu24e.png" alt="Re-Renders Are React's Superpower, Not a Disease to Cure" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was going to move the series straight into how an app looks and feels. But something has been bugging me for a while, and it's architectural enough that skipping it would be dishonest. So this is a short detour, and it's about a panic I keep seeing: someone opens the React Profiler, watches a component re-render when they didn't expect it to, and immediately starts demolishing their code to make the re-render stop. Props get removed. Components get split in strange places. &lt;code&gt;memo&lt;/code&gt; gets sprayed over everything. All to drive a number down.&lt;/p&gt;

&lt;p&gt;Here's the thing I want to say plainly, because almost nobody says it: &lt;strong&gt;re-rendering is not React's weakness. It's React's superpower.&lt;/strong&gt; And a lot of the "optimizations" people perform to avoid re-renders are quietly worse for the codebase than the re-renders ever were.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rendering &lt;em&gt;is&lt;/em&gt; the model
&lt;/h2&gt;

&lt;p&gt;Step back and remember what React actually is. Your UI is a function of state. State changes, React re-runs the function, produces a new description of the UI, and reconciles it against the last one. That re-run — the render — is not an accident or an overhead bolted onto React. It &lt;em&gt;is&lt;/em&gt; React. The entire programming model, the reason React felt like a relief after manual DOM manipulation, is that you get to describe "what the UI looks like for this state" and never wire up the updates yourself.&lt;/p&gt;

&lt;p&gt;Compare it to a signals-based library — Solid, or the fine-grained-reactivity family. There the model is different by design:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Solid-style: fine-grained. The component function runs ONCE.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createSignal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// this text node subscribes to count directly; only IT updates&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;// React: the component function re-runs on every state change.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// the whole function runs again; React diffs the result&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are legitimate architectures. Signals update surgically and never re-run the component; React re-runs the component and diffs. But notice: the React version's "re-run everything and figure out what changed" is the very thing that makes React's mental model so simple. You don't track dependencies by hand. You don't wire subscriptions. You describe UI-as-a-function-of-state and let the render do the work. Treating that render as a defect is like buying a bird and being annoyed that it keeps flying. The flying is the point.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A re-render is not React doing something wrong. It's React doing the one thing it exists to do. The goal was never zero renders — it was the freedom to stop thinking about updates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  So what's actually bad, then?
&lt;/h2&gt;

&lt;p&gt;I'm not saying render count never matters. It does — but the problem is narrower than the panic suggests. A bad situation is a &lt;em&gt;render storm at scale&lt;/em&gt;: every mousemove re-rendering every component on the page, a giant list re-rendering all thousand rows because one row's checkbox changed, an expensive tree re-running on every keystroke in an unrelated input. That's real, and it's worth fixing.&lt;/p&gt;

&lt;p&gt;But a component re-rendering because its parent re-rendered, doing a bit of cheap work and producing the same DOM? That's usually nothing. React is &lt;em&gt;fast&lt;/em&gt; at this. The reconciler is built for it. Most of the re-renders people agonize over cost less than the &lt;code&gt;useMemo&lt;/code&gt; they add to prevent them. The panic treats every render as a cost when the real question is only ever "is this render actually expensive, and does it actually happen often enough to matter?"&lt;/p&gt;

&lt;p&gt;Which is why the word matters so much. People say they want to &lt;em&gt;minimize&lt;/em&gt; re-renders. The word you actually want is &lt;strong&gt;optimize&lt;/strong&gt;. Minimize is a religion — zero renders at any cost. Optimize means find the balance: spend effort on the renders that hurt, and leave the cheap ones alone. Minimizing is something you do to a number. Optimizing is something you do to a system, with judgment about where the cost really is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade nobody prices: architecture for render count
&lt;/h2&gt;

&lt;p&gt;Here's where this becomes an architecture article and not a performance tip, because the real damage isn't the wasted effort — it's what people &lt;em&gt;break&lt;/em&gt; to save a render. Let me give you a few shapes I keep seeing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contorting to remove a "guilty" prop.&lt;/strong&gt; A component re-renders because a prop changes. So instead of leaving the honest data flow in place, someone restructures so the component no longer &lt;em&gt;receives&lt;/em&gt; that prop — it reaches into a store or context to grab the value itself, so the parent can't "trigger" it. The render is gone. But now the component's real dependency is hidden. It looks independent and isn't. You've taken a visible prop — a traceable edge in your dependency graph — and turned it into an invisible reach-out, the exact anti-pattern the &lt;a href="https://ma-x.im/blog/react-playbook-composition-and-di" rel="noopener noreferrer"&gt;composition and DI article&lt;/a&gt; argued against. You optimized a render and de-optimized your ability to understand the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memo everywhere as a reflex.&lt;/strong&gt; Every component wrapped in &lt;code&gt;memo&lt;/code&gt;, every value in &lt;code&gt;useMemo&lt;/code&gt;, every function in &lt;code&gt;useCallback&lt;/code&gt; — not because anything was measured, but because renders are scary now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// this "optimization" costs more than it saves&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSelect&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;RowProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;paddingLeft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a trivial row, the memoization machinery — the comparison, the dependency arrays, the mental overhead — is more expensive than just rendering the div. And every one of those dependency arrays is now a thing that can go subtly wrong and a thing the next person has to maintain. You bought a micro-optimization with a permanent tax on changeability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Splitting on render boundaries instead of responsibility boundaries.&lt;/strong&gt; This is the worst one, because it looks like good decomposition. To isolate a re-render, someone fractures a cohesive component into oddly-shaped pieces — not because those pieces are meaningful units, but because the split happens to stop a render from propagating. Now a single logical concern is smeared across three components that only make sense together, which is precisely the &lt;a href="https://ma-x.im/blog/smeared-component" rel="noopener noreferrer"&gt;smeared-component&lt;/a&gt; problem wearing a performance badge. You decomposed for the profiler, not for the reader.&lt;/p&gt;

&lt;p&gt;In each case the pattern is identical: a local win on render count, paid for with a structural loss that the profiler will never show you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part everyone forgets: the component has to be &lt;em&gt;maintained&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;The fixation on renders has tunnel vision. It optimizes for one moment — the render — and forgets that this component has a whole life ahead of it. It's going to grow. It's going to be reused somewhere the "clever" structure doesn't fit. Someone's going to need to change it in six months. The "ideal" low-render component is very often a maintenance liability, because its shape was dictated by a performance metric instead of by what the thing actually &lt;em&gt;is&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This is where the balance genuinely lives, and it's context-dependent. Is this a marketing landing page? Then honestly, do nothing — render "waste" on a page that mounts once and barely updates is a non-problem, and every hour spent memoizing it is wasted. But a large, high-load, long-lived application — the kind with a real &lt;a href="https://ma-x.im/blog/react-playbook-container-presentational" rel="noopener noreferrer"&gt;model / view-model / view separation&lt;/a&gt;, data fetching, aggregation, and preparation layers — &lt;em&gt;that's&lt;/em&gt; where the trade-off between architecture and render performance is a real engineering decision. And the answer there is deliberate: you optimize the hot paths you've measured, and you refuse to sacrifice the structure that keeps the app maintainable for renders that don't hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screw you can't reach
&lt;/h2&gt;

&lt;p&gt;I was reassembling my daughter's computer recently and it made this click for me. Ten screws on the back, off comes the panel, and everything inside is modular — the drive, the memory, the fan all lift out independently. Fifteen minutes to clean it. I remembered the old laptops I used to take apart years ago, where you had to remove things in one exact sequence, and if you wanted to reach one component you first had to dismantle four unrelated ones sitting on top of it. Miss the order and you're stuck.&lt;/p&gt;

&lt;p&gt;Bad render-driven architecture is the old laptop. To save a render you bury a "screw" — you tuck a dependency somewhere awkward, you weld two concerns together, you hide a prop — and later, when you need to change that part, you can't get to it without taking apart everything piled on top. Good architecture is my daughter's computer: parts have obvious places, and when you want to move, replace, or fix one, you just can. The whole reason we care about structure is that day &lt;em&gt;later&lt;/em&gt;, when requirements change and you need to reach in cleanly.&lt;/p&gt;

&lt;p&gt;That serviceability is worth far more than a render count, and it's exactly what people trade away when the profiler scares them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I land
&lt;/h2&gt;

&lt;p&gt;So my actual rule is unglamorous. Treat re-rendering as the normal, healthy heartbeat of a React app, not a symptom. Measure before you optimize — find the renders that are genuinely expensive and genuinely frequent, and fix &lt;em&gt;those&lt;/em&gt;, with the smallest intervention that works. And never, ever trade the reachability of your architecture for a number in a profiler, because one of those you can feel every day you maintain the code, and the other you mostly imagined.&lt;/p&gt;

&lt;p&gt;Renders are React's superpower. Point it where it helps, tune it where it hurts, and stop apologizing for the thing that makes the whole model work.&lt;/p&gt;

&lt;p&gt;With that off my chest, the series gets back on track — into how an app &lt;em&gt;looks and feels&lt;/em&gt; at an architectural level, starting with the one that looks like a CSS detail and is really a systems problem: theming and dark mode, done so it doesn't flash on load, doesn't drift across components, and doesn't fight server rendering. That's next.&lt;/p&gt;

&lt;p&gt;If you've got a component in your app that was reshaped purely to cut a re-render, go look at it now and ask an honest question: was the render actually expensive, or did you just make the code harder to change to satisfy a profiler? Tell me what you find — I suspect the ratio surprises people.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>performance</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Analytics Is Architecture: Stop Sprinkling track() Everywhere</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Mon, 27 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/analytics-is-architecture-stop-sprinkling-track-everywhere-3nol</link>
      <guid>https://dev.to/maximusft/analytics-is-architecture-stop-sprinkling-track-everywhere-3nol</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvy4c9pfr4m3yj2sz9jy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqvy4c9pfr4m3yj2sz9jy.png" alt="Analytics Is Architecture: Stop Sprinkling track() Everywhere" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This cluster has been about how the parts of an app communicate — &lt;a href="https://ma-x.im/blog/react-playbook-event-bus" rel="noopener noreferrer"&gt;event buses&lt;/a&gt; for ambient signals, &lt;a href="https://ma-x.im/blog/react-playbook-feature-flags" rel="noopener noreferrer"&gt;feature flags&lt;/a&gt; for toggling behavior. Both articles kept circling the same lesson: cross-cutting concerns want a typed contract and a single home, not a scatter of stringly-typed calls. Analytics is the purest example of that lesson, and the one almost everyone gets wrong first — myself included, years ago.&lt;/p&gt;

&lt;p&gt;Here's what analytics looks like in most codebases I've opened. A &lt;code&gt;track('button_click')&lt;/code&gt; in one component. An &lt;code&gt;analytics.logEvent('Button Clicked')&lt;/code&gt; in another. A raw &lt;code&gt;window.gtag('event', 'cta')&lt;/code&gt; inline in a third because someone was in a hurry. Event names that mean the same thing spelled three ways. Payloads where one place sends &lt;code&gt;userId&lt;/code&gt; and another sends &lt;code&gt;user_id&lt;/code&gt; and a third helpfully includes the user's &lt;em&gt;email&lt;/em&gt; into a third-party vendor. It works, in the sense that events show up somewhere. But it's un-auditable, un-typed, vendor-locked, and a privacy incident waiting to happen.&lt;/p&gt;

&lt;p&gt;That's not a tracking problem. That's an architecture problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics is a cross-cutting concern
&lt;/h2&gt;

&lt;p&gt;Analytics has the same shape as logging, i18n, and error reporting: it touches almost every part of the app, but it &lt;em&gt;belongs&lt;/em&gt; to none of them. And the &lt;a href="https://ma-x.im/blog/smeared-component" rel="noopener noreferrer"&gt;smeared-component article&lt;/a&gt; named exactly this failure mode — logic that should live in one layer instead gets spread thin across the whole UI until you can't change it in one place. A &lt;code&gt;track()&lt;/code&gt; call baked into a button's onClick is analytics logic living in the wrong layer. The button's job is to be a button. Knowing that clicking it should emit a "checkout started" event to three vendors is not the button's concern.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you can't answer "what does this app track, and where does that data go?" by opening one folder, your analytics isn't a layer — it's a rash.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix is the same as every other cross-cutting concern in this series: pull it into a dedicated layer with a clear, typed boundary, and let the rest of the app talk to that boundary instead of to a vendor SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  A typed event catalog
&lt;/h2&gt;

&lt;p&gt;Step one is to stop passing strings and start declaring events, once, with typed payloads. This is the same move that tamed the &lt;a href="https://ma-x.im/blog/react-playbook-event-bus" rel="noopener noreferrer"&gt;event bus&lt;/a&gt;: a catalog the compiler knows about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// the single source of truth for everything the app can track&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout_started&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cartValueCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;itemCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;checkout_completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;amountCents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_performed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;resultCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;team&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsEvents&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsEvents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;K&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;track('checkout_started', { cartValueCents: 4200, itemCount: 3 })&lt;/code&gt; autocompletes, the payload is type-checked, and a misspelled event name is a compile error instead of a silently-dropped data point six weeks of dashboards later. &lt;code&gt;AnalyticsEvents&lt;/code&gt; is the catalog — one file that answers "what does this product measure?" That question having a single, readable answer is worth more than any individual event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decouple your code from the vendor
&lt;/h2&gt;

&lt;p&gt;Step two is the part that pays off for years: your product code should emit &lt;em&gt;domain events&lt;/em&gt;, and a separate adapter should decide what to do with them. Product code says "checkout started." It does not know or care whether that goes to Amplitude, GA4, Segment, a data warehouse, or all four.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsSink&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// each vendor is an adapter — swappable, testable, isolated&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amplitudeSink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsSink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;amplitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// one subscriber wires the typed catalog to whatever sinks are configured&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initAnalytics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sinks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AnalyticsSink&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analyticsBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAny&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sink&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;sinks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;a href="https://ma-x.im/blog/react-playbook-composition-and-di" rel="noopener noreferrer"&gt;dependency injection&lt;/a&gt; applied to tracking: the product depends on an abstraction (&lt;code&gt;track&lt;/code&gt;), and the concrete vendor is injected at the edge. Swap Amplitude for Segment by changing one adapter — not by grep-and-replacing three hundred call sites. Add a second vendor without touching a single component. Run tests with a fake sink that just records calls, so you can assert "checkout_completed fired once with this payload" without any network. And notice this is a textbook &lt;em&gt;good&lt;/em&gt; use of the event bus from earlier in the cluster: the emitter (product code) genuinely should not know who's listening (the vendors), so the indirection is real decoupling, not hidden coupling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Governance is the whole point, not an afterthought
&lt;/h2&gt;

&lt;p&gt;Once analytics is a layer, the things that were impossible when it was scattered become easy — and these are the things that actually matter when the company grows up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Naming.&lt;/strong&gt; One catalog means one convention. No more &lt;code&gt;Button Clicked&lt;/code&gt; versus &lt;code&gt;button_click&lt;/code&gt; versus &lt;code&gt;btnClick&lt;/code&gt; for the same action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PII control.&lt;/strong&gt; A single choke point is where you enforce "no emails, no raw tokens, no free-text that might contain personal data." You can scrub, allowlist, or type-forbid sensitive fields in &lt;em&gt;one&lt;/em&gt; place. When legal asks "are we sending any personal data to this vendor," you have a file to point at instead of a codebase to spelunk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditing.&lt;/strong&gt; GDPR, CCPA, a privacy review — all of them ask "what do you collect and where does it go?" With a catalog and adapters, that's a five-minute answer. Sprinkled &lt;code&gt;track()&lt;/code&gt; calls make it a five-day investigation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that governance is possible when tracking is smeared across components. It's &lt;em&gt;trivial&lt;/em&gt; when tracking flows through one typed layer. That's the real argument for the architecture — not elegance, but the fact that a scattered approach becomes a liability the moment anyone with a compliance question walks in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the cluster
&lt;/h2&gt;

&lt;p&gt;That wraps this run on communication between the parts of an app, and the three pieces rhyme on purpose. Event buses, feature flags, and analytics all start life as innocent one-liners sprinkled through the UI — &lt;code&gt;emit&lt;/code&gt;, &lt;code&gt;if (flag)&lt;/code&gt;, &lt;code&gt;track&lt;/code&gt; — and all three rot the same way: stringly-typed, un-auditable, impossible to change in one place. And all three are fixed the same way: a typed contract, a single home, and a boundary the rest of the app talks to instead of reaching past. Communication between parts done well isn't ambient magic. It's explicit, typed, discoverable contracts — the exact opposite of "anything can talk to anything."&lt;/p&gt;

&lt;p&gt;Next, the series turns to a short but overdue detour — the panic over React re-renders, why rendering is the framework's superpower rather than its disease, and the architecture people quietly break trying to avoid it. Right after that it gets into how an app &lt;em&gt;looks and feels&lt;/em&gt; at an architectural level, starting with theming and dark mode. That's where we go.&lt;/p&gt;

&lt;p&gt;If your app tracks analytics, try the one-folder test right now: can you open a single place and read the full list of what you collect and where it's sent? If the honest answer is "no, it's everywhere," you've found your next refactor — and I'd like to hear how scattered it turned out to be.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>reactplaybook</category>
    </item>
    <item>
      <title>Feature Flags: Every Flag Is a Branch You Promised to Delete</title>
      <dc:creator>Maksym Kuzmitskyi (MaximusFT)</dc:creator>
      <pubDate>Sat, 25 Jul 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/maximusft/feature-flags-every-flag-is-a-branch-you-promised-to-delete-51om</link>
      <guid>https://dev.to/maximusft/feature-flags-every-flag-is-a-branch-you-promised-to-delete-51om</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhthiwunahxeveawt22a2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhthiwunahxeveawt22a2.png" alt="Feature Flags: Every Flag Is a Branch You Promised to Delete" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://ma-x.im/blog/react-playbook-event-bus" rel="noopener noreferrer"&gt;event bus article&lt;/a&gt; ended on a warning about a certain kind of switch teams sprinkle everywhere with a casual "it's just a toggle" attitude — and then can't get rid of. Feature flags are that switch. They're one of the most genuinely useful tools in a mature frontend, and also one of the fastest ways to accumulate debt that never shows up on a dashboard until it hurts.&lt;/p&gt;

&lt;p&gt;Let me be fair to them first, because they earn their place. A flag lets you deploy code without releasing it, roll a feature out to 5% of users and watch, keep a kill switch for something risky, or run an A/B test. Decoupling "the code is in production" from "users can see it" is a real superpower — it turns scary big-bang launches into calm, reversible dials. I'm not here to talk you out of flags. I'm here to talk about the bill that arrives later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hidden cost: every flag forks your code
&lt;/h2&gt;

&lt;p&gt;Here's what a flag actually is, mechanically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newCheckout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NewCheckout&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LegacyCheckout&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a fork. Your code now has two paths, and &lt;em&gt;both of them ship to production&lt;/em&gt;. Fine for one flag. But flags don't come alone. Add &lt;code&gt;newCheckout&lt;/code&gt;, &lt;code&gt;expressPay&lt;/code&gt;, and &lt;code&gt;giftingV2&lt;/code&gt;, and you don't have three toggles — you have up to eight combinations of code paths, and your test suite almost certainly covers two of them. The combination where &lt;code&gt;newCheckout&lt;/code&gt; is on, &lt;code&gt;expressPay&lt;/code&gt; is off, and &lt;code&gt;giftingV2&lt;/code&gt; is half-rolled-out is a real state a real user can hit, and nobody has ever seen it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every flag doubles the number of code paths you technically ship. Ten flags is a thousand possible combinations of your app, and you have tested maybe five of them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the cost that never makes it onto the ticket. The flag went in to &lt;em&gt;reduce&lt;/em&gt; risk, and each one individually does. But their product quietly grows a combinatorial space of untested app states, and that space is where the weird, unreproducible bugs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not all flags are the same thing
&lt;/h2&gt;

&lt;p&gt;The biggest conceptual mistake I see is treating "feature flag" as one category. It's at least four, and they have completely different lifespans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Release flags&lt;/strong&gt; — "show the new checkout." Short-lived. The whole point is to remove them once the feature is fully rolled out. If it's still there three months after 100% rollout, it's dead weight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ops / kill switches&lt;/strong&gt; — "disable the recommendation engine if it's melting." Long-lived on purpose. These are operational controls, not temporary forks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment flags&lt;/strong&gt; — "variant A vs B." Live exactly as long as the experiment, then die with a decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permission / entitlement flags&lt;/strong&gt; — "this customer's plan includes analytics." These aren't really flags at all. They're &lt;em&gt;configuration&lt;/em&gt; or &lt;em&gt;authorization&lt;/em&gt;, permanent by nature, and modeling them as feature flags muddies both systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The failure is conflating these. A release flag that's secretly treated as permanent config never gets deleted. An entitlement check that's built as a "flag" ends up in the same dusty list as abandoned experiments. Name the &lt;em&gt;kind&lt;/em&gt; of flag when you create it, because the kind determines whether — and when — it's supposed to disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead flags are the real debt
&lt;/h2&gt;

&lt;p&gt;A release flag that's been at 100% for six months isn't neutral. It's a permanent &lt;code&gt;if&lt;/code&gt; with a dormant &lt;code&gt;else&lt;/code&gt; branch full of code that no longer runs but still compiles, still ships, still shows up when someone greps the codebase, and still makes every reader pause to figure out whether the old path matters. It's a comment that lies, except the compiler enforces the lie.&lt;/p&gt;

&lt;p&gt;So flags need a &lt;em&gt;removal&lt;/em&gt; discipline baked in from creation, not bolted on never. The cheapest version that works: give every flag an owner and an expiry, in the definition itself, so a stale flag becomes visible instead of invisible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FlagDefinition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;release&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ops&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;experiment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/** Release/experiment flags carry a date they're expected to be gone by. */&lt;/span&gt;
  &lt;span class="nl"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ISO date&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FlagDefinition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newCheckout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;release&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payments&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2026-09-01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Rollout of the redesigned checkout flow.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A tiny CI check that fails when a &lt;code&gt;release&lt;/code&gt; flag is past its &lt;code&gt;expires&lt;/code&gt; date turns "we'll clean it up eventually" into "the build reminds us." This is exactly the &lt;em&gt;contract&lt;/em&gt; step from the &lt;a href="https://ma-x.im/blog/react-playbook-changing-shared-components" rel="noopener noreferrer"&gt;changing-shared-components article&lt;/a&gt;: a deprecation is a promise to remove the old path, and a flag is a promise to remove one of two branches. Flags that never get contracted are the same debt, wearing a different hat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Centralize evaluation behind a typed door
&lt;/h2&gt;

&lt;p&gt;Architecturally, the thing that makes flags survivable is refusing to scatter raw checks through the codebase. If &lt;code&gt;if (config.flags['new_checkout'])&lt;/code&gt; appears inline in forty components with forty slightly different string keys, you can neither find all usages nor safely delete the flag. Put evaluation behind one typed interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// keys are a closed set — typos are compile errors, usages are greppable&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FlagKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newCheckout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expressPay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;giftingV2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FlagKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFlagContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// safe default: off&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// usage — one obvious, discoverable pattern everywhere&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Checkout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newCheckout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newCheckout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;newCheckout&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NewCheckout&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;LegacyCheckout&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One door means you can list every flag from the &lt;code&gt;FlagKey&lt;/code&gt; type, find every use of a flag by searching for one function, and delete a flag by deleting its key and chasing the compile errors — the same "smaller public API is a smaller promise" idea from the &lt;a href="https://ma-x.im/blog/react-playbook-module-dependencies" rel="noopener noreferrer"&gt;module boundaries&lt;/a&gt; work. One more thing specific to this series' &lt;a href="https://ma-x.im/blog/nextjs-ssg-github-pages" rel="noopener noreferrer"&gt;static-export stack&lt;/a&gt;: decide deliberately whether a flag is evaluated at &lt;em&gt;build time&lt;/em&gt; (baked into the static output, changing it needs a rebuild) or at &lt;em&gt;runtime&lt;/em&gt; (fetched client-side, changeable live). Build-time flags are simpler and free; runtime flags cost you an evaluation path but let you flip without deploying. Neither is wrong — but conflating them leads to "why didn't my flag change do anything" confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reframe
&lt;/h2&gt;

&lt;p&gt;Feature flags are a borrowing tool. Each one lets you borrow flexibility now — ship dark, roll out slow, bail out fast — against a promise to pay it back by deleting the branch later. Teams get in trouble not because they use flags, but because they only ever do the borrowing half. The flag goes in, the feature ships, everyone moves on, and the &lt;code&gt;else&lt;/code&gt; branch sits there forever accruing interest in the form of code paths nobody understands.&lt;/p&gt;

&lt;p&gt;So the discipline is simple to say and hard to keep: know what &lt;em&gt;kind&lt;/em&gt; of flag you're adding, give the temporary ones an owner and a death date, put them all behind one typed door, and treat deleting a flag as part of shipping the feature, not a someday-chore. A flag system with a removal habit is a superpower. A flag system without one is a slowly growing maze.&lt;/p&gt;

&lt;p&gt;There's one more cross-cutting concern that gets sprinkled through components with the same casual carelessness as flags and events — scattered, stringly-typed, and impossible to audit — and it happens to be a perfect customer for the typed-event thinking from the last two articles: analytics. That's where this cluster ends.&lt;/p&gt;

&lt;p&gt;If you've got flags in your app, go count how many have been at 100% for more than a quarter. That number is your dead-branch debt, and I'd bet it's higher than anyone on the team would guess. Tell me what you find.&lt;/p&gt;

</description>
      <category>react</category>
      <category>architecture</category>
      <category>reactplaybook</category>
    </item>
  </channel>
</rss>
