Đang tải...

Ếch Trendy

Cùng chú ếch nhỏ khám phá thế giới Web đầy biến động. Cập nhật trending nhanh như cách ếch đớp mồi! ⌨️🌿


0

Svelte 5 Signals và Runes: Reactivity Model Mới Thách Thức React Hooks

Ếch Trendy
Ếch Trendy

5 tháng trước · 9 phút đọc

Tại sao Svelte 5 runes lại đáng để bạn quan tâm?

Mình nhớ lần đầu đọc về runes trong Svelte 5. Phản ứng đầu tiên: "Ủa, thêm một cú pháp lạ nữa à?" Nhưng sau khi build thử một app nhỏ, mình nhận ra đây không phải syntax mới cho có - đây là cách tiếp cận reactivity hoàn toàn khác.

React Hooks hoạt động theo kiểu: state thay đổi → re-render toàn bộ component → React diff virtual DOM → cập nhật DOM thật. Mỗi lần useState trigger, React phải chạy lại toàn bộ function component. Với component đơn giản thì không sao. Nhưng khi logic phức tạp lên, bạn bắt đầu phải dùng useMemo, useCallback, React.memo để "chặn" re-render thừa - và code bắt đầu cồng kềnh.

Svelte 5 runes làm khác. Thay vì re-render component, framework theo dõi chính xác từng giá trị nào đang được đọc ở đâu, và chỉ cập nhật đúng phần DOM liên quan khi giá trị đó thay đổi. Đây là fine-grained reactivity - hay còn gọi là signals.

343698 Signals cập nhật DOM trực tiếp, không cần diff toàn bộ component tree

Svelte không phải framework duy nhất đi theo hướng này. SolidJS đã làm điều tương tự từ sớm, Angular gần đây cũng bổ sung signals, và ngay cả React đang có RFC thảo luận về signals. Điều đó nói lên gì? Signals không phải trend nhất thời - đây là hướng đi mà cộng đồng frontend đang dần đồng thuận.


Ba rune cốt lõi bạn cần biết: $state, $derived, $effect

Svelte 5 đưa ra hệ thống runes gồm ba primitive chính. Đơn giản hơn nhiều so với bộ hooks của React nếu tính số thứ bạn phải nhớ.

$state - khai báo reactive state:

<script>
  // Thay vì: const [count, setCount] = useState(0)
  let count = $state(0);
</script>

<button onclick={() => count++}>{count}</button>

Khác biệt rõ nhất: bạn mutate trực tiếp (count++) thay vì gọi setter. Svelte compiler biến đổi code này thành signal subscription ở compile time.

$derived - giá trị tính từ state khác:

<script>
  let todos = $state([]);
  // Tự động re-tính khi todos thay đổi
  let done = $derived(todos.filter(t => t.completed).length);
  let remaining = $derived(todos.length - done);
</script>

Tương đương useMemo của React, nhưng không cần khai báo dependency array. Svelte tự track những gì bạn đọc bên trong $derived.

$effect - side effects:

<script>
  let query = $state('');

  $effect(() => {
    // Chạy lại mỗi khi query thay đổi
    console.log('Tìm kiếm:', query);
    // Cleanup (nếu cần)
    return () => console.log('cleanup');
  });
</script>

343699 Ba rune tương ứng với useState, useMemo, useEffect nhưng ít boilerplate hơn rõ rệt

Vấn đề useEffect trong React là bạn phải tự quản lý dependency array - thiếu một dependency là bug, thừa là performance tệ. $effect của Svelte tự động track dependency bằng cách theo dõi những signal nào được đọc trong lần chạy đầu tiên.

Trade-off cần nhận rõ: cách auto-tracking này đôi khi khó đoán hơn với logic phức tạp. Nếu bạn đọc một signal bên trong if điều kiện, Svelte chỉ track nó khi nhánh đó thực sự chạy. Cần quen với cách nghĩ này mới tránh được bug lạ.


Shared state không cần store phức tạp

Đây là phần mình thích nhất ở Svelte 5. Không cần Redux, Zustand, hay Pinia - shared state chỉ là một file JS thông thường.

// stores/todos.svelte.js
export let todos = $state([]);
export let filter = $state('all');

// $derived tự cập nhật khi todos hoặc filter thay đổi
export let filtered = $derived(
  filter === 'all'
    ? todos
    : todos.filter(t => t.completed === (filter === 'done'))
);

export function addTodo(text) {
  todos.push({ id: Date.now(), text, completed: false });
}

export function toggleTodo(id) {
  const todo = todos.find(t => t.id === id);
  if (todo) todo.completed = !todo.completed;
}

Import và dùng trong bất kỳ component nào:

<!-- TodoList.svelte -->
<script>
  import { filtered, toggleTodo } from '../stores/todos.svelte.js';
</script>

{#each filtered as todo}
  <li onclick={() => toggleTodo(todo.id)}>
    {todo.text}
  </li>
{/each}

343700 Shared state dùng module JS thông thường - không cần provider, context hay store boilerplate

So sánh với React: để share state tương tự, bạn cần createContext, useContext, wrap component trong Provider, và nếu cần global thì setup thêm Zustand/Redux. Svelte chỉ cần một file .svelte.js với $state.

Tại sao được? Vì $state trong Svelte 5 không bị ràng buộc trong component lifecycle như useState. Nó là signal thực sự - có thể tồn tại bên ngoài component và được subscribe từ nhiều nơi đồng thời.

Nếu bạn đang học JavaScript nền tảng và muốn hiểu rõ hơn về modules, closures, hay cách JS quản lý state, khóa Lập Trình JavaScript Nâng Cao của F8 cover khá kỹ các concept này - giúp bạn hiểu tại sao cách tiếp cận của Svelte lại work.


So sánh trực tiếp: Todo app trong Svelte vs React

Mình build cùng một todo app với đủ tính năng: thêm/xóa/toggle/filter task. Kết quả về bundle size khá thú vị.

Bundle size sau khi build production (gzip):

React + Vite Svelte 5 + Vite
Framework runtime ~60KB (gzip) (Hacker News discussion) ~5-8KB (gzip)
App code ~3-5KB ~3-5KB
Tổng ~45-50KB ~8-12KB

Tại sao Svelte nhỏ hơn nhiều? React ship toàn bộ virtual DOM runtime xuống browser - client phải chạy diff algorithm ở runtime. Svelte làm ngược lại: compiler phân tích component ở build time, tạo ra imperative DOM update code không cần runtime framework. Output là JavaScript thuần, không có Svelte runtime ở giữa.

Về developer experience, mình thấy Svelte 5 cần ít code hơn rõ rệt cho cùng chức năng:

<!-- Svelte 5: toàn bộ todo logic -->
<script>
  import { todos, filtered, filter, addTodo, toggleTodo } from './store.svelte.js';
  let input = $state('');
</script>

<input bind:value={input} />
<button onclick={() => { addTodo(input); input = ''; }}>Thêm</button>

{#each filtered as todo (todo.id)}
  <li class:done={todo.completed} onclick={() => toggleTodo(todo.id)}>
    {todo.text}
  </li>
{/each}

<span>{todos.length} tasks</span>

343701 Bundle size nhỏ hơn giúp first load nhanh hơn - quan trọng với người dùng mobile 4G

Trade-off thật sự: ecosystem. React có hàng nghìn thư viện UI, headless component, animation library. Svelte ecosystem nhỏ hơn nhiều. Nếu project cần Radix UI, React Query, hay Framer Motion, bạn phải tự build hoặc tìm alternative cho Svelte. Đây là lý do Svelte vẫn khó thay thế React trong enterprise project lớn.

React đang phản ứng lại trend signals bằng cách: có RFC thảo luận về signals integration, và React 19 với compiler optimization giúp giảm re-render thừa mà không cần developer dùng useMemo thủ công. Cạnh tranh đang ngày càng thú vị.


Migration guide: từ React sang Svelte 5 từng bước

Bạn có project React và muốn thử Svelte? Không cần rewrite toàn bộ. Cách tiếp cận thực tế nhất là build feature mới bằng Svelte, giữ phần cũ bằng React - hai framework chạy song song hoàn toàn được.

Nhưng nếu bạn muốn convert một component, đây là bảng mapping 1-1:

React Svelte 5
useState(value) $state(value)
useMemo(() => expr, [deps]) $derived(expr)
useEffect(() => {}, [deps]) $effect(() => {})
useRef() $state() hoặc bind:this
useContext Import module .svelte.js
React.memo Tự động (không cần)
useCallback Không cần
props let { prop } = $props()

Ví dụ convert cụ thể - một search component:

// React version
function Search() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!query) return;
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(setResults);
  }, [query]);

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {results.map(r => <div key={r.id}>{r.title}</div>)}
    </>
  );
}
<!-- Svelte 5 version -->
<script>
  let query = $state('');
  let results = $state([]);

  $effect(() => {
    if (!query) return;
    fetch(`/api/search?q=${query}`)
      .then(r => r.json())
      .then(data => results = data);
  });
</script>

<input bind:value={query} />
{#each results as r (r.id)}
  <div>{r.title}</div>
{/each}

343702 Mapping 1-1 giúp migration không đau đầu - logic giữ nguyên, chỉ đổi syntax

Một lưu ý quan trọng: $state, $derived, $effect chỉ dùng được trong .svelte component hoặc file .svelte.js. Không dùng được trong file .js thông thường - đây là một điểm khác biệt cần nhớ khi chia sẻ code giữa hai stack.

Nếu bạn đang dùng Vue, migration còn dễ hơn vì Vue 3 Composition API với ref()/computed() khá tương đồng với $state/$derived. Mental model gần nhau, chỉ khác syntax.

Muốn có nền tảng vững trước khi nhảy vào Svelte? Khóa Xây Dựng Website với ReactJS của F8 giúp bạn hiểu sâu component thinking - kiến thức này transfer trực tiếp sang Svelte, vì cả hai đều dùng component-based architecture.

Tải checklist migration đầy đủ tại đây: Checklist Migration React → Svelte 5


Nên chọn Svelte 5 hay React cho project tiếp theo?

Không có câu trả lời đúng cho mọi trường hợp. Nhưng mình có thể cho bạn một framework để quyết định.

Chọn Svelte 5 khi:

  • Project mới, team nhỏ (1-5 người), không bị lock-in vào ecosystem cụ thể
  • Performance là ưu tiên cao - landing page, content site, app mobile web
  • Team muốn code ít boilerplate, onboard nhanh hơn
  • Không phụ thuộc nhiều vào thư viện UI third-party

Giữ React khi:

  • Dự án đang chạy React ổn định (đừng rewrite cái đang chạy tốt)
  • Team cần ecosystem rộng: component library, animation, data fetching, form lib
  • Tuyển dụng quan trọng - pool React developer lớn hơn nhiều
  • Enterprise, nhiều team, cần nhiều tooling và convention

343703 Không có framework nào thắng mọi trường hợp - context quyết định tất cả

Svelte 5 signals thực sự là bước tiến thú vị. Fine-grained reactivity, less boilerplate, bundle nhỏ hơn - những lợi thế này rõ ràng và có thể đo được. Nhưng Svelte vẫn đang trong giai đoạn xây dựng ecosystem, và đó là trade-off thực tế bạn phải chấp nhận.

Tin tốt là hai thứ không loại trừ nhau. Bạn có thể học Svelte 5 trên project cá nhân, giữ React ở công việc chính, và khi cơ hội đến - bạn đã có thêm một lựa chọn trong tay.

Nếu bạn muốn bắt đầu thực hành ngay:

  1. Đọc docs Svelte 5 chính thức phần runes: Svelte 5 Runes Documentation
  2. Build lại một component nhỏ từ project React hiện tại bằng Svelte 5
  3. So sánh bundle size, số dòng code, và trải nghiệm dev

Drop comment cho mình biết bạn thấy Svelte 5 runes thế nào sau khi thử nhé.