Blog
Short, opinionated reads on the Universal Frontend Architecture — the atoms and organisms vocabulary, where state actually lives, how the server layer fits, and how to stay valuable as a frontend engineer while an AI writes half the diff. Every post ends by sending you to the questions that drill it.
-
Container queries: size a component by its parent, not the viewport
A media query asks "how wide is the viewport?" A container query asks "how wide is my parent?" — and that is the question a reusable component actually needs answered before it can lay itself out.
-
Normalize your API responses: the refactor that pays for itself
A nested API payload is a tax you pay on every read and every update. Flattening it into entity tables keyed by id is the one refactor that makes the rest of your state code simpler.
-
Atoms, molecules, organisms: the vocabulary of a component system
Atomic design is not a folder-naming fad — it is a shared vocabulary for arguing about where a component belongs before you write it.
-
The Universal Frontend Architecture: UI, Server, and State as three seams
One architecture that survives a framework swap because it separates the three concerns every frontend has — rendering the UI, talking to the server, and holding state — instead of tangling them.
-
Surviving as a frontend engineer in the age of AI
An AI can write the component. It cannot decide where state lives, why the organism should not fetch, or whether the diff is safe to ship. That judgement is the job now.
-
An AI harness for keeping UI and State apart
The fastest way to lose the UI/State boundary is to let an AI write across it a hundred times. The fix is a harness that fails the build the first time a component reaches into the store.
-
Real DOM, Virtual DOM, Shadow DOM: three different things with one word
They share three letters and nothing else. One is what the browser renders, one is a diffing trick, and one is an encapsulation boundary — confusing them is how DOM questions get failed.
-
What frontend interviews actually measure
Frontend interviews are not quizzing you on trivia. Each round probes a specific axis — API recall, UI construction, state modelling, and trade-off reasoning — and knowing which is which is half the preparation.
-
Semantic HTML is the cheapest accessibility you will ever ship
Before you reach for a single ARIA attribute, use the element that already means what you want. A button is a button; a div pretending to be one is a bug waiting to happen.
-
CSS specificity decides who wins, and it is not about order
When two rules target the same element, the more specific selector wins — regardless of which came last. Understanding the tiebreak is the difference between fixing a style and piling !important on it.
-
Flexbox or Grid? Pick by how many axes you are laying out
The endless flexbox-versus-grid debate has a boring answer — one axis, use flexbox; two axes, use grid. Everything else is a corollary of that one distinction.
-
Custom properties are runtime variables, and that changes theming
A Sass variable is gone by the time the browser runs. A CSS custom property is live — it cascades, inherits, and can be changed at runtime, which is why theming and dark mode are built on it.
-
Reflow and repaint are not the same, and the difference is your frame budget
Changing a color repaints. Changing a size reflows. Reflow is the expensive one, and doing it inside a loop is how a smooth list turns into a janky one.
-
Event bubbling and capturing: the two trips every click takes
Every DOM event travels down to the target and back up again. Knowing which phase your listener runs in is what makes event delegation, and stopping propagation, predictable instead of magic.
-
Focus management is the keyboard user's cursor
For a keyboard or screen-reader user, focus is the cursor. A dialog that opens without moving focus, or a menu that traps it, is as broken as a mouse that stops moving.
-
The accessibility tree is what screen readers actually read
Screen readers do not read your DOM. They read a parallel structure the browser builds from it — the accessibility tree — and knowing that explains why a styled div announces nothing.
-
Controlled vs uncontrolled inputs: who owns the value?
The whole controlled-versus-uncontrolled question comes down to one thing — does your component's state own the input's value, or does the DOM? Pick per field, not per app.
-
Debounce and throttle are different tools for different problems
Both limit how often a function runs, but they answer different questions. Debounce waits for quiet; throttle enforces a steady rate. Using the wrong one makes a search box feel broken.
-
Pure functions, and why a reducer has to be one
A reducer that reads the clock, mutates its input, or fires a request is not a reducer — it is a bug. The purity rule is what makes state predictable, testable, and time-travellable.
-
Immutability is how you detect change cheaply
Immutable updates are not about purity for its own sake. They let the framework decide 'did this change?' with a reference check instead of a deep scan — which is what keeps re-renders bounded.
-
Actions are messages, not setters
The most common Redux mistake is treating actions like setters — SET_USER, SET_LOADING, SET_ERROR. An action should describe something that happened, not command the store how to change.
-
The store is one source of truth, or it is not a source of truth
A single source of truth is not a slogan — it is a constraint. The moment the same fact lives in two places, you own the job of keeping them in sync, and you will lose.
-
Selectors derive; they don't store
A selector is a function from state to a derived value. Used well, it is the seam that lets you reshape state without touching components — and memoized, it keeps derived data cheap.
-
Middleware is the store's pipeline, not a grab bag
Middleware sits between dispatch and the reducer, and each one can inspect, delay, transform, or swallow an action. Understanding it as an ordered pipeline is what makes async and logging predictable.
-
Thunks and sagas both handle async — they just disagree on how
A thunk is a function you dispatch; a saga is a long-running process that watches for actions. Pick by how complex your async is — most apps need thunks, some genuinely need sagas.
-
The request/success/fail triple is the shape of every fetch
Every asynchronous call has three outcomes worth modelling — it started, it worked, it failed — and a single isLoading boolean cannot represent them. Model the triple as a state machine and your spinners, errors, and race conditions all fall into place.
-
Server state is not client state, and treating them the same hurts
The data you fetch from a server and the state your UI owns are different animals with different rules. Cramming server data into your Redux store as if you owned it is the source of most stale-data bugs.
-
Optimistic updates: show it now, reconcile later
An optimistic update applies the change to the UI before the server confirms it, then rolls back if the server disagrees. It makes an app feel instant — and it is only safe if you plan the rollback.
-
Colocate state until you can't
The right home for a piece of state is the smallest scope that needs it. Lifting everything to a global store by default is how a simple app grows a state-management problem it never needed.
-
When you don't need Redux
Redux is a tool for a specific problem — a lot of client state, changed from many places, that many parts of the app must read. If that is not your problem, Redux is overhead you will resent.
-
combineReducers splits the store without splitting the truth
One state tree can still have many owners. combineReducers gives each slice its own reducer while keeping a single store — the trick is that each reducer sees only its slice, and that constraint is a feature.
-
The URL is state too — and often the right place for it
Filters, tabs, search queries and pagination all belong in the URL more often than in a store. If a user would want to bookmark, share, or reload into a view, that view's state should live in the address bar.
-
Server-side rendering: HTML first, JavaScript second
Server-side rendering sends real HTML on the first response instead of an empty div waiting for JavaScript. That changes what the user sees first, what a crawler indexes, and what your server has to do.
-
Static site generation: render once, serve a million times
Static generation renders your pages at build time, not per request, so the server just hands out files. It is the fastest and cheapest option for content that does not change per user — until the content changes often.
-
Client-side rendering is not the villain, it is a trade-off
The single-page app that renders everything in the browser gets blamed for slow first paints and bad SEO. Both are real, both are fixable, and for the right app CSR is simpler and cheaper than the alternatives.
-
Hydration is the handoff from server HTML to a live app
Hydration is the moment server-rendered HTML becomes interactive, when the client JavaScript attaches to the existing markup. It is also where a whole class of subtle SSR bugs and performance costs live.
-
The app shell: load the frame instantly, fill it after
The app shell is the minimal HTML, CSS and JavaScript that renders your app's frame — nav, layout, chrome — instantly from cache, so the user sees structure while the content loads. It is the backbone of a fast, installable app.
-
HTTP cache headers are a contract you are signing with every browser
Cache-Control, ETag and the rest are not obscure server trivia — they are how you tell every browser and CDN how long to trust a response. Get them wrong and you either serve stale files or throw away free speed.
-
Proxy it or fix CORS? Two answers to the same cross-origin wall
When your frontend cannot call an API because of a cross-origin error, you have two real fixes — proxy the request through your own origin, or set CORS headers on the API. Which one is right depends on who owns the API.
-
CORS is the browser's rule, not the server's
The single fact that makes CORS make sense: it is enforced by the browser, not the server. The request often reaches the server and runs — the browser just refuses to let your JavaScript read the response.
-
Sessions vs tokens: where does the user's identity actually live?
A session keeps the source of truth on the server and hands the browser a key. A token hands the browser a signed claim and keeps nothing. That one difference drives revocation, scaling, and where you must store the thing.
-
The critical rendering path is the story of your first paint
Between the HTML arriving and the first pixel painting, the browser runs a fixed sequence — and CSS and synchronous JavaScript can block it. Knowing the path is how you make a page paint sooner.
-
Code splitting: ship the JavaScript this page needs, not all of it
One big bundle makes the user download your entire app to see the login page. Code splitting breaks it into pieces loaded on demand, so the first screen ships only what it needs and the rest arrives when it is used.
-
Responsive images: describe the options, let the browser choose
Shipping one large image to every device wastes data on phones and looks soft on retina screens. srcset and sizes let you describe the options and hand the choice to the browser, which knows the device better than you do.
-
Where AI actually fits in the frontend development lifecycle
AI is not one thing you bolt onto the end of development. It shows up at every stage — scaffolding, implementing, reviewing, testing — and it is strong at some and dangerous at others. Knowing which is the whole skill.
-
Prompt to component: the quality is bounded by the boundary you hand over
Asking a model for a component produces good code or a mess depending almost entirely on how sharply you scoped the request. The skill is not prompting tricks — it is drawing the box before you ask.
-
Review an AI diff adversarially: assume it works and looks for the catch
The question for an AI diff is never "does it run" — it usually does. It is "what would make this wrong that the tests do not cover?" Reviewing AI code well means reading it looking for the plausible-but-broken.
-
An executable guardrail beats a review comment every time
A rule that lives in a senior engineer's head gets enforced when they happen to review. A rule that lives in a check gets enforced on every diff, forever, by something that never gets tired. In an age of AI-authored code, that difference decides.
-
AI writes markup fast, and accessibility is exactly what it drops
A model will happily generate a div that looks like a button, because it renders and looks right. Accessibility lives in the parts that do not show — roles, names, keyboard behaviour — which is precisely what optimizing for "looks right" skips.
-
Evals are tests for the things unit tests can't check
A unit test checks that a function returns the right value. An eval checks that a piece of prose, a design, or an AI output meets a standard you can describe but not compute. As AI writes more, evals become as important as tests.
-
Agent skills are how you stop re-explaining yourself to the model
If you find yourself pasting the same architecture rules and conventions into every prompt, you have discovered the need for a skill — a packaged, reusable set of instructions the model loads for a class of task instead of you retyping it.
-
When not to use AI on the frontend
Using AI well includes knowing when not to. There are tasks where a model is slower, riskier, or actively misleading, and reaching for it there is not sophistication — it is a mistake that costs you more than typing would have.
-
The harness pattern: make 'is it done' a number, not an opinion
A harness is the set of checks that decide whether work is good enough, run automatically. Build one and "is this done" stops being a meeting and becomes an exit code — which is the only way autonomous or AI-heavy work stays honest.
-
AI cannot hold your architecture, so you have to
A model has no memory of your system and no stake in its shape, so it optimizes each task locally and erodes the structure globally. The durable job is owning the architecture the model keeps forgetting.
-
AI-generated tests need a human oracle
A model is great at writing test scaffolding and terrible at deciding what "correct" is. Ask it to test existing code and it will assert that the current behaviour — bugs and all — is right. The oracle has to be you.
-
AI does not remove technical debt — it lets you create it faster
A model that generates code at ten times human speed also generates debt at ten times human speed, if you let it. The productivity is real; so is the pile it can leave behind without a standard to hold it to.
-
Presentational and container components: the split that keeps testing sane
Split components by whether they render or whether they fetch and decide. The presentational half takes props and stays dumb; the container half talks to the outside. That one boundary is what makes both halves testable and reusable.
-
Folder structure should follow architecture, not file type
Grouping every component in one folder, every style in another, every test in a third feels tidy and ages badly. Structure that mirrors your architecture — atoms, molecules, containers, state — tells a new reader how the app is built.
-
The container line: draw it, and defend it with a check
Somewhere in your tree is a line above which components may touch the store and below which they may not. Naming that line — and enforcing it with a check — is what keeps your UI layer portable and testable.
-
Design tokens are the API of a design system
A design token is a named value — a color, a space, a radius — used everywhere instead of a raw number. Treating tokens as the design system's public API is what makes theming, dark mode, and rebrands a config change instead of a find-and-replace.
-
The testing pyramid, adjusted for the frontend
The classic pyramid — many unit tests, some integration, few end-to-end — needs a frontend twist. The most valuable tests here are the ones that render a component and interact with it the way a user does. Test behaviour, not implementation.
-
Skeleton screens beat spinners, and it is not close
A spinner says "wait, something is happening somewhere." A skeleton says "here is the shape of what is coming, in this exact spot." The second feels faster and shifts less, and the reasons are worth knowing.
-
How to prepare for a frontend interview without boiling the ocean
Most candidates prepare by grinding random problems and hoping. The loop is legible: prepare per round — utilities, UI components, system design, behavioural — and drill the specific muscle each one tests.
-
The take-home is a different game from the live round
A live coding round rewards thinking out loud under time pressure. A take-home rewards the opposite — polish, structure, tests, and the judgement to know when to stop. Treating one like the other is how strong candidates underperform.
-
The frontend system design round is about trade-offs, not diagrams
There is no right answer in a frontend system design round, and that is the point. The interviewer is testing whether you can name a trade-off, take a side, and defend it — not whether you can draw the "correct" boxes.
-
The UI coding round rewards the parts that don't show in a screenshot
Anyone can make a component look right in an interview. What separates a pass is the invisible half — keyboard operation, focus, correct roles, coherent state — done while the clock runs, not bolted on at the end.
-
In the coding round, your narration is most of the score
The interviewer cannot read your mind, and they are scoring your thinking, not just your code. Silence while you type — even to a correct answer — leaves most of the available signal unspoken. Talk.
-
From junior to senior frontend: the shift is from code to consequences
The jump to senior is not writing fancier code. It is caring about the consequences of code — how it ages, how it fails, how the next person changes it — and making decisions with the whole system in mind.
-
Progressive web apps: the service worker is the whole trick
A PWA is a website that behaves like an installed app — offline, installable, fast on repeat visits. Almost all of that comes from one piece: a service worker sitting between your page and the network.
-
SEO for single-page apps: help the crawler see what the user sees
A client-rendered app ships an empty div and fills it with JavaScript. Some crawlers run that JavaScript, many do not or do it late. If organic traffic matters, you have to get real content into the HTML the crawler first sees.
-
Internationalization is more than swapping the words
Translating strings is the easy, visible part. The hard part is everything else a locale changes — dates, numbers, currency, plurals, text direction, and layouts that must survive words twice as long.
-
Micro-frontends solve an org problem, not a tech one
Splitting a frontend into independently deployable pieces is worth it when teams are stepping on each other, and a costly mistake when they are not. The boundary should follow the org chart, not the tech.
-
The backend-for-frontend: an API shaped for the screen, not the database
A backend-for-frontend is a thin server layer that exists to serve one frontend — aggregating calls, reshaping data, and holding secrets — so the client gets exactly what the screen needs in one request.
-
Prefetching hides latency by doing the work before it's asked for
The fastest request is the one that already finished. Prefetching loads the code or data for what the user is likely to do next, during idle time, so the next click feels instant instead of waiting on the network.
-
The box model, and why box-sizing: border-box exists
Every element is a box of content, padding, border, and margin. The one setting that changes how width is measured — box-sizing — is why your 50% columns overflow, and why every reset sets it to border-box.
-
z-index: 9999 doesn't work, and stacking contexts are why
When your modal with z-index 9999 still hides behind the header, the problem is not a bigger number — it is that z-index only competes within a stacking context, and something created a new one you did not notice.
-
Keys in lists are identity, and using the index breaks it
A list key tells the framework which item is which across renders. Use a stable id and reordering and editing just work; use the array index and you get lost input, wrong animations, and state attached to the wrong row.
-
Error boundaries stop one broken component from blanking the page
Without an error boundary, one component throwing during render takes the whole app down to a blank screen. A boundary catches the crash, shows a fallback for that subtree, and keeps the rest of the app alive.
-
Portals render outside the tree so overlays escape their parents
A portal renders a component's output somewhere else in the DOM while keeping it in the component tree. It is how modals and dropdowns escape the overflow and stacking traps of their parents without losing their props and state.
-
Design your loading and empty states before your happy path
Every data-driven component has at least four states — loading, empty, error, and loaded — and the ones that are not the happy path are where real apps feel broken. Design all four, not just the screenshot.
-
Composition beats configuration when a component grows props
When a component sprouts a dozen boolean props to cover every variation, the fix is usually not another prop — it is letting the caller compose the pieces. Configuration scales to a point; composition scales past it.
-
Why an organism should not fetch its own data
The moment a reusable organism fetches its own data, it stops being reusable and becomes a feature bolted to one endpoint. Keeping the fetch above it, in a container, is what keeps the UI layer portable and testable.
-
Accessible forms come down to labels, grouping, and error wiring
Most form accessibility is not exotic ARIA — it is labels tied to inputs, related fields grouped, and errors wired so a screen reader announces them. Get those three right and the form works for everyone.
-
The event loop, and why a busy function freezes the whole UI
JavaScript runs on one thread, so a function that runs too long blocks everything — clicks, scrolls, rendering, all frozen until it returns. Understanding the event loop is understanding why, and how to keep the thread free.
-
Memoization is just caching with a key you have to get right
Memoization caches a result against its inputs and returns the cache when the inputs repeat. Simple — except the whole thing depends on comparing inputs correctly, which is where most memoization quietly does nothing.
-
Progressive enhancement still matters, even in a JavaScript world
Build on a foundation that works without JavaScript, then layer richness on top. It sounds old-fashioned until a script fails to load, a network flakes, or a crawler visits — and the baseline is what saves you.
-
Scroll and resize fire constantly — throttle them or pay for it
Scroll and resize can fire dozens of times a second, and doing real work in their handlers is a reliable way to jank the page. Throttle them, or move the work to the platform APIs built for exactly this.
-
Web components are the platform's answer to reusable UI
Custom elements, shadow DOM, and templates let you build framework-agnostic components the browser understands natively. They will not replace your framework, but they are the right tool for cross-framework, long-lived UI.
-
Feature flags decouple deploying code from releasing a feature
A feature flag lets you merge and deploy unfinished or unreleased code that stays dark until you flip it on. That separation — deploy is not release — is what makes trunk-based development and safe rollouts possible.
-
Every dependency is a loan, and the interest is paid in bytes and risk
Adding a package feels free — one install command. The real cost shows up later in bundle size, security surface, maintenance, and the day it breaks. Weigh it before you borrow, not after.
-
Largest Contentful Paint: make the biggest thing appear fast
LCP measures when the largest visible element — usually a hero image or headline — finishes rendering. It is the Core Web Vital users feel most, and fixing it is mostly about the critical path and one image.
-
The request waterfall: when your fetches wait in line for no reason
A waterfall is a chain of requests that each wait for the previous one when they did not have to. It is the quiet cause of slow pages — data that could have loaded in parallel loading in sequence instead.
-
State machines turn impossible UI states into unreachable ones
A pile of booleans can represent nonsense — loading and error true at once. A state machine names the legal states and the transitions between them, so the impossible combinations simply cannot occur.
-
A component should do one job, and you should be able to name it
If you cannot describe a component in one sentence without saying "and", it is doing too much. The single-responsibility test is the cheapest way to know when to split — and where.
-
Accessibility is not a checklist you run at the end
Automated checkers catch maybe half of accessibility issues, and only the mechanical half. The rest — does the keyboard flow make sense, does the screen reader tell a coherent story — needs building in, not auditing on.
-
Test behaviour, not implementation, or your tests become the bug
A test that asserts on internal state breaks every time you refactor, even when nothing user-facing changed. A test that asserts on behaviour survives refactors and fails only when something real breaks. The distinction decides whether your suite helps or hurts.
-
The portfolio that gets interviews shows depth, not a wall of clones
A dozen tutorial to-do apps say less than one project built to a real standard — tested, accessible, deployed, and explained. Depth on a few things beats breadth across many, because depth is what a reviewer cannot fake-detect.
-
Debugging is a skill you can practice, not a talent you're born with
Fast debuggers are not smarter — they follow a method. Reproduce, isolate, form a hypothesis, test it, repeat. Guessing and changing things at random feels like debugging and mostly wastes time.
-
The cost of premature abstraction: wrong is more expensive than repeated
Pulling two similar bits of code into a shared abstraction feels responsible. Do it too early, before you know how they will actually diverge, and you build the wrong abstraction — which costs more than the duplication ever would.
-
Ship small diffs: the pull request nobody can review is nobody's friend
A 40-line pull request gets a real review in minutes. A 2000-line one gets a rubber stamp, because nobody can hold it in their head. Small diffs are not a nicety — they are how bugs get caught and how you ship faster.
-
Reading a frontend job description for what it actually says
A job description is a wish list, a signal about the team, and a hint about the interview — if you read it critically. The required years and the twenty listed technologies rarely mean what they literally say.
No posts match those filters.