Giới Thiệu
Khi làm việc với Svelte, việc quản lý dữ liệu và truyền thông tin giữa các thành phần có thể trở nên phức tạp, đặc biệt là khi bạn phải truyền props qua nhiều lớp thành phần - điều này được gọi là "prop drilling". Bài viết này sẽ giúp bạn hiểu cách sử dụng context và quản lý dữ liệu async trong Svelte một cách hiệu quả.
Vấn Đề: Prop Drilling 🕳️
Giả sử bạn có một layout định nghĩa một chủ đề (sáng hoặc tối). Một thành phần Logo bên trong layout cần biết chủ đề đó. Nếu không có context, bạn sẽ phải truyền thông tin chủ đề qua từng lớp:
Ví dụ về Prop Drilling
svelte
// Logo.svelte
<script>
export let theme;
</script>
<h1 class={theme}>My App</h1>
svelte
// Header.svelte
<script>
import Logo from './Logo.svelte';
export let theme;
</script>
<nav>
<Logo theme={theme} />
</nav>
svelte
// Layout.svelte
<script>
import Header from './Header.svelte';
export let theme = 'light';
</script>
<div class={theme}>
<Header theme={theme} />
</div>
<style>
.light { background: #f8f8f8; color: #222; }
.dark { background: #222; color: #f8f8f8; }
</style>
svelte
// +page.svelte
<script>
import Layout from '$lib/Layout.svelte';
</script>
<Layout theme="dark" />
Như bạn thấy, chủ đề được truyền qua từng lớp. Ngay cả Header
, không quan tâm đến theme
, cũng phải truyền nó để Logo
có thể sử dụng. Điều này rất rối rắm và khó bảo trì.
Giải Pháp: Context 🎒
Context giúp giảm thiểu việc truyền props, cho phép một thành phần cha đặt giá trị một lần và bất kỳ thành phần con nào cũng có thể lấy trực tiếp giá trị đó.
Svelte cung cấp cho chúng ta hai hàm trợ giúp:
setContext(key, value)
→ được gọi trong thành phần cha để cung cấp giá trị.getContext(key)
→ được gọi trong thành phần con để tiêu thụ giá trị.
Cả hai hàm này phải đồng ý về key
(một chuỗi hoặc ký hiệu).
Tạo ThemeProvider
Chúng ta sẽ xây dựng một ThemeProvider
để quản lý chủ đề sáng/tối.
svelte
// ThemeProvider.svelte
<script>
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
const theme = writable('light');
setContext('theme', theme);
function toggle() {
$theme = $theme === 'light' ? 'dark' : 'light';
}
let { children } = $props();
</script>
<div class={$theme}>
<button onclick={toggle}>Toggle Theme</button>
{@render children?.()}
</div>
<style>
.light { background: #f8f8f8; color: #222; padding: 1rem; }
.dark { background: #222; color: #f8f8f8; padding: 1rem; }
</style>
Giải Thích
- Chúng ta tạo một store
theme
có thể ghi. - Đặt nó vào context với khóa là "theme".
- Provider sẽ hiển thị một nút để chuyển đổi giữa các chủ đề.
- Các thành phần con bên trong
ThemeProvider
sẽ tự động có quyền truy cập vào context chủ đề.
Tạo ThemedButton
svelte
// ThemedButton.svelte
<script>
import { getContext } from 'svelte';
const theme = getContext('theme');
</script>
<button class={$theme}>
I just follow the theme!
</button>
<style>
.light { background: white; color: black; }
.dark { background: black; color: white; }
</style>
Nút này không nhận prop theme
. Thay vào đó, nó sẽ hỏi context.
Thử Nghiệm
svelte
// +page.svelte
<script>
import ThemeProvider from '$lib/ThemeProvider.svelte';
import ThemedButton from '$lib/ThemedButton.svelte';
</script>
<h1>Context Demo</h1>
<ThemeProvider>
<p>This text is inside the provider.</p>
<ThemedButton />
</ThemeProvider>
Khi bạn chạy đoạn mã này, bạn sẽ thấy:
- Một container chủ đề với nút chuyển đổi.
- Văn bản và nút theo chủ đề bên trong.
- Khi bạn chuyển đổi, tất cả các thành phần con sẽ tự động phản ứng.
Dữ Liệu Async
Sau khi đã nắm vững context, chúng ta sẽ chuyển sang dữ liệu async. Các ứng dụng không thể hoạt động độc lập, chúng cần dữ liệu từ API, cơ sở dữ liệu, hoặc tập tin cục bộ. Và dữ liệu đó không phải lúc nào cũng đến ngay lập tức - nó là async.
Tại Sao Dữ Liệu Async Khác Biệt
Khi bạn lấy dữ liệu, có ba trạng thái cần xem xét:
- Đang tải — chờ phản hồi.
- Thành công — dữ liệu đã đến.
- Lỗi — có gì đó không đúng.
Nếu bạn không xử lý cả ba trạng thái, giao diện của bạn có thể trông bị hỏng hoặc để người dùng nhìn vào một màn hình trống rỗng.
Svelte cung cấp cho chúng ta một công cụ tích hợp để quản lý các trạng thái này: khối {#await}
.
Khối {#await}
Hàm fetch()
của JavaScript trả về một promise — một giá trị chưa sẵn sàng nhưng sẽ có trong tương lai. Thay vì phải quản lý cờ loading
và try/catch
, Svelte cho phép bạn khai báo tất cả ba trạng thái ngay trong mã.
svelte
// JokeFetcher.svelte
<script>
let promise = fetch('https://api.chucknorris.io/jokes/random')
.then(res => res.json());
</script>
{#await promise}
<p>Loading a hilarious joke…</p>
{:then data}
<p>{data.value}</p>
{:catch error}
<p style="color: red">Oops: {error.message}</p>
{/await}
Làm Mới Dữ Liệu
Giả sử joke chỉ tải một lần khi thành phần được gắn. Nhưng nếu bạn muốn một joke mới mỗi lần nhấn?
Giải pháp: bọc hàm fetch trong một hàm và gán promise
khi bạn cần dữ liệu mới.
svelte
// JokeFetcher.svelte
<script>
let promise;
function loadJoke() {
promise = fetch('https://api.chucknorris.io/jokes/random')
.then(res => res.json());
}
loadJoke();
</script>
<button onclick={loadJoke}>New Joke</button>
{#await promise}
<p>Loading…</p>
{:then data}
<p>{data.value}</p>
{:catch error}
<p style="color: red">Error: {error.message}</p>
{/await}
Bọc Logic Async Trong Store
Lấy dữ liệu trực tiếp trong một thành phần có thể hoạt động cho các ví dụ. Nhưng trong một ứng dụng thực tế, nó nhanh chóng trở nên lộn xộn:
- Bạn lặp lại cùng một logic
fetch
ở nhiều nơi. - Tải lại có nghĩa là viết lại cùng một mã boilerplate.
- Kiểm tra/gỡ lỗi khó khăn hơn vì logic bị rải rác.
Giải pháp: bọc logic async của bạn trong một custom store. Store sẽ sở hữu logic fetching, theo dõi trạng thái và cung cấp kết quả. Các thành phần chỉ cần tiêu thụ nó.
Bước 1 — Tạo Store
javascript
// userStore.js
import { writable } from 'svelte/store';
export function createUserStore() {
const { subscribe, set } = writable({
status: 'loading',
data: null,
error: null
});
async function load() {
set({ status: 'loading', data: null, error: null });
try {
const id = Math.floor(Math.random() * 10) + 1;
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
const data = await res.json();
set({ status: 'success', data, error: null });
} catch (err) {
set({ status: 'error', data: null, error: err });
}
}
load();
return { subscribe, reload: load };
}
Bước 2 — Sử Dụng Trong Thành Phần
svelte
// UserProfile.svelte
<script>
import { createUserStore } from '$lib/userStore.js';
const user = createUserStore();
</script>
{#if $user.status === 'loading'}
<p>Loading user...</p>
{:else if $user.status === 'error'}
<p style="color: red">Error loading user</p>
{:else}
<h2>{ $user.data.name }</h2>
<p>Email: { $user.data.email }</p>
<button onclick={user.reload}>Reload</button>
{/if}
Kết Hợp Nhiều Cuộc Gọi Async
Đôi khi bạn cần nhiều hơn một mảnh dữ liệu trước khi hiển thị. Ví dụ: lấy một người dùng và bài viết của họ.
svelte
// UserWithPosts.svelte
<script>
let promise = Promise.all([
fetch('https://jsonplaceholder.typicode.com/users/1').then(r => r.json()),
fetch('https://jsonplaceholder.typicode.com/posts?userId=1').then(r => r.json())
]);
</script>
{#await promise}
<p>Loading user and posts...</p>
{:then [user, posts]}
<h2>{user.name}</h2>
<ul>
{#each posts as post}
<li>{post.title}</li>
{/each}
</ul>
{:catch error}
<p style="color: red">Failed: {error.message}</p>
{/await}
Kết Luận
Trong bài viết này, bạn đã học cách sử dụng context và quản lý dữ liệu async trong Svelte. Với sự kết hợp của các khái niệm này, bạn có thể xây dựng các ứng dụng Svelte mạnh mẽ, dễ bảo trì và có trải nghiệm người dùng tốt.
Hãy cùng thực hành và khám phá thêm về Svelte để áp dụng vào các dự án thực tế của bạn.
Câu Hỏi Thường Gặp (FAQ)
1. Context là gì và tại sao nên dùng?
Context cho phép chia sẻ trạng thái giữa các thành phần mà không cần phải truyền props qua nhiều lớp. Điều này giúp mã nguồn trở nên sạch sẽ và dễ bảo trì hơn.
2. Làm thế nào để quản lý dữ liệu async trong Svelte?
Bạn có thể sử dụng khối {#await}
để xử lý các trạng thái tải dữ liệu và có thể bọc logic fetching trong một store để dễ dàng quản lý và tái sử dụng.
3. Khi nào nên dùng props, context hay store?
- Props: Sử dụng cho giao tiếp đơn giản giữa cha và con.
- Context: Sử dụng cho trạng thái chia sẻ trong một subtree.
- Store: Sử dụng cho trạng thái toàn cục, có thể truy cập từ bất kỳ đâu.
Hãy thử áp dụng những kiến thức này vào dự án của bạn và xem sự khác biệt mà nó mang lại!