Web development
-
Five Modern JavaScript Features That Make the Old Patterns Look Silly
I’ve been doing some reading on what JavaScript has been picking up over the last few releases, and the current batch is unusually good. Cleaner resource management, real Set math, lazy iterators, and a couple of small ergonomic wins that retire some genuinely tedious patterns. So this post is my attempt at summarizing five of the more interesting ones, what they replace, and where each one stands on browser support. I hope it helps if you’re trying to figure out what’s actually shipping versus what’s still a proposal.
1. Explicit Resource Management with
usingIf you’ve written C# or Python, this will feel familiar. The
usingkeyword (and its async siblingawait using) ensures a resource is cleaned up the moment the variable goes out of scope, even if your code throws. Under the hood it looks forSymbol.disposeorSymbol.asyncDisposeon the object.The old way meant remembering to wrap everything in
try/finally:async function fetchUser() { const db = new DatabaseConnection(); await db.connect(); try { return await db.query('SELECT * FROM users WHERE id = 1'); } finally { await db.close(); } }The new way:
async function fetchUser() { await using db = new DatabaseConnection(); await db.connect(); return await db.query('SELECT * FROM users WHERE id = 1'); }No
finally, no forgetting to close the connection on the error path. The cleanup is guaranteed.Browser/runtime support: Chrome 123+, Firefox 119+, Node 20.9+. Safari is still pending.
2. New Set Methods
For years, JavaScript’s
Setwas basically a deduplicated array with a fancy name. If you wanted actual set math, you were converting to arrays and looping. Now the operations are built in and run at engine speed.const userRoles = new Set(['read', 'write', 'comment']); const adminRoles = new Set(['read', 'write', 'delete', 'ban', 'comment']); userRoles.intersection(adminRoles); // shared roles adminRoles.difference(userRoles); // what admin has that user doesn't userRoles.union(adminRoles); // everything, deduped userRoles.isSubsetOf(adminRoles); // trueThat’s it. That’s the whole job. No more
new Set([...a].filter(x => b.has(x)))incantations. The full method set also includessymmetricDifference,isSupersetOf, andisDisjointFrom.These shipped as part of ES2024 and have reached Baseline. Available in Chrome 122+, Safari 17+, and recent Firefox.
3. Iterator Helpers
Until now,
.map()and.filter()only worked on arrays, and arrays load everything into memory. If you’re streaming a 50GB log file through a generator, callingArray.from()on it will introduce you to your operating system’s OOM killer.Iterator helpers bring those same methods to iterators, operating lazily, one item at a time.
The old way:
function* infiniteNumbers() { let i = 1; while (true) yield i++; } const evens = []; for (const num of infiniteNumbers()) { if (num % 2 === 0) { evens.push(num); if (evens.length === 3) break; } }The new way:
const result = infiniteNumbers() .filter(n => n % 2 === 0) .take(3) .toArray(); // [2, 4, 6]It only computes what
take(3)needs. You can chain on an infinite sequence and it just works.These are part of ES2025. Firefox has shipped them, Chrome is in the process of shipping in V8, and Safari’s implementation is roughly half done.
4. Map Upsert
The naming bounced around (early proposals called it
emplace, thenupsert), but the final landing isgetOrInsert(key, default)andgetOrInsertComputed(key, callback). The idea is simple: stop doing the three-step “check, default, fetch” dance every time you group data.The old way:
const wordMap = new Map(); for (const word of words) { const key = word[0]; if (!wordMap.has(key)) { wordMap.set(key, []); } wordMap.get(key).push(word); }The new way:
const wordMap = new Map(); for (const word of words) { wordMap.getOrInsert(word[0], []).push(word); }This is the kind of thing every codebase has a
groupByhelper for. The proposal reached Stage 4 in January 2026, so it’s officially in the spec, but engine implementations are still in progress as of this writing. Worth knowing about, not yet safe to ship without a polyfill.5. Import Attributes
As ES Modules took over, importing JSON natively became a real need. The catch is that just letting
importpull in a.jsonfile is a security problem. If the server quietly serves JavaScript instead of JSON, the engine would happily execute it as code.Import attributes fix that by making you declare the type explicitly. If the file isn’t what you said it was, the engine refuses.
import config from './config.json' with { type: 'json' }; console.log(config.databaseHost);No more
fs.readFileSyncfor config, no morerequirehacks in otherwise-modern codebases. Just an import that’s safe by default.If you’ve seen the
assert { type: 'json' }form in older articles, that was an earlier syntax that got renamed before shipping. The current keyword iswith. Available in Chrome, Edge, Firefox, and Safari since April 2025, plus Node and Deno.The Through-Line
What stands out across these five is that each one retires a pattern that’s been written into JavaScript codebases millions of times. The
try/finallycleanup. The customgroupByhelper. The Lodash imports for set operations. Theforloop with a manual counter because there was no.take()on generators. Thefs.readFileSyncfor loading a config file in an otherwise-modern ESM project.The language is quietly absorbing the utility belt, and the code that’s left looks a lot more like what we meant to write in the first place. Sign me up.
Sources
- Explicit Resource Management — V8
- JavaScript Set methods reach Baseline — web.dev
- Iterator helpers — V8
- Map.prototype.getOrInsert — MDN
- Import attributes — MDN
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Three CSS Features That Finally Let Us Delete the JavaScript
Every few years, CSS quietly absorbs something we used to need a JavaScript library for. This year, three big ones are landing at once: masonry layouts, scroll-driven animations, and styleable
<select>elements. All three have a long history of clunky workarounds (Masonry.js, scroll event listeners,div-based fake dropdowns) and watching them become native CSS features is genuinely surprising progress for a single year.Here’s what each one does.
Native Masonry Layouts
Pinterest-style grids, where items of varying heights pack together without awkward vertical gaps, used to require Masonry.js or one of its cousins. Those libraries measure every element, calculate positions, and use absolute positioning to slot things in. The result works but is expensive, fragile on resize, and hard to make truly responsive.
The native version is
display: grid-lanes:.masonry-grid { display: grid-lanes; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; }Set up your columns the way you would for any responsive grid, switch the display value to
grid-lanes, and the browser handles packing. There’s also aflow-toleranceproperty that controls how aggressively items can shift to fill gaps.The naming has a history. The CSS Working Group debated for years between an earlier
grid-template-rows: masonryproposal (which would have layered onto existing Grid) and a separatedisplay: masonryvalue. The resolution, in early 2026, was a third option: a newgrid-lanesdisplay value that’s distinct from Grid but borrows its column/row sizing. So if you read older articles showinggrid-template-rows: masonry, that syntax did not ship.Safari was first to ship
grid-lanes(Safari 26). Chrome and Firefox have it behind experimental flags as of early 2026.Scroll-Driven Animations
Reading progress bars, fade-ins as elements enter the viewport, parallax effects. All of these used to require listening to the window’s
scrollevent and updating styles in JavaScript. Scroll events fire dozens of times per second, and if your main thread is busy doing literally anything else, the animation stutters. It’s the most common source of jank on the modern web.animation-timeline: scroll()ties a standard CSS keyframe animation to scroll position instead of a time duration. The browser runs it on the compositor thread, completely independent of your JavaScript. Buttery smooth, even when the main thread is on fire.Here’s a reading progress bar in pure CSS:
@keyframes grow-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } } .progress-bar { position: fixed; top: 0; left: 0; width: 100%; height: 8px; background: linear-gradient(to right, #ff416c, #ff4b2b); transform-origin: 0 50%; animation: grow-progress linear; animation-timeline: scroll(root block); }No event listeners. No
requestAnimationFrame. No throttling logic. The animation is just bound to scroll position and the browser figures out the rest.Customizable
<select>This is the one with the longest backstory. The native
<select>element is rendered by the operating system, which means whatever the OS decides to give you is what you get. The dropdown picker, option styling, hover states inside the list, none of it has been styleable from CSS.The workaround was
div-based fake dropdowns built with JavaScript. Most of them broke keyboard navigation, screen reader support, or both. Accessibility regression in exchange for visual polish, every time.appearance: base-selectopts you out of the OS-rendered version and into a fully styleable structure with new pseudo-elements for the popup. To opt the popup itself in, you also applyappearance: base-selectto::picker(select):.custom-dropdown { appearance: base-select; padding: 10px; border-radius: 8px; border: 1px solid #ccc; } .custom-dropdown::picker(select) { appearance: base-select; background-color: #1e1e1e; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.2); padding: 8px; } .custom-dropdown option:hover { background-color: #333; cursor: pointer; }The
::picker(select)pseudo-element is the headline feature. The popup container is the thing every custom dropdown library was reinventing from scratch, and now it’s a styleable part of a real<select>, so native keyboard and accessibility behavior comes along for free.Browser support is Chromium-only as of early 2026 (Chrome, Edge, Opera 135+). Firefox and Safari haven’t shipped it yet.
A Note on Adoption
Support varies by feature. Scroll-driven animations are the most settled, with stable support in Chrome and Safari and a flag-gated implementation in Firefox.
display: grid-lanesshipped first in Safari 26 and is behind flags in Chrome and Firefox.appearance: base-selectis Chromium-only, with no Firefox or Safari implementation yet. For all three, a@supportsblock with a sane fallback is the right pattern. Check caniuse.com before you ship.But the direction is clear. Three of the most common reasons we used to pull in JavaScript are becoming native CSS features. Better performance, less code, and accessibility you don’t have to rebuild from scratch.
I’ll take that trade.
Sources
- Introducing CSS Grid Lanes — WebKit
- Masonry layout — MDN
- CSS scroll-driven animations — MDN
- The
<select>element can now be customized with CSS — Chrome for Developers - Customizable Select Element explainer — Open UI
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ Web development / Css / Front-end
-
CSS Finally Got Inline Conditionals
I’ve been digging into some of the newer features landing in CSS, and inline conditionals with the
if()function jumped out as one of the more interesting ones. The idea of writing a condition right next to the property it affects, instead of opening a separate@mediablock ten lines away, sounds useful. So this post is my attempt at summarizing what I found. I hope it helps if you’re trying to figure out whatif()actually does and where it fits.What It Actually Does
The shape is condition, colon, value, with an optional
elsebranch:property: if(<condition>: <value>; else: <fallback>;);You evaluate something, you get back a value, and the property uses that value. The
else:branch is optional. If you leave it off and the condition fails, the property falls back to its inherited or initial value.That’s it.
What makes it powerful is what you can put in that condition slot. Three kinds of queries are supported:
media()for viewport and device conditions,style()for custom property values on the element, andsupports()for feature detection. The same things you’ve always been able to ask CSS, but now you can ask them inline, right next to the property they affect.Theming Without the Yo-Yo
Dark mode is the most obvious win.
.card { background-color: white; color: black; } @media (prefers-color-scheme: dark) { .card { background-color: #333; color: white; } }And here’s the same thing with
if():.card { background-color: if(media(prefers-color-scheme: dark): #333; else: white;); color: if(media(prefers-color-scheme: dark): white; else: black;); }The logic lives next to the property. You don’t have to hunt down a separate block to figure out what happens in dark mode. When you change the light color, the dark color is right there staring at you. That’s a real maintenance win, especially in a stylesheet that’s been touched by five different people.
Variants Without Modifier Classes
This is the example that makes the case best. Component libraries are drowning in modifier classes.
.btn-primary,.btn-danger,.btn-success, on and on. Every one of them is just a different background color and maybe a border.With
if()and a custom property, you can collapse the whole thing:.button { padding: 10px 20px; border-radius: 6px; color: white; background-color: if( style(--variant: danger): red; style(--variant: success): green; else: blue; ); }<button class="button">Submit</button> <button class="button" style="--variant: danger;">Delete</button>No JavaScript reading props and toggling class names. No BEM modifier soup. You set a variable, the CSS reacts. The component owns its own logic.
Feature Detection Inline
The same pattern works for graceful degradation when you want a newer feature with a fallback:
.element { display: if(supports(display: grid): grid; else: flex;); color: if( supports(color: lch(75% 0 0)): lch(75% 0 0); else: rgb(185 185 185); ); }This used to require a dedicated
@supportsblock with the property repeated inside. Now it doesn’t.Observations
A few things are happening here that go deeper than syntactic sugar.
The first is bloat reduction. A component that used to need a base class plus four modifier classes plus a media query block can be one selector. Multiply that across a design system and the savings get real.
The second is encapsulation. A component’s stylesheet can carry its own logic without relying on a JavaScript framework to compute the right class name and shove it into the DOM. The CSS engine is doing the work, natively, and it’s a lot faster at this than your render loop is.
If the logic lives where the value lives, you don’t context-switch to figure out why a property has the value it does. You read the property, you read the condition, you understand it, you move on.
That’s the same reason inline error handling beats a separate try/catch block ten lines away. Co-location is a maintenance superpower.
Browser Support
Browser support is still narrow. Chrome shipped
if()in version 137, but Firefox and Safari haven’t followed yet, so this isn’t something to drop into a production stylesheet without a fallback. The pattern is to declare a plain value first and then override withif(), so non-supporting browsers ignore the line they don’t understand:.card { background-color: white; background-color: if(media(prefers-color-scheme: dark): #333; else: white;); }The syntax has also shifted through CSS Working Group drafts, so older articles you find about
if()may show a comma-ternary form that no longer works. Check MDN before copying anything.But the direction is right. CSS has been quietly absorbing more and more of the work we used to push to JavaScript, and
if()is one of the bigger steps in that direction. I’ll probably keep poking at it.Sources
- if() CSS function — MDN
- CSS conditionals with the new if() function — Chrome for Developers
- Lightly Poking at the CSS if() Function in Chrome 137 — CSS-Tricks
- CSS if() function — Can I use
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
/ Web development / Css / Frontend
-
JavaScript Finally Gets a Real Date API
When has working with dates ever been easy? Every language has its own version of the same headaches: time zones, parsing, leap years, arithmetic that does weird things at month boundaries. JavaScript just had some quirks layered on top, extra cruft left over from when the language was first created. Third party libraries like Moment.js or date-fns had to fill in the gaps.
Those days are over. Now we have the Temporal API.
Why Date Was Broken
Let me give you the short version of why
Dateis the way it is. It was inspired by Java’sjava.util.Datefrom the 90s, which Java itself eventually deprecated. JavaScript inherited the design and never let it go.The problems are well-known at this point:
Mutability. Pass a
Dateto a function and that function can change it underneath you.const myDate = new Date('2023-01-01'); function addDays(date, days) { date.setDate(date.getDate() + days); return date; } addDays(myDate, 5); console.log(myDate.toISOString().slice(0, 10)); // '2023-01-06'. Surprise, your original is gone.Time zone confusion.
Datestores milliseconds since the Unix epoch but formats itself in the user’s local time zone. Working in any other zone means reaching formoment-timezoneordate-fns-tz.Parsing roulette.
new Date("2023-01-01")andnew Date("Jan 1, 2023")can return different things depending on the browser and the assumed time zone.Math that lies. Adding a month to January 31st?
Daterolls it forward to March 3rd because February doesn’t have 31 days. That’s not a bug exactly. It’s justDatebeing honest that it doesn’t really understand calendars.What Temporal Actually Fixes
Temporal is a new global object designed from the ground up to address all of this. The design choices are worth walking through because they’re opinionated in the right ways.
Everything is immutable
Every operation returns a new object. Your original data stays put.
const start = Temporal.PlainDate.from('2023-01-01'); const end = start.add({ days: 5 }); console.log(start.toString()); // '2023-01-01' console.log(end.toString()); // '2023-01-06'Different types for different concepts
This is the part I find most interesting.
Datetries to be everything, a timestamp, a calendar date, a wall clock time, all at once. Temporal splits these into distinct types and forces you to pick:Temporal.PlainDate: a calendar date, no time, no zone. Birthdays, anniversaries.Temporal.PlainTime: a wall-clock time, no date.Temporal.PlainDateTime: date and time, no zone.Temporal.ZonedDateTime: fully zone-aware and calendar-aware. The one for global apps.Temporal.Instant: an exact point in time, like epoch milliseconds.Temporal.Duration: a length of time.
Making you pick the right type up front is the whole game. Half the bugs in date code come from pretending a
Dateis one thing when it’s actually another.Time zones and calendars built in
Temporal natively understands IANA time zones (
America/New_York,Europe/Paris) and non-Gregorian calendars (Hebrew, Islamic, Japanese). No external library needed.Math that respects the calendar
const t = Temporal.PlainDate.from('2023-01-31'); const nextMonth = t.add({ months: 1 }); console.log(nextMonth.toString()); // '2023-02-28'It clamps to the end of the month instead of rolling over. That’s almost always what you actually wanted.
Comparisons and Diffs
A couple of quick ones, because these are the operations you do constantly.
Comparing two dates:
const t1 = Temporal.PlainDate.from('2023-01-01'); const t2 = Temporal.PlainDate.from('2023-01-01'); console.log(Temporal.PlainDate.compare(t1, t2) === 0); // trueNo more
getTime()dance to compare primitives. There’s an actual comparison function.Finding the difference:
const start = Temporal.PlainDate.from('2023-01-01'); const end = Temporal.PlainDate.from('2023-12-31'); const diff = start.until(end, { largestUnit: 'days' }); console.log(diff.days); // 364No more dividing milliseconds by
1000 * 60 * 60 * 24and hoping DST doesn’t mess you up.Should You Use It Yet?
Check your runtime. Browser and Node support has been landing, but you’ll want to verify Temporal is available where you’re shipping, or use the official polyfill while you wait.
For most date and time work, this replaces Moment.js and date-fns entirely. Moment has been in maintenance mode for years. Temporal gives you the good parts of those libraries as a standard, immutable, well-typed API.
Datewill stick around forever for backwards compatibility. But for new code, use Temporal. The API is better, the semantics are saner, and less bug-prone.I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].
-
Why javascript:void(0) Needs to Stay in the Past
A few months ago, I ran across a
javascript:void(0)in the wild. I’m not going to get into the specific context because it doesn’t really matter, also it gets a bit too personal for this blog post. But I took a screenshot, thought “huh, that’s weird,” and promptly forgot about it.Then I was scrolling back through my screenshots and found it again. So here we are. Let’s talk about
javascript:void(0)and why you shouldn’t be using it in 2026.Here’s the screenshot that started this:

What Does It Actually Do?
If you’ve never encountered this pattern before, here’s the quick version.
voidis a JavaScript operator, so not a function, and its only job is to evaluate the expression next to it, throw away the result, and returnundefined. That’s it. That’s the whole job.When a browser receives
undefinedfrom clicking a link, it does nothing. No navigation, no page refresh. It’s a way to override the browser’s default behavior for an anchor tag.In practice, it looked like this:
<a href="javascript:void(0);" onclick="openModal()">Click Me</a>The
hrefprevents the browser from doing anything, and theonclickfires whatever JavaScript you actually wanted to run. Clever? Sure. A good idea today? No.Why Did We Use It?
Back in the day, we wanted to stop the browser from doing its default thing, following a link, so we could trigger events and make web pages more interactive. This was typically done on anchor tags because, well, that’s what we had. JavaScript didn’t give us a better way to handle it at the time, so
javascript:void(0)became the go-to pattern.It worked. But “it works” and “it’s a good idea” are two very different things.
Three Reasons to Stop Using It
1. It Breaks the Anchor Tag’s Purpose
The biggest issue is that
javascript:void(0)completely overrides what an anchor tag is supposed to do. An<a>tag exists to link to things. When you stuff JavaScript into thehref, you’re hijacking the element’s entire reason for existing.We’ve moved on from needing to do this. If you want something clickable that triggers behavior, use a
<button>. If you want a link that also has JavaScript behavior, give it a real URL as a fallback.2. Separation of Concerns
Modern best practices tell us that HTML should define the structure of the page, and JavaScript should define the behavior. When you’ve got JavaScript living inside an
hrefattribute or relying on inlineonclickhandlers, you’re mixing the two in ways that make code harder to maintain and reason about.The better approach? Use
event.preventDefault()in your JavaScript:<a href="/fallback-page" id="myLink">Click Me</a>document.getElementById('myLink').addEventListener('click', function(event) { event.preventDefault(); openModal(); });This way, if JavaScript is disabled or fails to load, the link still works. There’s a fallback behavior, which matters for accessibility and backwards compatibility. The HTML stays clean, and the behavior lives where it belongs, in your JavaScript files.
Now, I will say that plenty of modern front-end frameworks add their own semantic patterns and play pretty loosey-goosey with this separation of concerns rule. But even React’s
onClickhandlers and Vue’s@clickdirectives are compiled and managed in a way that’s fundamentally different from jamming raw JavaScript into an HTML attribute.3. Content Security Policy Will Block It
I’d like to believe Security still matters in 2026 so lets talk about the Content Security Policy (CSP).
CSP is a set of rules that a web server sends to the browser via HTTP headers, telling the browser what resources the page is allowed to load or execute. Before CSP, browsers just assumed that if code was in the HTML document, it was meant to be there. Web pages were incredibly vulnerable to cross-site scripting (XSS) attacks.
With CSP, the server tells the browser: “Only execute JavaScript if it comes from my own domain. Do not execute any code written directly inside the HTML file.”
A proper CSP header looks something like this:
Content-Security-Policy: default-src 'self'; script-src 'self';This is great for security. But guess what
javascript:void(0)is? Inline JavaScript. A strict CSP will block it.So if you see a site still using
javascript:void(0), check the response headers. Chances are you’ll find something like:Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline';See that
'unsafe-inline'addition? That’s the security risk. By addingunsafe-inline, the developer is telling the browser to trust all inline scripts. Every single one. So if an attacker manages to inject JavaScript onto the page, the browser will execute it without hesitation.You’re weakening your entire site’s security posture just to keep a legacy pattern alive. That’s not a tradeoff worth making.
You Probably Don’t Even Need to Think About This
If you’re working with any modern JavaScript framework, this problem is already solved for you.
React, Svelte, Vue, Solid, whatever you’re using, they all ship components that handle default browser behavior the right way. Take forms as an example. The raw HTML
<form>element will, by default, submit and trigger a full page navigation. That’s why developers used to manually callevent.preventDefault()everywhere. But now, frameworks like Next.js, Remix, and SvelteKit give you a<Form>component (or equivalent) that overrides that default behavior for you. No page reload. No manual prevention.The same applies to links, buttons, and pretty much any interactive element. The framework’s component handles the wiring so you don’t have to remember the low-level browser quirks. You import the component, use it, and move on.
That’s the real reason
javascript:void(0)feels so out of place in 2026. It’s not just that we have better patterns available, but the tooling has abstracted the problem away entirely. The history is worth knowing, because understanding why things work the way they do makes you a better developer! -
Routing in SvelteKit vs Next.js vs Astro
Continuing my series on Svelte, I want to dig into how SvelteKit handles routing and how it compares with Next.js and Astro. These are the frameworks I am most familiar with, so apologies if you wanted a different framework.
Let’s start with everybody’s favorite topic: routing. Okay, maybe I’m the only one excited about this, but stick with me.
Astro: The Traditional Approach
Astro’s routing feels the most traditional of the three. You have a
src/pages/directory, and the file name is the route. Sosrc/pages/dashboard.astromaps to/dashboard. If you’ve worked with PHP or other languages with file-based routing, this will feel immediately familiar.Inside the
.astrofile, you separate your backend logic at the top of the file with---fences. That code runs entirely on the server, and everything below is your HTML template. Clean and straightforward.Next.js: Folder-Based with the App Router
Next.js (the current versions, at least) uses the App Router. The structure is folder-based:
app/dashboard/page.tsxmaps to/dashboard. The key difference from Astro is that the folder name determines the route, but the file must always be calledpage.tsx.By default, everything in the
appdirectory is a server component, meaning it runs on the server. If you want client-side interactivity, you explicitly add"use client"at the top of the file. You can also set"use server"if you want to be explicit, but it’s the default. I think Next.js does a really good job of making it clear what runs where.SvelteKit: Convention Over Configuration
SvelteKit also uses folder-based routing, similar to Next.js. The structure is
src/routes/dashboard/, but instead ofpage.tsx, SvelteKit uses a reserved+prefix for its special files:+page.svelte— Renders on the server, then hydrates on the client+page.server.ts— Runs only on the server (data loading, form actions)+server.ts— A raw API endpoint with no UI
There’s no
"use client"or"use server"directive. The file naming convention itself tells SvelteKit what should run where. If you fetch a database record in+page.server.ts, that data is returned as an object, fully typed, and available in your+page.sveltevia the$propsrune. It just works.The Case for Standard File Names
I can see if some people get annoyed that every route has files named
+page.svelteand+page.server.ts. The files will all look the same in the IDE, but there’s a real advantage here: you can group all related components in the same route folder.For example, if you’re building a dashboard, you can keep your
DashboardChart.svelte,DashboardFilters.svelte, and other components right alongside your+page.svelteand+page.server.ts. You always know which file is the route entry point, which handles server logic, and which are supporting components. It encourages logical grouping instead of scattering related files across the project.Quick Comparison
Feature Astro Next.js SvelteKit Route structure File name = route Folder + page.tsxFolder + +page.svelteServer/client split ---fences"use client"directiveFile naming convention API routes src/pages/api/app/api/route.ts+server.tsDefault rendering Server Server Server + hydration All three frameworks use file-based routing, but they each have a slightly different philosophy about how to organize and separate concerns. Astro keeps it simple with traditional file mapping. Next.js gives you explicit directives. SvelteKit leans on naming conventions to keep things clean.
I think I can get used to the
+prefix convention in SvelteKit. The type safety between your server file and your page component is nice.Next up in the series, I’ll dig into how each framework handles data loading and forms.
/ Astro / svelte / Web development / Sveltekit / Nextjs
-
Svelte 5 Runes: A React Developer's Guide to Reactivity
Continuing my series on Svelte topics, today we’re talking about runes. If you’re coming from React, this is is going to be a different way to work with reactivity in modern JavaScript.
These blog posts might be what is considered the basics, but it helps me learn and think through the topics if I work on blog posts around the important things that every developer needs to know.
What Are Runes?
In Svelte 5, runes are special symbols that start with the dollar sign (
$). They look like regular JavaScript functions, but they’re actually compiler directives, reserved keywords that tell the Svelte compiler how to wire up reactivity during the build step.If you’ve used decorators in Python or macros in other languages, runes fill a similar role. They look like standard JavaScript, but the compiler transforms them into something more powerful behind the scenes.
Let’s walk through the four runes you’ll use most.
$state— The Engine of Reactivity$stateis the foundation. It declares reactive state in your component.<script> let count = $state(0); </script> <button onclick={() => count++}>{count}</button>In React,
useStatereturns an immutable value and a setter function, so you always need thatsetCountcall. In Svelte,$statereturns a deeply reactive proxy. You just mutate the value directly, and the compiler handles the rest. No setter function, no spread operators for nested objects. It just works.$derived— Computed Values Without Dependency ArraysIn React, you’d reach for
useMemohere, and you’d need to explicitly declare a dependency array so React knows when to recalculate.<script> let count = $state(0); let doubled = $derived(count * 2); </script>Dependency arrays are prone to human error. We forget what depends on what, and that leads to stale data or unnecessary recalculations.
$derivedautomatically tracks whatever state variables are used inside of it. No dependency array needed. It just reads what it reads, and recalculates when those values change.$effect— Side Effects That Actually Make SenseThis is the equivalent of
useEffectin React, which is notoriously tricky. Missing dependencies, stale closures, infinite loops… all the big gotchas are in useEffect calls.In Svelte,
$effectis used to synchronize state with external systems, like writing to local storage or updating a canvas:<script> let theme = $state('dark'); $effect(() => { localStorage.setItem('theme', theme); }); </script>Just like
$derived, it automatically tracks its dependencies and only runs when the state it reads actually changes. No dependency array, no cleanup function gotchas. It runs when it needs to run. That’s it.$props— Clean Component InterfacesEvery framework needs a way to pass data into components. In Svelte 5,
$propsmakes this look like standard JavaScript object destructuring:<script> let { name, age, role = 'viewer' } = $props(); </script> <p>{name} ({age}) - {role}</p>Default values, rest parameters, renaming … it all works exactly how you’d expect from CommonJS. If you know destructuring, you already know
$props. It’s readable, predictable, and there’s nothing new to learn.Runes Me Over
You’ve probably noticed a theme. Svelte 5 runes eliminate a whole class of bugs that come from manually managing dependencies. React makes you think about when things should update. Svelte’s goal is to figure it out for you at compile time.
/ Web-development / javascript / svelte / React
-
Svelte vs React: State Management Without the Ceremony
Continuing my Svelte deep-dive series, let’s talk about state management and reactivity. This is where the differences between React and Svelte can ‘feel’ much different.
React’s State Ceremony
In React, state requires a specific ritual. You declare state with
useState, which gives you a getter and a setter:const [count, setCount] = useState(0); function increment() { setCount(count + 1); }Want to update a variable? You have to call a function. You can’t just reassign
count. React won’t know anything changed. This is fine once you internalize it, but it adds ceremony to what should be a simple operation.Then there’s
useEffect, which is where things get tricky. You need to understand dependency arrays, and if you get them wrong, you’re looking at infinite loops or stale data:useEffect(() => { document.title = `Count: ${count}`; }, [count]); // forget this array and enjoy your infinite loopSome of
useEffectusage is actually unnecessary and likely using it wrong. If you’re using it for data transformations, derived values from state or props, or responding to user events, you’re probably reaching for the wrong tool.The React docs themselves will tell you that you might not need an effect. It’s a common source of bugs and confusion, especially for developers who are still building their mental model of React’s render cycle.
Svelte: Reactivity Through the Language Itself
Svelte takes a fundamentally different approach. Reactivity is baked into the language semantics. Want to declare state? Just declare a variable:
<script> let count = $state(0); function increment() { count += 1; } </script> <button onclick={increment}>{count}</button>That’s it. You assign a new value, and the DOM updates. The Svelte compiler sees your assignments and automatically generates the code to update exactly the parts of the DOM that depend on that variable. No virtual DOM diffing, no setter functions, no dependency arrays to manage.
Need a derived value? Svelte has you covered with
$derived:<script> let count = $state(0); let doubled = $derived(count * 2); </script> <p>{count} doubled is {doubled}</p>In React, you’d either compute this inline, use
useMemowith a dependency array, or… if you didn’t know better reach foruseEffectand a second piece of state (please don’t do this).Svelte’s
$effectrune exists for side effects like updatingdocument.titleor logging, but you should reach for it far less often thanuseEffectin React. The compiler handles most of whatuseEffectgets used for automatically.More Svelte comparisons coming as I keep digging in. Thanks for Svelting with me.
/ javascript / svelte / React / Web development
-
Svelte vs React: The Virtual DOM Tax You Might Not Need
I’m diving more into Svelte and SvelteKit lately, and I’m going to be writing a few posts about it as I learn. Fair warning: some of these will be general knowledge posts, but writing things out helps me internalize the details.
The Virtual DOM Question
It’s well known that React relies on a virtual DOM. The basic idea is that React maintains a copy of the DOM in memory, diffs it against the actual DOM, and then batches the changes to update the real thing. This works, but having to maintain this virtual DOM can lead to complications and confusion around what triggers a render or a re-render. If you’ve ever stared at a
useEffectdependency array wondering why your component is re-rendering, you know what I mean.Svelte takes a completely different approach. It’s not a library you ship to the browser, it’s a compiler step. You write Svelte code, and it compiles down to highly optimized vanilla JavaScript that surgically updates the DOM directly. No virtual DOM to maintain. No diffing algorithm running in the background. The framework essentially disappears at build time, and what you’re left with is just… JavaScript.
Templating That Feels Like the Web
I like how Svelte handles the relationship between HTML, CSS, and JavaScript. React forces you to write HTML inside JavaScript using JSX. You get used to it, sure, but it’s a specific way of thinking about your UI that can take some getting used to.
Svelte flips this around. Your
.sveltefiles are structured more like traditional web pages — you’ve got<script>tags for your JavaScript, regular HTML for markup, and<style>tags for CSS. Everything lives in one file, but there’s a clear separation between the three concerns.If you’ve ever worked with Django templates, Laravel Blade, or Ruby on Rails views, this will feel immediately familiar. It’s a lot closer to how the web actually works than JSX’s “everything is JavaScript” approach. For someone coming from those backgrounds, the learning curve is noticeably gentler.
More Svelte posts coming as I dig deeper. That’s all for now!
/ javascript / svelte / React / Web development
-
jQuery 4.0 was released on the 17th and they removed IE10 support. IE10 was first deprecated by Microsoft in January 2016 and fully retired in 2020. You might wonder, “What are they doing still supporting Internet Explorer?” They did say they were going to fully remove support in version 5.
-
Twenty Years of DevOps: What's Changed and What Hasn't
I’ve been thinking about how much our industry has transformed over the past two decades. It’s wild to realize that 20 years ago, DevOps as we know it didn’t even exist. We were deploying to production using FTP. Yes, FTP. You use the best tool that is available to you and that’s what we had.
So what’s actually changed, and what’s stayed stubbornly the same?
The Constants
JavaScript is still king. Although to be fair, the JavaScript of 2005 and the JavaScript of today are almost unrecognizable. We’ve gone from jQuery spaghetti to sophisticated module systems, TypeScript, and frameworks that would have seemed like science fiction back then.
And yet, we’re still centering that div.
Certainly, HTML5 and semantic tags have genuinely helped, and I’m certainly grateful we’re not building everything out of tables and spans anymore.
What’s Different
The list of things we didn’t have 20 years ago is endless but here are some of the big ones:
- WebSockets
- HTTP/2
- SSL certificates as a default (most sites were running plain HTTP)
- Git and GitOps
- Containers and Kubernetes
- CI/CD pipelines as we know them
- Jenkins didn’t exist
- Docker wasn’t even a concept
The framework landscape is unrecognizable. You might call it a proliferation … We went from a handful of options to, well, a new JavaScript framework every week, so the joke goes.
Git adoption has been one of the best things to happen to our industry. (RIP SVN) Although I hear rumors that some industries are still clinging to some truly Bazaar version control systems. Mecurial anyone?
The Bigger Picture
Here’s the thing that really gets me: our entire discipline didn’t exist. DevOps, SRE, platform engineering… these weren’t job titles. They weren’t even concepts people were discussing.
We had developers in their hole and operations in their walled gardens. Now we have infrastructure as code, GitOps workflows, observability platforms, and the expectation that you can deploy to production multiple times a day without breaking a sweat.
The cultural shift from “ops handles production” to “you build it, you run it” fundamentally changed how we think about software.
What Stays the Same
Despite all the tooling changes, some things remain constant. We’re still trying to ship reliable software faster. We’re still balancing speed with stability.
Twenty years from now, I wonder what we’ll be reminiscing about. Remember when we used to actually write software ourselves and complain about testing?
What seems cutting-edge is the new legacy before you know it.
/ DevOps / Web-development / Career / Retrospective