# ✅ Checklist Migration: React → Svelte 5
**Phiên bản:** Svelte 5 (Runes API) | **Cập nhật:** 2025
---
## 📋 Mục lục
1. [Chuẩn bị](#1-chuẩn-bị)
2. [Mapping React Hooks → Svelte Runes](#2-mapping-react-hooks--svelte-runes)
3. [Kiểm tra từng Component](#3-kiểm-tra-từng-component)
4. [Common Pitfalls](#4-common-pitfalls)
---
## 1. Chuẩn bị
### 1.1 Đọc tài liệu bắt buộc
- [ ] Đọc [Svelte 5 Runes docs](https://svelte.dev/docs/svelte/what-are-runes) — hiểu khái niệm compiler-based reactivity
- [ ] Đọc [Migration guide](https://svelte.dev/docs/svelte/v5-migration-guide) — nắm breaking changes từ Svelte 4
- [ ] Đọc [SvelteKit docs](https://svelte.dev/docs/kit/introduction) — nếu project dùng routing/SSR
- [ ] Xem qua [Svelte 5 Playground](https://svelte.dev/playground) — thử các runes cơ bản trước khi code thật
### 1.2 Cài đặt môi trường
- [ ] Node.js >= 18.x đã được cài
- [ ] Khởi tạo project SvelteKit mới
```bash
npx sv create my-app
# Chọn: SvelteKit minimal → TypeScript → Svelte 5
```
- [ ] Hoặc thêm Svelte 5 vào project Vite hiện có
```bash
npm install svelte@^5 @sveltejs/vite-plugin-svelte@^4
```
- [ ] Kiểm tra `svelte.config.js` đã đúng
```js
// svelte.config.js
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
};
```
- [ ] Kiểm tra `vite.config.js` / `vite.config.ts`
```ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()],
});
```
### 1.3 Setup công cụ hỗ trợ
- [ ] Cài VS Code extension **Svelte for VS Code** (`svelte.svelte-vscode`)
- [ ] Cài ESLint + Prettier cho Svelte
```bash
npm install -D eslint-plugin-svelte prettier-plugin-svelte
```
- [ ] Cấu hình TypeScript — thêm vào `tsconfig.json`
```json
{
"compilerOptions": {
"moduleResolution": "bundler",
"verbatimModuleSyntax": true
}
}
```
- [ ] Cài Vitest để chạy unit test tương đương Jest/RTL
```bash
npm install -D vitest @testing-library/svelte
```
### 1.4 Lập kế hoạch migrate
- [ ] Liệt kê toàn bộ components cần convert (dùng `find . -name "*.tsx" -o -name "*.jsx"`)
- [ ] Phân loại theo độ phức tạp: **Simple** (chỉ state/props) → **Medium** (hooks, context) → **Complex** (HOC, render props)
- [ ] Ưu tiên migrate từ **leaf components** (không có children) lên dần
- [ ] Giữ React và Svelte chạy song song trong giai đoạn chuyển đổi nếu project lớn
---
## 2. Mapping React Hooks → Svelte Runes
> 💡 **Nguyên tắc chung:** Runes là **compiler directives** — chúng chỉ hoạt động trong `.svelte` file hoặc `.svelte.ts/.svelte.js` file. Không phải runtime functions như React hooks.
---
### 2.1 `useState` → `$state`
| React | Svelte 5 |
|-------|----------|
| `const [count, setCount] = useState(0)` | `let count = $state(0)` |
| `setCount(5)` | `count = 5` |
| `setCount(prev => prev + 1)` | `count += 1` |
| `const [obj, setObj] = useState({a: 1})` | `let obj = $state({a: 1})` |
| `setObj({...obj, a: 2})` | `obj.a = 2` *(deep reactive)* |
**React:**
```tsx
// React
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: 'An', age: 25 });
return (
);
}
```
**Svelte 5:**
```svelte
```
> ⚠️ **Khác biệt quan trọng:** `$state` object hỗ trợ **deep reactivity** — mutate trực tiếp property là OK. Không cần `setState` hay spread operator.
---
### 2.2 `useMemo` → `$derived`
| React | Svelte 5 |
|-------|----------|
| `useMemo(() => a + b, [a, b])` | `$derived(a + b)` |
| `useMemo(() => list.filter(...), [list])` | `$derived(list.filter(...))` |
| Logic phức tạp nhiều bước | `$derived.by(() => { ... })` |
| Dependency array thủ công `[a, b]` | Tự động track — không cần khai báo |
**React:**
```tsx
// React
function ProductList({ products, category }) {
const filtered = useMemo(
() => products.filter(p => p.category === category),
[products, category]
);
const total = useMemo(
() => filtered.reduce((sum, p) => sum + p.price, 0),
[filtered]
);
const summary = useMemo(() => {
const avg = total / filtered.length;
const max = Math.max(...filtered.map(p => p.price));
return { avg, max, count: filtered.length };
}, [filtered, total]);
return
{summary.count} sản phẩm, TB: {summary.avg}
;
}
```
**Svelte 5:**
```svelte
{summary.count} sản phẩm, TB: {summary.avg}
```
> ✅ **Lợi thế:** Svelte tự động phát hiện dependencies. Không bao giờ bị lỗi "missing dependency" như eslint-plugin-react-hooks.
---
### 2.3 `useEffect` → `$effect`
| React | Svelte 5 |
|-------|----------|
| `useEffect(() => {}, [])` — run once | Không có tương đương trực tiếp — dùng `onMount` |
| `useEffect(() => {}, [dep])` — run on change | `$effect(() => { /* đọc dep ở đây */ })` |
| `useEffect(() => { return cleanup }, [dep])` | `$effect(() => { return () => cleanup() })` |
| `useEffect(() => {})` — run every render | `$effect(...)` — tương đương nhưng chỉ khi deps thay đổi |
**React:**
```tsx
// React
function SearchBox({ query }) {
const [results, setResults] = useState([]);
// Run once on mount
useEffect(() => {
console.log('Component mounted');
return () => console.log('Component unmounted');
}, []);
// Run when query changes
useEffect(() => {
if (!query) return;
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(r => r.json())
.then(setResults)
.catch(err => {
if (err.name !== 'AbortError') console.error(err);
});
// Cleanup: hủy request cũ khi query đổi
return () => controller.abort();
}, [query]);
return
{results.map(r =>
{r.name}
)}
;
}
```
**Svelte 5:**
```svelte
{#each results as r (r.id)}
{r.name}
{/each}
```
> ⚠️ **Quan trọng:** `$effect` **KHÔNG chạy trong SSR** (server-side rendering). Dùng `$effect.pre()` nếu cần chạy trước khi DOM update.
---
### 2.4 `useRef` → `$state` hoặc `bind:this`
| React | Svelte 5 |
|-------|----------|
| `useRef(null)` → DOM element | `let el = $state()` + `bind:this={el}` |
| `useRef(value)` → mutable, không trigger re-render | `let val = $state.raw(value)` |
| `ref.current` | `el` trực tiếp |
**React:**
```tsx
// React
function VideoPlayer() {
const videoRef = useRef(null);
const playCountRef = useRef(0); // Không trigger re-render
const play = () => {
videoRef.current?.play();
playCountRef.current += 1;
console.log('Played:', playCountRef.current, 'times');
};
return ;
}
```
**Svelte 5:**
```svelte
```
---
### 2.5 `useContext` → Svelte Context API
| React | Svelte 5 |
|-------|----------|
| `createContext(defaultValue)` | Dùng unique key (string hoặc object) |
| `` | `setContext(key, value)` trong component cha |
| `useContext(MyContext)` | `getContext(key)` trong component con |
| Context + useState để reactive | Context + `$state` object |
**React:**
```tsx
// React
const ThemeContext = createContext<'light' | 'dark'>('light');
function ThemeProvider({ children }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
return (
{children}
);
}
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
);
}
```
**Svelte 5:**
```svelte
{@render children()}
```
```svelte
```
> 💡 **Best practice:** Export `THEME_KEY` từ một file `context.ts` riêng để dùng chung giữa provider và consumer.
---
### 2.6 `useCallback` → Không cần
| React | Svelte 5 |
|-------|----------|
| `useCallback(fn, [deps])` | **Không cần** — functions trong Svelte không re-create mỗi render |
| Memoize để tránh re-render children | Dùng Svelte reactivity — components chỉ update khi props thực sự đổi |
**React:**
```tsx
// React — cần useCallback để tránh re-render ListItem
function List({ items }) {
const [filter, setFilter] = useState('');
const handleClick = useCallback((id: string) => {
console.log('clicked', id);
}, []); // deps rỗng
const handleDelete = useCallback((id: string) => {
setFilter(prev => prev.includes(id) ? prev : prev + id);
}, [filter]); // cần filter trong deps
return items.map(item => (
));
}
```
**Svelte 5:**
```svelte
{#each items as item (item.id)}
{/each}
```
---
### 2.7 `React.memo` → Không cần (hầu hết trường hợp)
| React | Svelte 5 |
|-------|----------|
| `React.memo(Component)` | **Không cần** — Svelte chỉ update DOM khi cần |
| `React.memo(Comp, compareFn)` | Không có tương đương — thường không cần |
> ✅ Svelte compile ra code DOM manipulation trực tiếp, không có Virtual DOM diffing. Component children chỉ update đúng phần thay đổi.
---
### 2.8 Props
| React | Svelte 5 |
|-------|----------|
| `function Comp({ name, age }: Props)` | `let { name, age } = $props()` |
| Default props: `{ count = 0 }` | `let { count = 0 } = $props()` |
| Rest props: `{ style, ...rest }` | `let { style, ...rest } = $props()` |
| `children` (ReactNode) | `children` snippet — `{@render children?.()}` |
| `onChange` callback | Convention: `on` prefix — `onclick`, `onchange` |
| `className` | `class` (Svelte dùng tên gốc HTML) |
**React:**
```tsx
// React
interface CardProps {
title: string;
count?: number;
onSave: (value: string) => void;
children: React.ReactNode;
className?: string;
}
function Card({ title, count = 0, onSave, children, className }: CardProps) {
return (
{title} ({count})
{children}
);
}
```
**Svelte 5:**
```svelte
{title} ({count})
{@render children()}
```
---
### 2.9 Bảng tổng hợp nhanh
| React | Svelte 5 | Ghi chú |
|-------|----------|---------|
| `useState(v)` | `$state(v)` | Mutate trực tiếp thay vì setter |
| `useMemo(fn, deps)` | `$derived(expr)` | Auto-track deps |
| `useMemo(fn, deps)` — complex | `$derived.by(fn)` | Cho logic nhiều bước |
| `useEffect(fn, [])` | `onMount(fn)` | Import từ `svelte` |
| `useEffect(fn, [deps])` | `$effect(fn)` | Auto-track deps |
| `useEffect cleanup` | `return () => {}` trong `$effect` | Cú pháp giống nhau |
| `useRef(null)` — DOM | `bind:this={el}` | El là `$state` |
| `useRef(val)` — mutable | `$state.raw(val)` | Không trigger reactivity |
| `useContext(Ctx)` | `getContext(key)` | Import từ `svelte` |
| `Context.Provider` | `setContext(key, val)` | Gọi trong script |
| `useCallback(fn, deps)` | Viết fn bình thường | Không cần |
| `React.memo` | Không cần | Svelte efficient mặc định |
| `className` | `class` | Tên attribute HTML gốc |
| `children: ReactNode` | `children: Snippet` | `{@render children()}` |
---
## 3. Kiểm tra từng Component
> 📝 **Hướng dẫn sử dụng:** Copy checklist này, điền tên component, check từng mục sau khi convert.
---
### Template Checklist cho mỗi Component
```
Component: ______________________
File gốc (.tsx/.jsx): ____________
File mới (.svelte): ______________
Ngày convert: ___________________
```
---
### 3.1 Kiểm tra State
- [ ] Tất cả `useState` đã được thay bằng `$state`
- [ ] State object/array được mutate trực tiếp (không còn spread `{...obj}` không cần thiết)
- [ ] State array: dùng `.push()`, `.splice()` thay vì `[...arr, newItem]` *(vẫn hoạt động nhưng không cần thiết)*
- [ ] Không còn setter function (`setCount`, `setUser`, ...) — thay bằng assignment trực tiếp
- [ ] State hoạt động đúng khi test thủ công: thay đổi state → UI update
- [ ] State phức tạp (nested object) deep reactivity hoạt động đúng
```svelte
```
---
### 3.2 Kiểm tra Derived Values
- [ ] Tất cả `useMemo` đã được thay bằng `$derived` hoặc `$derived.by`
- [ ] Không còn dependency array thủ công `[a, b, c]`
- [ ] `$derived` tự động update khi dependencies thay đổi — test bằng cách thay đổi source state
- [ ] Logic trong `$derived.by` không có side effects (không gọi fetch, không modify state ngoài)
- [ ] Computed value hiển thị đúng giá trị ban đầu (không phải `undefined`)
- [ ] Kiểm tra edge case: derived khi state là empty array / null / 0
---
### 3.3 Kiểm tra Effects
- [ ] `useEffect(() => {}, [])` đã được thay bằng `onMount`
- [ ] `onMount` được import từ `'svelte'`
- [ ] `useEffect` có dependencies đã được thay bằng `$effect` với dependencies được đọc **bên trong** function
- [ ] Cleanup function trả về đúng (`return () => { ... }`)
- [ ] Test cleanup: navigate away khỏi component → cleanup chạy (check console/network tab)
- [ ] Effect không bị infinite loop (không write vào state đang được read trong cùng effect)
```svelte
```
- [ ] `$effect` chỉ chứa side effects thực sự (DOM manipulation, fetch, subscription) — không chứa derived logic
---
### 3.4 Kiểm tra Props
- [ ] `const { prop1, prop2 } = $props()` thay thế destructuring trong function params
- [ ] Default values khai báo đúng trong destructuring: `let { count = 0 } = $props()`
- [ ] TypeScript: `$props()` có type đúng
- [ ] `className` → `class` (và handle conflict với reserved word: `class: className`)
- [ ] Event callbacks: `onClick` → `onclick`, `onChange` → `onchange` (lowercase)
- [ ] `children` prop: dùng `Snippet` type và `{@render children()}`
- [ ] Optional children: `{@render children?.()}` để tránh lỗi khi không truyền
- [ ] Test: truyền props từ parent → component nhận và hiển thị đúng
- [ ] Test: thay đổi props từ parent → component re-render đúng
---
### 3.5 Kiểm tra Context
- [ ] `setContext(key, value)` được gọi trong component cha **synchronously** (không bên trong effect)
- [ ] Context value chứa `$state` reactive: dùng getter `get prop() { return stateVar }` thay vì truyền value trực tiếp
- [ ] `getContext(key)` được gọi với **đúng key** (export key ra file riêng nếu cần)
- [ ] Context không undefined khi consumer render (provider wrap đúng chỗ)
- [ ] Test: thay đổi context value ở provider → consumer tự update
---
### 3.6 Kiểm tra Template / Markup
- [ ] `className` → `class`
- [ ] `htmlFor` → `for`
- [ ] `onClick` → `onclick`, `onChange` → `onchange`, `onSubmit` → `onsubmit`
- [ ] JSX expressions `{condition && }` → `{#if condition}{/if}`
- [ ] Ternary phức tạp → `{#if}{:else}{/if}`
- [ ] `.map()` render list → `{#each items as item (item.id)}{/each}`
- [ ] Key prop `key={id}` → `(id)` trong `#each`
- [ ] Fragments `<> >` → không cần (Svelte hỗ trợ multiple root elements)
- [ ] `dangerouslySetInnerHTML={{ __html: str }}` → `{@html str}`
---
### 3.7 Kiểm tra TypeScript
- [ ] Import types đúng: `import type { Snippet } from 'svelte'`
- [ ] `$props()` có generic type
- [ ] Event handlers có type đúng: `(e: MouseEvent) => void`
- [ ] Không còn type lỗi trong IDE
- [ ] Chạy `tsc --noEmit` hoặc `svelte-check` — 0 errors
```bash
npx svelte-check --tsconfig ./tsconfig.json
```
---
### 3.8 Kiểm tra cuối cùng
- [ ] Chạy app — component render không có console error
- [ ] Kiểm tra Network tab — không có request bị duplicate/leak
- [ ] Test tất cả interactive features (click, input, form submit)
- [ ] Test trên mobile viewport nếu có responsive logic
- [ ] Unit test (nếu có) pass: `npm run test`
- [ ] Xóa file React gốc sau khi confirm OK
---
## 4. Common Pitfalls
### ❌ Pitfall 1: Dùng Runes ngoài `.svelte` file
**Vấn đề:** Runes là compiler directives — chỉ hoạt động trong `.svelte` hoặc `.svelte.ts`/`.svelte.js`
```ts
// ❌ SAI — utils.ts (file thường)
export function useCounter() {
let count = $state(0); // Lỗi! $state không được nhận diện
return { count };
}
```
```ts
// ✅ ĐÚNG — counter.svelte.ts (chú ý .svelte.ts)
export function useCounter() {
let count = $state(0); // OK!
return {
get count() { return count; },
increment: () => count++
};
}
```
```ts
// ✅ ĐÚNG — hoặc dùng class pattern
export class Counter {
count = $state(0); // OK trong .svelte.ts
increment() { this.count++; }
}
```
**Checklist:**
- [ ] Mọi file dùng runes có đuôi `.svelte` hoặc `.svelte.ts`/`.svelte.js`
- [ ] Shared stateful logic được tách ra file `.svelte.ts` riêng
---
### ❌ Pitfall 2: Dependency Tracking trong Conditional / Async
**Vấn đề:** Svelte chỉ track dependencies được **đọc synchronously** trong `$effect` / `$derived`. Dependency bên trong `if` hoặc `async` có thể không được track.
```svelte
```
```svelte
```
**Checklist:**
- [ ] Tất cả state cần track được đọc **trước** block `if/else` đầu tiên trong `$effect`
- [ ] Không dùng `await` trực tiếp trong `$effect` — wrap trong IIFE async hoặc dùng helper
- [ ] Test: thay đổi từng dependency một → effect có re-run không
---
### ❌ Pitfall 3: Reactive Mutation Arrays/Objects — Mất Reactivity
**Vấn đề:** Reassign toàn bộ array/object trong một số trường hợp, hoặc dùng non-reactive methods.
```svelte
```
```svelte
```
**Checklist:**
- [ ] Không lưu `$state` vào biến intermediate rồi reassign biến đó
- [ ] Array operations: dùng `.push()`, `.pop()`, `.splice()`, `.sort()` trực tiếp trên biến `$state`
- [ ] Không dùng `Object.assign({}, state, changes)` — thay bằng mutate property hoặc reassign
- [ ] Test: thêm/xóa item khỏi list → UI cập nhật đúng
---
### ❌ Pitfall 4: Quên Cleanup trong `$effect`
```svelte
```
**Checklist:**
- [ ] Mọi `setInterval` / `setTimeout` trong `$effect` có `clearInterval` / `clearTimeout` trong cleanup
- [ ] WebSocket / EventSource connections có `close()` trong cleanup
- [ ] `addEventListener` có `removeEventListener` tương ứng trong cleanup
- [ ] Fetch requests có `AbortController.abort()` trong cleanup
---
### ❌ Pitfall 5: `$props()` Destructuring Sai
```svelte
```
```svelte
```
**Checklist:**
- [ ] `$props()` chỉ được gọi **một lần** trong mỗi component
- [ ] Destructure trực tiếp trong lời gọi `$props()`, không qua intermediate
---
### ❌ Pitfall 6: `$effect` Không Phải Thay Thế Cho `$derived`
```svelte
```
**Nguyên tắc:**
- `$derived` → **tính toán giá trị** từ state khác (synchronous, no side effects)
- `$effect` → **side effects** (fetch, DOM, subscriptions, logging)
**Checklist:**
- [ ] Không có `$effect` nào chỉ dùng để gán giá trị cho state khác
- [ ] Logic tính toán thuần túy → `$derived`
- [ ] Fetch / DOM manipulation / console.log → `$effect`
---
## 📊 Tóm tắt bảng tra cứu nhanh
| # | Pitfall | Triệu chứng | Fix |
|---|---------|-------------|-----|
| 1 | Runes ngoài `.svelte` | Build error, `$state is not defined` | Đổi sang `.svelte.ts` |
| 2 | Async dependency tracking | Effect không re-run khi dep đổi | Đọc deps synchronously trước async |
| 3 | Mutation mất reactivity | UI không update sau thay đổi array/obj | Mutate trực tiếp hoặc reassign biến gốc |
| 4 | Thiếu cleanup | Memory leak, timer chạy nhiều lần | `return () => cleanup()` trong `$effect` |
| 5 | `$props()` sai cách | Props không reactive, type error | Destructure trực tiếp trong `$props()` |
| 6 | `$effect` thay `$derived` | Frame delay, logic khó debug | Dùng `$derived` cho computed values |
---
## 🖨️ Ghi chú khi in
> In trang này với **khổ A4, portrait**, font size 10-11, margins 15mm.
> Dùng bút để check các ô `[ ]` khi hoàn thành.
> Mỗi component nên dùng một bản copy của **Section 3**.
---
*Checklist này áp dụng cho Svelte 5.x (Runes API). Kiểm tra [svelte.dev/docs](https://svelte.dev/docs) nếu có thay đổi từ phiên bản mới hơn.*