I’m slowly moving my projects to Svelte from React
My side projects don’t need a hiring pipeline. They don’t need a component library with 200 contributors. They don’t need the ecosystem…
I’m slowly moving my projects to Svelte from React
My side projects don’t need a hiring pipeline. They don’t need a component library with 200 contributors. They don’t need the ecosystem that powers half the internet. What they need is for me to sit down on a Saturday afternoon, open the code, and not spend thirty minutes remembering why I wrote that useEffect the way I did.
That’s roughly when I started moving things to Svelte.
Photo by Lautaro Andreani on Unsplash
The React fatigue is real
I’ve been writing React for about six years. At work, it’s the right call: the team knows it, the component library is built on it, onboarding new engineers is straightforward. That’s not changing.
But on personal projects, I started noticing a specific type of friction. Not bugs, not crashes. Overhead. The overhead of setting up a new project: create-react-app is dead, so now you're choosing between Vite, Next.js, Remix, or something else. Then you add TypeScript, ESLint, Prettier, a router if you need one, a state manager if your app grows, a data-fetching library because fetch alone doesn't cut it for caching. You're easily at 20+ packages before you've written a line of your own code.
Then there are hooks. I don’t hate hooks. The mental model is fine once it clicks. But the rules never fully disappear from your mind: don’t call them conditionally, don’t call them in loops, declare every dependency in the dependency array, or stale closures will find you eventually. The exhaustive-deps ESLint rule is there to help, and it does, but it also means you’re often writing this:
useEffect(() => {
fetchUserPosts(userId);
}, [userId, fetchUserPosts]);
And then fetchUserPosts is either memoized with useCallback or it's triggering the effect on every render. So now you have:
const fetchUserPosts = useCallback(async (id: string) => {
const response = await fetch(`/api/posts?userId=${id}`);
return response.json();
}, []);
useEffect(() => {
fetchUserPosts(userId);
}, [userId, fetchUserPosts]);
This is not wrong. It’s how React works. But for a side project where I’m the only developer, it’s a considerable amount of ceremony.
What made me try Svelte
I kept seeing Svelte pop up in the State of JS survey in the satisfaction rankings. Not in usage, but in the “would use again” column. Developers who had used it weren’t leaving. The 2025 survey put Svelte 5 at 91% retention, the highest of any frontend framework.
The pitch that caught my attention wasn’t “no virtual DOM” or “smaller bundles” (though both are true). It was the .svelte file format. You write HTML at the top, a <script> block with your logic, and a <style> block for CSS. It looks like what the web is. There's no JSX mental translation, no className instead of class, no importing CSS modules.
I converted a small personal dashboard tool. It took a weekend. And then I didn’t go back.
Svelte 5 runes: what the reactivity looks like
Svelte 5 shipped runes as its new reactivity model. If you’re coming from React, the mapping is direct enough that the learning curve is shallow.
$state vs useState
In React:
import { useState, useCallback } from "react";
function PostEditor() {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [isDraft, setIsDraft] = useState(true);
const handlePublish = useCallback(() => {
setIsDraft(false);
}, []);
return (
<div>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<textarea value={content} onChange={(e) => setContent(e.target.value)} />
<button onClick={handlePublish}>Publish</button>
<p>Status: {isDraft ? "Draft" : "Published"}</p>
</div>
);
}
In Svelte 5:
<script lang="ts">
let title = $state("");
let content = $state("");
let isDraft = $state(true);
function handlePublish() {
isDraft = false;
}
</script>
<input bind:value={title} />
<textarea bind:value={content}></textarea>
<button onclick={handlePublish}>Publish</button>
<p>Status: {isDraft ? "Draft" : "Published"}</p>
No setter functions. You update isDraft like a normal variable. The compiler knows it's reactive because you declared it with $state. Two-way binding with bind:value means no onChange handlers for inputs.
For objects and arrays, $state creates a deeply reactive proxy. If you have an array of posts, you can call posts.push(newPost) and the UI updates. In React you'd spread or create a new array to trigger a re-render.
$derived vs useMemo
In React, deriving a value from state means useMemo with a dependency array:
const publishedPosts = useMemo(
() => posts.filter((post) => !post.isDraft),
[posts]
);
const wordCount = useMemo(
() => content.split(" ").filter(Boolean).length,
[content]
);
In Svelte 5:
<script lang="ts">
let posts = $state<Post[]>([]);
let content = $state("");
let publishedPosts = $derived(posts.filter((post) => !post.isDraft));
let wordCount = $derived(content.split(" ").filter(Boolean).length);
</script>
No dependency arrays. Svelte figures out what $derived depends on by watching what it reads. If content changes, wordCount recalculates. That's it.
$effect vs useEffect
This is where the biggest ergonomic difference shows up. In React:
// post and postId state declared elsewhere in the component
useEffect(() => {
if (!postId) return;
const controller = new AbortController();
fetch(`/api/posts/${postId}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => setPost(data));
return () => controller.abort();
}, [postId]);
In Svelte 5:
<script lang="ts">
let postId = $state<string | null>(null);
let post = $state<Post | null>(null);
$effect(() => {
if (!postId) return;
const controller = new AbortController();
fetch(`/api/posts/${postId}`, { signal: controller.signal })
.then((res) => res.json())
.then((data) => { post = data; });
return () => controller.abort();
});
</script>
The code structure is nearly identical. The difference: no dependency array. Svelte tracks that postId is read synchronously inside the effect and automatically re-runs when it changes. The cleanup function works the same way.
One thing Svelte does that React doesn’t: it discourages using $effect to synchronize state. If you find yourself writing $effect(() => { derivedValue = someState * 2; }), that's a sign you should use $derived instead. The framework nudges you toward the right pattern instead of letting you stumble into it.
Watch out: The automatic dependency tracking in
$effectonly captures values read synchronously. If you read a$statevariable after anawait, Svelte won't track it as a dependency and the effect won't re-run when that value changes.
SvelteKit for routing
I was on Next.js for most of my side projects that needed routing. SvelteKit is the Svelte equivalent and the comparison is fair.
SvelteKit uses filesystem-based routing with a src/routes directory, similar to Next.js App Router. A file at src/routes/posts/[slug]/+page.svelte creates a dynamic route accessible at /posts/my-post-title. Server-side data loading goes in +page.server.ts, which only runs on the server and never ships to the browser.
The part that surprised me: you navigate with plain <a> elements. No <Link> component to import, no router.push() to remember. SvelteKit intercepts <a> clicks automatically and handles client-side navigation. It feels closer to how HTML works.
For deployment, SvelteKit uses adapters: one for Node, one for serverless, one for Cloudflare Workers, one for static output. You swap the adapter in the config. The Cloudflare adapter produces a roughly 25KB worker bundle vs the roughly 95KB you get with Next.js, which translates to noticeably faster cold starts at the edge.
What I genuinely miss from React
This isn’t a “Svelte is better in every way” article, so here’s the honest part.
- The component library ecosystem. shadcn/ui is one of the best things that happened to frontend development in recent years. Copy-paste components, full TypeScript, no hidden abstractions. There’s no Svelte equivalent with the same breadth. Skeleton UI and shadcn-svelte exist and are decent, but they’re catching up, not leading.
- TanStack Query. The React Query model for server state, caching, background refetching, and optimistic updates is excellent. TanStack Query now has a Svelte adapter, but it was clearly designed with React as the primary target. Some patterns feel slightly awkward.
- react-hook-form. For complex forms with validation, react-hook-form with zod is hard to beat. Svelte has Superforms, which is good and integrates well with SvelteKit’s form actions, but the ecosystem depth isn’t there yet.
- The error messages. React has spent years making its error messages useful. The Svelte compiler errors are good but not at the same level of polish for edge cases.
When I pick Svelte now
Personal projects and side projects where I’m the sole developer. Anything where the final bundle size matters: dashboards on slow connections, tools that run on embedded devices, marketing pages where Core Web Vitals affect real users. Prototypes where I want to move from idea to something in a browser as fast as possible.
The bundle size difference is concrete. React’s runtime alone is around 45KB gzipped. A comparable todo app compiled with Svelte totals around 3.6KB, framework included. For a side project, that means near-instant loads on mobile networks. For a content site, it means better Lighthouse scores without any optimization work.
The developer experience feels lighter. There’s less framework surface area to hold in your head. A .svelte file reads top to bottom: script, markup, styles. No separate file for component logic, no CSS module, no barrel export. It's one file.
When I stay on React
Any project with a team. The hiring reality is stark: React appears in around 65–70% of frontend job listings that specify a framework. Svelte appears in 5–8%. If you’re building something that other people will maintain, optimize for the thing they already know.
Any project where you need the React ecosystem depth. If you need date pickers, drag-and-drop, rich text editors, data grids, and charting all in the same app, React has more mature options for all of them. That gap is closing but it’s still real.
Any project where someone has already built the domain-specific component library in React. At my day job, we have an internal design system in React. That’s not getting rewritten.
The honest summary
React fatigue is real and worth acknowledging. But the answer for most professional work is still “stay on React and manage the complexity,” not “migrate the whole stack.” What I’ve found is that personal projects are the right place to try something different, and Svelte consistently gets out of my way more than I expected.
The rune-based reactivity in Svelte 5 solves specific problems I had with hooks: no dependency arrays to get wrong, no stale closure bugs, no useCallback wrapping functions to keep effects stable. The code I write is shorter and the things that can go wrong are fewer.
For side projects, I’m not looking back. For the day job, React isn’t going anywhere, and that’s fine too. The two can coexist. You don’t have to pick one framework as the answer to everything.
If you’ve been curious about Svelte and have a small project that needs a weekend rewrite, that’s the right entry point. The official docs at svelte.dev and the interactive tutorial there will get you up to speed in a few hours. SvelteKit’s docs are equally good.
Start small. Port one project. See how it feels after a month.
메타데이터
- post_id
- 12fe0d1e30ca
- slug
- im-slowly-moving-my-projects-to-svelte-from-react-12fe0d1e30ca
- url
- https://medium.com/@sarathm09/im-slowly-moving-my-projects-to-svelte-from-react-12fe0d1e30ca
- canonical_url
- https://medium.com/@sarathm09/im-slowly-moving-my-projects-to-svelte-from-react-12fe0d1e30ca
- author_url
- https://medium.com/@sarathm09
- status
- ok
- fetched_at
- 2026-06-17 13:53:50