javascript
-
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].
-
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].
-
npmx.dev Is the NPM Frontend We've Been Asking For
If you’ve spent any time on npmjs.com, you know the drill. You land on a package page, eyeball the tarball size, squint at the dependency list, then bounce out to bundlephobia, then to Are The Types Wrong, then to Socket.dev, just to figure out if this thing is safe to install. That’s not a workflow. That’s a scavenger hunt.
For years, the JavaScript community has been filing requests on the official npm tracker asking for this stuff to live in one place. As Andrew Nesbitt notes, native dark mode was the single most upvoted request on the tracker for something like five years before it shipped. Real install sizes (transitive, not just the tarball). UI warnings about compromised packages and hidden postinstall hooks. Concrete version resolution instead of squinting at semver ranges. The list is long, and it mostly went nowhere.
So the community built npmx.dev instead.
What It Actually Is
It’s important to note: npmx.dev is not a separate registry. It doesn’t mirror packages, and
npm installstill pulls from the official Microsoft/GitHub-owned registry. What npmx is, strictly, is an alternative frontend. It hits the official npm APIs in real time, caches the results at the edge, and renders a much better page.The stack is Nuxt on top of the full VoidZero toolchain with Vite, Vitest, Rolldown, oxlint, oxfmt, and the other usual packages you’d expect. It also leans heavily on SSR and ISR for near-instant page loads. VoidZero (the company behind Vite) sponsors the project, which makes sense, since it’s open-source MIT and the operational costs are basically web traffic and compute, not package storage.
What You Actually Get
I really like the dependency view. Instead of a flat list of what the package declares, you get an expandable tree with recursive vulnerability tracking via OSV. You can drill into transitive dependencies and see which ones are flagged. That’s the thing every security-conscious developer has been doing manually for years.
A few other wins worth calling out:
- Real install size. Not the tarball. The actual disk footprint with all dependencies resolved.
- postinstall scripts surfaced. If a package is going to run arbitrary code on your machine at install time, npmx puts that on the page where you can see it. Not buried in the manifest.
- Concrete resolved versions displayed alongside the semver range, so you know what you’re actually getting.
- Banners suggesting lighter alternatives for bloated legacy packages. Opinionated, and I’m here for it.
A Quick Note on OSV
The vulnerability data comes from the OSV ecosystem, and it’s worth understanding what that is, because it’s one of the more important pieces of open-source infrastructure right now.
OSV is a centralized aggregator that pulls from GitHub Security Advisories, PyPA, RustSec, the Global Security Database, and many ecosystem-specific feeds. The schema is standardized JSON, machine-readable, and maps vulnerabilities to exact versions or commit hashes. That matters because it’s what lets automated scanners avoid the false-positive flood that makes most security tooling annoying to use.
It’s an OpenSSF project under the Linux Foundation. Google maintains the infrastructure; GitHub, the Rust Foundation, the PyPA, Red Hat, and a long list of ecosystem maintainers contribute data. Vendor-neutral, free, and the boring kind of governance you want for security data.
Making It Your Default
Here is how you can make it the actual default npm frontend. Since npmx.dev maintains URL parity with npmjs.com, you can redirect every npm link you click without thinking about it. A couple of options depending on your setup.
A few options:
Kagi users can add a global URL redirect rule:
^https://www.npmjs.com|https://npmx.devClick an npm link in your search results, land on npmx instead.
Browser extensions work for everyone else. There’s a dedicated
npmx-redirectextension for Chrome and Firefox that does only this one thing. Or use Redirector with a regex rule:- Include:
^(?:https?://)?(?:www\.)?npmjs\.com/(.*) - Redirect to:
https://npmx.dev/$1
Custom site search. In Chrome, Brave, Arc, or Safari, add a new site search with shortcut
nx(or@npm) pointing at:https://npmx.dev/search?q=%sNow you type
nx, space,zod, enter. Done. You never see npmjs.com again unless you want to.Why This Matters
The npm registry is critical infrastructure for, conservatively, a huge chunk of the software running on the internet. The fact that the community had to build its own frontend to surface basic security and observability data tells you something about where the priorities have been.
I’ll be using npmx as my default going forward. I’m not going back.
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].
Sources
-
What I Learned Building My First Chrome Extension
I built a Chrome extension to navigate Letterboxd movie lists with keyboard shortcuts. Rate, like, watch, next. Here’s what I learned.
The Idea
I going through the Letterboxd lists, but wanted a better way. “Top 250 Films,” curated genre lists, friends’ recommendations. The flow becomes tedious: click a movie, rate it, go back to the list, find where you were, click the next one. I wanted to load a list into a queue and step through movies one by one with keyboard shortcuts.
So I did what any reasonable person would do and built a Chrome extension for it.
The Framework Graveyard
The Chrome extension ecosystem has a framework problem. CRXJS, the most popular Vite plugin for extensions, was being archived. Its successor, vite-plugin-web-extension, was deprecated in favor of WXT. WXT is solid but it’s another abstraction layer that could go the same way.
I went with plain Vite and manual configuration. Four separate Vite configs, one per entry point (content script, background worker, popup, manager page). A simple build script that runs them sequentially and copies the manifest. No framework dependency that could die on me.
For the UI I used React and TypeScript. Not because the extension needed React, most of the work is content scripts and background messaging, but the popup and settings page benefit from component structure.
Four Separate Worlds
One thing I learned was a Chrome extension isn’t one app. It’s four separate JavaScript contexts that can’t directly share state:
- Content scripts run on the webpage (letterboxd.com). They can read and modify the DOM but can’t access chrome.tabs or other extension APIs.
- Background service worker runs independently. It handles messaging, storage, and tab navigation. It can die at any time and restart.
- Popup is a tiny React app that opens and closes with the extension icon. It loses all state when closed.
- Extension page (the manager) is a full tab running your own HTML. It persists as long as the tab is open.
They communicate through
chrome.runtime.sendMessageandchrome.storage.local. This is an important architectural challenge you need to be aware of. If you’ve never built an extension before, it could trip you up.Letterboxd’s DOM Is a Moving Target
The existing open-source Letterboxd Shortcuts extension uses selectors like
.ajax-click-action.-liketo click the like button. Those selectors don’t exist anymore. Letterboxd has migrated to React components, and the sidebar buttons (watch, like, watchlist, rate) are loaded asynchronously via CSI (Client Side Includes). They’re not in the initial HTML at all.I had to inspect the actual loaded DOM to find the current selectors:
.watch-link,.like-link,a.action.-watchlist. The rating widget still uses the old.rateitpattern with adata-rate-actionattribute and CSRF token POST.If you’re building an extension that interacts with a third-party site’s DOM, expect the selectors to break. Build your DOM interaction layer as a thin, isolated module so you can update selectors without touching the rest of the codebase.
Service Workers Can’t Use DOMParser
My list scraper used
DOMParserto parse HTML responses. Works fine in tests (jsdom), works fine in content scripts (browser context), fails completely in the background service worker. Service workers don’t have access to DOM APIs.I rewrote the parser to use regex. Less elegant but it works everywhere. If I were doing it again, I’d run the parsing in a content script and message the results back to the background worker.
The Build System Is Simpler Than You Think
I expected the multi-entry-point build to be painful. It wasn’t. Each Vite config is about 20 lines. Content script and background worker build as IIFE (single file, no imports). Popup and manager build as standard React apps. The build script is 30 lines of
execFileSynccalls.One gotcha: asset paths. Vite defaults to absolute paths (
/assets/index.js), but extension popups and pages need relative paths (./assets/index.js). Addingbase: './'to the popup and manager configs fixed it.TDD Was Worth It (For the Right Parts)
The extension has four pure logic modules: rating double-tap behavior, auto-advance detection, queue state operations, and keyboard shortcut matching. These are the core of the extension and they’re completely testable without a browser.
Writing tests first caught edge cases I wouldn’t have thought of. What happens when you press the same rating key on a movie that was already rated in a previous session? What if the queue is empty and someone hits “next”? The tests document these decisions.
For DOM interaction code, the Letterboxd API layer, overlays, CSI-loaded content, unit testing isn’t practical. I tested those manually.
What I’d Do Differently … or might change
Start with the DOM. I built the pure logic first and the DOM interaction last. This meant I didn’t discover the CSI loading issue, the changed selectors, or the DOMParser problem until the end. Next time I’d build a minimal content script first, verify it can interact with the target site, then build the logic on top.
Use fewer Vite configs. Four config files with duplicated path aliases is annoying. A single config with a build mode flag, or a shared config factory function, would be cleaner.
Consider the popup lifecycle earlier. Popups close when you click outside them. Any state they hold is gone. I designed around this (the popup is stateless, it queries the background on every open), but it’s easy to get wrong if you don’t plan for it.
The Result
The extension loads any Letterboxd list into a queue, navigates through movies one by one, and lets me rate/like/watch/watchlist with single keystrokes. Auto-advance moves to the next movie when I’ve completed my actions. A dark-themed manager page shows the full queue and lets me customize every shortcut.
It’s a personal tool right now, so not published to the Chrome Web Store. But it’s made going through movie lists is pretty cool. Sometimes the best software is the kind you build for yourself!
If you’re a developer, 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! -
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.
-
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.
-
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!
-
Introducing EmDash — the spiritual successor to WordPress that solves plugin security
Today we are launching the beta of EmDash, a full-stack serverless JavaScript CMS built on Astro 6.0. It combines the features of a traditional CMS with modern security, running plugins in sandboxed Worker isolates.
-
Turborepo is a build system optimized for JavaScript and TypeScript, written in Rust.