- vừa được xem lúc

Svelte Stores: Quản lý state hiệu quả và đơn giản

0 0 6

Người đăng: mai ly

Theo Viblo Asia

🤔 Bạn có từng cảm thấy mệt mỏi khi phải viết hàng đống boilerplate code cho Redux? Hay bối rối với Context API phức tạp của React? 😫 Chào mọi người! Hôm nay mình sẽ giới thiệu đến các bạn một cách quản lý state cực kỳ đơn giản và hiệu quả trong Svelte - đó chính là Svelte Stores! 🚀

1. Vấn đề của State Management truyền thống

Nếu bạn từng làm việc với React hoặc Vue, chắc hẳn đã quen với cảnh phải đấu tranh với việc quản lý state:

1.1. Redux - Quá nhiều boilerplate code

Redux tuy mạnh mẽ nhưng đòi hỏi rất nhiều code boilerplate. Để thêm một state đơn giản, bạn cần:

  • Tạo action types
  • Viết action creators
  • Cập nhật reducers
  • Kết nối với components qua connect() hoặc useSelector
// Redux - Chỉ để thêm một counter đơn giản
// Action Types
const INCREMENT = 'INCREMENT';
const DECREMENT = 'DECREMENT'; // Action Creators const increment = () => ({ type: INCREMENT });
const decrement = () => ({ type: DECREMENT }); // Reducer
const counterReducer = (state = { count: 0 }, action) => { switch (action.type) { case INCREMENT: return { count: state.count + 1 }; case DECREMENT: return { count: state.count - 1 }; default: return state; }
}; // Component
function Counter() { const count = useSelector(state => state.count); const dispatch = useDispatch(); return ( <div> <span>{count}</span> <button onClick={() => dispatch(increment())}>+</button> </div> );
}

1.2. React Context API - Phức tạp và dễ gây re-render

Context API tưởng chừng đơn giản nhưng lại có những vấn đề:

  • Khó tách logic ra khỏi component
  • Dễ gây re-render không cần thiết
  • Khó test và maintain
// Context API - Cũng khá rắc rối
const CounterContext = createContext(); function CounterProvider({ children }) { const [count, setCount] = useState(0); const increment = () => setCount(prev => prev + 1); const decrement = () => setCount(prev => prev - 1); return ( <CounterContext.Provider value={{ count, increment, decrement }}> {children} </CounterContext.Provider> );
} function Counter() { const { count, increment } = useContext(CounterContext); return <button onClick={increment}>{count}</button>;
}

Câu hỏi: Liệu có cách nào đơn giản hơn để quản lý state mà không cần quá nhiều setup phức tạp?

2. Svelte Stores - Giải pháp tối giản và mạnh mẽ

Svelte Stores chính là câu trả lời! Đây là hệ thống quản lý state được tích hợp sẵn trong Svelte, với những ưu điểm vượt trội:

  • Cực kỳ đơn giản - Chỉ cần vài dòng code
  • Reactive by design - Tự động cập nhật UI khi state thay đổi
  • TypeScript friendly - Hỗ trợ type safety tốt
  • Nhẹ nhàng - Không add thêm bundle size đáng kể
  • Linh hoạt - Có thể sử dụng outside components Cùng xem ví dụ counter tương tự với Svelte Stores:
// stores.js - Chỉ 1 dòng!
import { writable } from 'svelte/store';
export const count = writable(0); // Counter.svelte - Component đơn giản
<script> import { count } from './stores.js';
</script> <button on:click={() => $count++}> Count: {$count}
</button>

Thế thôi! Chỉ vậy thôi! 🤯 So với Redux hay Context API, Svelte Stores đơn giản đến không thể tin được.

3. 4 loại Stores trong Svelte

3.1. Writable Stores - Cho dữ liệu có thể thay đổi

Sử dụng khi: Bạn cần lưu trữ và thay đổi dữ liệu (shopping cart, form data, user preferences)

  • Ví dụ thực tế: Shopping Cart
// stores/cart.js
import { writable, derived } from 'svelte/store'; // Tạo store cho giỏ hàng
export const cartItems = writable([]); // Derived store cho tổng tiền
export const cartTotal = derived(cartItems, $items => $items.reduce((sum, item) => sum + item.price * item.quantity, 0)
); // Helper functions
export const cartActions = { addItem: (item) => { cartItems.update(items => { const existingItem = items.find(i => i.id === item.id); if (existingItem) { existingItem.quantity += 1; return items; } else { return [...items, { ...item, quantity: 1 }]; } }); }, removeItem: (itemId) => { cartItems.update(items => items.filter(item => item.id !== itemId) ); }
};
<!-- ShoppingCart.svelte -->
<script> import { cartItems, cartTotal, cartActions } from './stores/cart.js'; const products = [ { id: 1, name: 'iPhone 15', price: 25000000 }, { id: 2, name: 'MacBook Pro', price: 50000000 } ];
</script> <div class="shopping-app"> <h2>Sản phẩm</h2> {#each products as product} <div class="product"> <h3>{product.name}</h3> <p>{product.price.toLocaleString()}đ</p> <button on:click={() => cartActions.addItem(product)}> Thêm vào giỏ </button> </div> {/each} <h2>Giỏ hàng</h2> {#each $cartItems as item} <div class="cart-item"> <span>{item.name} x {item.quantity}</span> <button on:click={() => cartActions.removeItem(item.id)}> Xóa </button> </div> {/each} <p><strong>Tổng: {$cartTotal.toLocaleString()}đ</strong></p>
</div>

3.2. Readable Stores - Cho dữ liệu chỉ đọc

Sử dụng khi: Dữ liệu chỉ được thay đổi từ bên trong store (timer, real-time data, authentication status)

  • Ví dụ thực tế: User Authentication
// stores/auth.js
import { writable } from 'svelte/store'; // Private writable store để update từ bên trong
const _user = writable(null); // Khởi tạo: kiểm tra localStorage khi app start
if (typeof localStorage !== 'undefined') { const savedUser = localStorage.getItem('user'); if (savedUser) { _user.set(JSON.parse(savedUser)); }
} export const authActions = { login: async (email, password) => { try { // Giả lập API call const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) }); const userData = await response.json(); if (userData.success) { // Lưu vào localStorage localStorage.setItem('user', JSON.stringify(userData.user)); // Cập nhật store _user.set(userData.user); return { success: true }; } else { return { success: false, error: userData.error }; } } catch (error) { return { success: false, error: 'Network error' }; } }, logout: () => { localStorage.removeItem('user'); _user.set(null); }
}; // Export readonly version
export const user = { subscribe: _user.subscribe };
<!-- Login.svelte -->
<script> import { user, authActions } from './stores/auth.js'; let email = ''; let password = ''; let isLoading = false; let error = ''; const handleLogin = async () => { isLoading = true; error = ''; const result = await authActions.login(email, password); if (!result.success) { error = result.error; } isLoading = false; };
</script> {#if $user} <div class="user-info"> <h2>Xin chào, {$user.name}!</h2> <button on:click={authActions.logout}>Đăng xuất</button> </div>
{:else} <form on:submit|preventDefault={handleLogin}> <h2>Đăng nhập</h2> <input type="email" bind:value={email} placeholder="Email" required /> <input type="password" bind:value={password} placeholder="Password" required /> {#if error} <p class="error">{error}</p> {/if} <button type="submit" disabled={isLoading}> {isLoading ? 'Đang đăng nhập...' : 'Đăng nhập'} </button> </form>
{/if}

3.3. Derived Stores - Cho dữ liệu tính toán

Sử dụng khi: Bạn cần tạo data mới dựa trên store khác (filtered lists, computed values, combined data)

  • Ví dụ thực tế: API Data Fetching với Search và Filter

Lưu ý: Derived store sẽ tự động cập nhật khi bất kỳ store nào trong dependencies thay đổi, nên không cần gọi hàm update thủ công. Điều này giúp code sạch hơn và tránh bugs!

// stores/products.js
import { writable, derived } from 'svelte/store'; // Base stores
export const allProducts = writable([]);
export const searchTerm = writable('');
export const selectedCategory = writable('all');
export const isLoading = writable(false); // Derived store: filtered products
export const filteredProducts = derived( [allProducts, searchTerm, selectedCategory], ([$allProducts, $searchTerm, $selectedCategory]) => { let filtered = $allProducts; // Filter by category if ($selectedCategory !== 'all') { filtered = filtered.filter(p => p.category === $selectedCategory); } // Filter by search term if ($searchTerm) { filtered = filtered.filter(p => p.name.toLowerCase().includes($searchTerm.toLowerCase()) || p.description.toLowerCase().includes($searchTerm.toLowerCase()) ); } return filtered; }
); // Derived store: categories list
export const categories = derived( allProducts, ($allProducts) => { const cats = new Set($allProducts.map(p => p.category)); return ['all', ...Array.from(cats)]; }
); // Derived store: search statistics
export const searchStats = derived( [allProducts, filteredProducts, searchTerm], ([$allProducts, $filteredProducts, $searchTerm]) => ({ total: $allProducts.length, filtered: $filteredProducts.length, hasSearchTerm: $searchTerm.length > 0 })
); // API actions
export const productActions = { fetchProducts: async () => { isLoading.set(true); try { const response = await fetch('/api/products'); const products = await response.json(); allProducts.set(products); } catch (error) { console.error('Failed to fetch products:', error); } finally { isLoading.set(false); } }
};
<!-- ProductList.svelte -->
<script> import { onMount } from 'svelte'; import { filteredProducts, categories, searchStats, searchTerm, selectedCategory, isLoading, productActions } from './stores/products.js'; onMount(() => { productActions.fetchProducts(); });
</script> <div class="product-list"> <div class="filters"> <input type="text" bind:value={$searchTerm} placeholder="Tìm kiếm sản phẩm..." /> <select bind:value={$selectedCategory}> {#each $categories as category} <option value={category}> {category === 'all' ? 'Tất cả danh mục' : category} </option> {/each} </select> </div> <div class="stats"> Hiển thị {$searchStats.filtered} / {$searchStats.total} sản phẩm {#if $searchStats.hasSearchTerm} cho "{$searchTerm}" {/if} </div> {#if $isLoading} <p>Đang tải...</p> {:else} <div class="products-grid"> {#each $filteredProducts as product} <div class="product-card"> <h3>{product.name}</h3> <p>{product.description}</p> <span class="category">{product.category}</span> <span class="price">{product.price.toLocaleString()}đ</span> </div> {/each} </div> {/if}
</div>

3.4. Custom Stores - Cho logic phức tạp

Sử dụng khi: Bạn cần combine nhiều functionality vào một store với API custom

// stores/notification.js
import { writable } from 'svelte/store'; function createNotificationStore() { const { subscribe, update } = writable([]); return { subscribe, add: (message, type = 'info', duration = 5000) => { const notification = { id: Date.now(), message, type, duration }; update(notifications => [...notifications, notification]); // Auto remove after duration if (duration > 0) { setTimeout(() => { notificationStore.remove(notification.id); }, duration); } }, remove: (id) => { update(notifications => notifications.filter(n => n.id !== id) ); }, clear: () => { update(() => []); }, // Convenience methods success: (message, duration) => notificationStore.add(message, 'success', duration), error: (message, duration) => notificationStore.add(message, 'error', duration), warning: (message, duration) => notificationStore.add(message, 'warning', duration) };
} export const notificationStore = createNotificationStore();

4. Thực hành: Xây dựng ứng dụng Todo với Stores

Giờ chúng ta sẽ xây dựng một ứng dụng Todo hoàn chỉnh để thấy được sức mạnh của Svelte Stores trong thực tế!

4.1. Thiết lập Stores

Ghi chú: Trong ví dụ này, dữ liệu sẽ mất khi reload trang. Trong ứng dụng thực tế, bạn có thể thêm localStorage persistence hoặc sync với server.

// stores/todos.js
import { writable, derived } from 'svelte/store'; // Base store
export const todos = writable([]);
export const filter = writable('all'); // 'all', 'active', 'completed' // Derived stores
export const filteredTodos = derived( [todos, filter], ([$todos, $filter]) => { switch ($filter) { case 'active': return $todos.filter(t => !t.completed); case 'completed': return $todos.filter(t => t.completed); default: return $todos; } }
); export const todosStats = derived( todos, ($todos) => ({ total: $todos.length, active: $todos.filter(t => !t.completed).length, completed: $todos.filter(t => t.completed).length })
); // Actions
export const todoActions = { add: (text) => { if (text.trim()) { todos.update(items => [ ...items, { id: crypto.randomUUID(), // Tránh trùng ID khi thêm nhanh nhiều task text: text.trim(), completed: false, createdAt: new Date() } ]); } }, toggle: (id) => { todos.update(items => items.map(item => item.id === id ? { ...item, completed: !item.completed } : item ) ); }, remove: (id) => { todos.update(items => items.filter(item => item.id !== id) ); }, edit: (id, newText) => { todos.update(items => items.map(item => item.id === id ? { ...item, text: newText } : item ) ); }, clearCompleted: () => { todos.update(items => items.filter(item => !item.completed) ); }
};

4.2. Component chính

<!-- TodoApp.svelte -->
<script> import { filteredTodos, todosStats, filter, todoActions } from './stores/todos.js'; import TodoInput from './TodoInput.svelte'; import TodoItem from './TodoItem.svelte'; import TodoFilter from './TodoFilter.svelte';
</script> <div class="todo-app"> <h1>My Todo App</h1> <TodoInput /> <TodoFilter /> <div class="stats"> <span>Tổng: {$todosStats.total}</span> <span>Còn lại: {$todosStats.active}</span> <span>Hoàn thành: {$todosStats.completed}</span> </div> <ul class="todo-list"> {#each $filteredTodos as todo (todo.id)} <TodoItem {todo} /> {/each} </ul> {#if $todosStats.completed > 0} <button class="clear-completed" on:click={todoActions.clearCompleted} > Xóa đã hoàn thành ({$todosStats.completed}) </button> {/if}
</div>

4.3. Components con

<!-- TodoInput.svelte -->
<script> import { todoActions } from './stores/todos.js'; let inputText = ''; const handleSubmit = () => { todoActions.add(inputText); inputText = ''; };
</script> <form on:submit|preventDefault={handleSubmit}> <input type="text" bind:value={inputText} placeholder="Thêm todo mới..." class="todo-input" /> <button type="submit">Thêm</button>
</form>
<!-- TodoItem.svelte -->
<script> import { todoActions } from './stores/todos.js'; export let todo; let isEditing = false; let editText = todo.text; const handleEdit = () => { isEditing = true; editText = todo.text; }; const handleSave = () => { todoActions.edit(todo.id, editText); isEditing = false; }; const handleCancel = () => { isEditing = false; editText = todo.text; };
</script> <li class="todo-item" class:completed={todo.completed}> <input type="checkbox" checked={todo.completed} on:change={() => todoActions.toggle(todo.id)} /> {#if isEditing} <input type="text" bind:value={editText} on:blur={handleSave} on:keydown={(e) => { if (e.key === 'Enter') handleSave(); if (e.key === 'Escape') handleCancel(); }} /> {:else} <span class="todo-text" on:dblclick={handleEdit} > {todo.text} </span> {/if} <button class="delete-btn" on:click={() => todoActions.remove(todo.id)} > × </button>
</li>

Kết quả: Chúng ta đã có một ứng dụng Todo hoàn chỉnh với tính năng thêm, sửa, xóa, filter và thống kê - tất cả với code cực kỳ đơn giản và dễ hiểu!

5. So sánh với các giải pháp khác

Tiêu chí Svelte Stores Redux Context API
Độ phức tạp setup Cực đơn giản Nhiều boilerplate Trung bình
Bundle size Gần như không thêm bundle size ~20KB+ Built-in React
Performance Rất tốt Tốt với middleware Dễ re-render
Learning curve Dễ học Khó cho newbie Trung bình
DevTools Cơ bản Rất mạnh Có React DevTools
Ecosystem Đang phát triển Rất lớn Lớn (React)

5.1. Ưu điểm của Svelte Stores

  • Cực kỳ đơn giản - Không cần học concepts phức tạp
  • Reactive by default - UI tự động update khi state thay đổi
  • Lightweight - Không làm tăng bundle size đáng kể
  • TypeScript friendly - Type inference tốt
  • Framework agnostic - Có thể dùng outside Svelte components
  • Không cần Provider/Consumer pattern - Import và dùng thôi!

5.2. Hạn chế của Svelte Stores

  • DevTools hạn chế: Chưa có DevTools mạnh như Redux DevTools
  • Ecosystem nhỏ: Ít middleware và extensions
  • Time travel debugging: Không có sẵn như Redux
  • Chỉ dành cho Svelte: Không thể dùng với React/Vue

Kết luận

Svelte Stores là một giải pháp state management cực kỳ elegant và practical. Với philosophy "đơn giản nhưng mạnh mẽ", nó giúp developers:

  • Phát triển nhanh hơn với ít boilerplate code
  • Ít bugs hơn nhờ API đơn giản và rõ ràng
  • Dễ học và dạy cho team members mới
  • Performance tốt với bundle size nhỏ

💛 Takeaway: Nếu bạn đang cảm thấy mệt mỏi với Redux boilerplate hay Context API phức tạp, hãy thử Svelte và Svelte Stores. Có thể đây chính là giải pháp bạn đang tìm kiếm! Hy vọng bài viết này đã giúp bạn hiểu rõ hơn về Svelte Stores và cách áp dụng trong dự án thực tế. Chúc các bạn coding vui vẻ!

Tài liệu tham khảo

Bình luận

Bài viết tương tự

- vừa được xem lúc

Svelte - Một framework mà các tín đồ Javascript không thể bỏ qua

1. Giới thiệu.

0 0 51

- vừa được xem lúc

Svelte cơ bản

Đây là bài viết tổng hợp lại kiến thức của mình khi bắt đầu tìm hiểu về Svelte. Một framework khác để code UI ngoài các thư viện truyền thống như reactjs, vuejs,... Để tạo một dự án với svelte bản có thể dùng codesandbox, repl tạo template rồi tải về. Hoặc cài đặt với lệnh sau:. npx degit sveltejs/t

0 0 124

- vừa được xem lúc

Xây dựng eCommerce shopping cart bằng Svelte JavaScript framework.

Introduction. Svelte JavaScript framework là miễn phí và là open-source.

0 0 50

- vừa được xem lúc

Review: Svelte, SvelteKit - Kẻ thách thức đám đông - Viblo

FE Framework review: Svelte, SvelteKit - Kẻ thách thức đám đông. Tại sao nên đọc bài này. Pros/cons của Nextjs. .

0 0 39

- vừa được xem lúc

5 Reasons Why Svelte Should Be Your Next JavaScript Framework

As web development continues to grow in popularity, developers have more options for choosing a JavaScript framework to build their applications. Svelte is one such framework that has gained a lot of

0 0 39

- vừa được xem lúc

Khám Phá Svelte: Framework JavaScript “Siêu Nhẹ” Đang Thống Trị Cộng Đồng Dev!

HIện nay, Svelte đang nổi lên như một lựa chọn thú vị so với các framework truyền thống như React và Vue. Vậy điều gì đã khiến cho nó trở thành xu hướng như vậy, cùng mình tìm hiểu nhé.

0 0 12