Angular vs. Svelte: 7 of 24 Client Components Need No State At All
DI-injected signals versus a build-time compiler. Both assume a component needs a reactive primitive — 7 of this codebase's 24 Client Components prove a third of the time, it doesn't.
Angular and Svelte sit at opposite ends of how a UI framework handles state: Angular requires a dependency-injection container and (traditionally) RxJS observables or its newer signals API before a component can react to anything; Svelte compiles reactivity into direct DOM updates at build time, with no runtime and no container to register into. Choose Angular for a large team that wants enforced structure; choose Svelte for a small, fast, JavaScript-light app. Neither answers a question this codebase's own component split makes concrete: how much of a typical UI actually needs a reactive-state primitive at all.
The decision in one table
| Angular | Svelte | |
|---|---|---|
| Reactivity model | DI-injected services + Zone.js change detection (default), or signal()/computed() (zoneless, current Angular) | Compile-time — let and $state() runes (Svelte 5) become direct DOM-mutation instructions, no runtime reconciler |
| What a component needs before it can hold state | A provider registered in the injector tree | Nothing — state is a local variable the compiler tracks |
| Async data | HttpClient injected via DI, resource()/httpResource() expose value()/status()/error() as signals | await inside a <script> block, or a store (writable, readable) subscribed with $store |
| Bundle cost | Angular's runtime ships regardless of app size | Near-zero framework runtime — the compiler already resolved what changes |
| Structure enforced | Modules, services, DI hierarchy — opinionated by design | None enforced — a .svelte file is markup, script and style in one place |
| Learning curve | Steep — DI, RxJS (or signals), the module system | Shallow — closest of any major framework to writing plain HTML/JS |
| Where this repo's architecture lands | No DI container exists — Server Components are plain async functions | No compiler runtime either — Server Components ship zero client JS by default |
Both models assume the interesting question is "how does state get declared and tracked." Neither asks whether a given piece of UI needs a reactive-state primitive in the first place — and that's the question this codebase's own Client Component census actually answers.
What "needs a reactive primitive" turns out to mean here
This storefront ships 92 components. 24 of them are Client Components ("use client") — the ones needing any client-side JavaScript to work at all, whether Angular's injected signals, Svelte's compile-time state, or React's useState. Grepping those 24 for actual local state:
17 hold state via useState()
7 hold none — they exist for exactly one event handler or effect
The 7 with no state are not an edge case — they're a third of every component that needed "use client" at all:
// src/components/molecules/AuthCard.tsx — redirect after a Server Action succeeds
"use client";
const router = useRouter();
useEffect(() => {
if (formState.success) router.push("/dashboard");
}, [formState.success, router]);
// src/components/molecules/BlogCtaLink.tsx — fire an analytics event, render nothing new
"use client";
<Link onClick={() => track("blog_cta_clicked", { post, target })} href={href}>
{children}
</Link>
Neither of those needs a signal, a store, or a useState call — a router redirect and a click handler are both stateless. WishlistButton, NewsletterForm, PurchaseCta and ContactForm are the same shape: one effect or one handler, zero tracked values. Angular's DI container and Svelte's compiler both have to be reached for regardless — there's no smaller unit than "this component needs "use client"" in either framework's model — but neither model distinguishes "needs interactivity" from "needs state," and a third of this codebase's interactive components are proof the two aren't the same thing.
SSR and hydration: the part neither table row captures
Both frameworks now do server rendering, and both pay a hydration cost that Server Components in this codebase mostly don't. SvelteKit and Angular Universal both render HTML on the server, ship the framework runtime to the browser regardless, and re-run enough client-side logic to attach event listeners and reconcile state — hydration, the step that makes a server-rendered page interactive. Svelte's hydration is lighter because there's less runtime to wake up in the first place; Angular's Zone.js-based version has historically been the heavier of the two, which is part of why zoneless (signals-driven) change detection exists — it removes the automatic-everywhere patching Zone.js does and lets a component opt into exactly the reactivity it needs, closer to what Svelte's compiler already does at build time.
Neither framework's hydration model is "wrong" — a component that holds state has to run its logic in the browser eventually, whichever framework you're in. What Server Components change is which components have that obligation at all: a component that never needs "use client" doesn't hydrate, doesn't ship its logic to the browser, and isn't part of either framework's hydration cost. That's the same 24-of-92 split from above, looked at from the runtime-cost angle instead of the state-tracking one.
Where each model actually wins
Svelte wins outright on an app where most of the tree holds state — a dashboard, a canvas editor, a real-time board. There's no framework runtime to amortize, so the bundle-size argument already measured against React applies even harder against Angular's larger baseline.
Angular wins on a large team building something that has to stay structured as headcount grows — the DI container and module boundaries that feel like ceremony on a 5-component app are exactly what keeps a 500-component one navigable. The same DI-container argument, worked in detail against Next.js, is the fuller version of that tradeoff.
Neither wins the question this post actually measured: on a marketing site or storefront where most components render once and never change, both frameworks make you set up a reactive-state mechanism to ship a single click handler. Server Components sidestep that by not requiring the mechanism to exist unless a component actually opts in with "use client" — and even then, this codebase's own count shows a third of the time, nothing gets tracked.
Migrating between them
A team moving from Angular to Svelte (or the reverse) is porting more than syntax — it's porting the assumption about where state lives. An Angular service injected into six components becomes, in Svelte, either six independent local states or one shared store explicitly imported into each — there's no injector to walk up automatically, so the sharing has to become visible in the code. The direction that tends to surprise teams is Svelte-to-Angular: a handful of top-level let variables that worked fine without any structure suddenly need a service and a provider before Angular's change detection will notice them, even for state that never needed sharing in the first place. Neither codebase change is wrong — they're the same tradeoff the DI-container-versus-none argument above describes, just experienced as a migration instead of a starting choice.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| An Angular service works in one component, "no provider found" in another | The service isn't registered at a level both components' injectors can see | Register at the module or root level, or use providedIn: "root" |
| A Svelte 5 component doesn't re-render on a prop change | Still using let for a value the parent mutates, instead of $state()/$derived() | Runes need explicit opt-in in Svelte 5 — plain let is not automatically reactive across all cases the old $: label covered |
| Angular's zoneless mode breaks a third-party library | The library assumes Zone.js patches async APIs (setTimeout, fetch) to trigger change detection | Wrap the library's callback in NgZone.run(), or wait for a zoneless-compatible version |
| A React (or Svelte) developer new to Angular can't find where state "lives" | Looking for a local variable instead of an injected service | State that needs sharing lives in a service; state local to one component is a plain class field or signal |
| Reaching for a store/service for a value only one component reads | The instinct to centralize state is right for shared data, wrong for local UI state | Keep it local ($state(), a plain field) until a second component actually needs it |
FAQ
Is Svelte always smaller than Angular in production? For comparable functionality, yes — Angular ships a runtime (DI, change detection) regardless of app size, while Svelte's compiler emits only the DOM-update code your app actually uses.
Does Angular's signals API make it comparable to Svelte on reactivity?
Closer, not equal. Angular signals remove RxJS's manual-subscription boilerplate, but a signal still has to be constructed inside a component or service the DI system manages — Svelte's $state() is just a variable.
Which is better for a small team shipping fast? Svelte, on ramp-up time alone — there's no DI system or module structure to learn before writing a working component.
Does either framework distinguish components that need state from ones that just need an event handler? No — both treat "needs client-side behavior" as one category. This codebase's own count (17 of 24 Client Components hold state, 7 don't) is the kind of measurement neither framework's docs make you take, because their APIs don't ask the question.
Templates in this post
ASoc Keystone is a mortgage-lender website template, ASoc Ledger a finance-app landing page, and ASoc Magnet a lead-generation SaaS template — three landing pages built with exactly the Server Component split measured above.
Browse the full set: Next.js landing page templates and Tailwind landing page templates. For the two-way comparisons this post sits between, see Next.js vs. Angular and Next.js vs. Svelte.
