Bài 29.3: SWR hook trong TypeScript
Bài 29.3: SWR hook trong TypeScript
Chào mừng bạn quay trở lại với series blog về Lập trình Web Front-end! Hôm nay, chúng ta sẽ đi sâu vào một chủ đề cực kỳ quan trọng và hữu ích trong việc phát triển các ứng dụng React/Next.js hiện đại: quản lý việc fetch dữ liệu. Việc lấy dữ liệu từ API, xử lý trạng thái loading, error, caching, và cập nhật dữ liệu sau khi thay đổi (mutation) có thể trở nên phức tạp và lặp đi lặp lại. May mắn thay, chúng ta có SWR.
SWR là một thư viện React Hooks để fetch dữ liệu, được tạo ra bởi đội ngũ Vercel (đứng sau Next.js). Tên SWR là viết tắt của Stale-While-Revalidate, một chiến lược caching thông minh. Kết hợp SWR với sức mạnh của TypeScript, chúng ta sẽ có một giải pháp mạnh mẽ và an toàn kiểu (type-safe) để quản lý dữ liệu trong ứng dụng của mình.
SWR Là Gì và Tại Sao Nó Lại Tuyệt Vời?
Trước khi đi vào TypeScript, hãy hiểu SWR là gì. SWR dựa trên chiến lược "stale-while-revalidate". Nó hoạt động như sau:
- Khi bạn yêu cầu dữ liệu lần đầu, SWR sẽ fetch dữ liệu từ nguồn (API).
- Nó lưu trữ dữ liệu này vào cache.
- Lần tới khi bạn yêu cầu dữ liệu tương tự, SWR sẽ ngay lập tức trả về dữ liệu đã lưu trong cache (dữ liệu này có thể đã cũ - "stale").
- Đồng thời, SWR sẽ fetch dữ liệu mới từ nguồn trong nền ("revalidate").
- Khi dữ liệu mới về, SWR sẽ cập nhật cache và UI của bạn.
Chiến lược này mang lại trải nghiệm người dùng tuyệt vời: ứng dụng phản hồi nhanh chóng bằng cách hiển thị dữ liệu cũ ngay lập tức, đồng thời đảm bảo dữ liệu luôn được cập nhật trong nền mà không chặn giao diện người dùng.
Các lợi ích chính khi sử dụng SWR:
- Caching Tự Động: Giảm thiểu số lần fetch không cần thiết, tăng tốc độ tải trang.
- Revalidation Thông Minh: Tự động cập nhật dữ liệu khi có thay đổi (ví dụ: khi người dùng focus lại tab trình duyệt, hoặc kết nối mạng hoạt động trở lại).
- Xử lý Loading & Error: Cung cấp trạng thái
isLoadingvàerrormột cách dễ dàng. - Tối ưu Hiệu năng: Hỗ trợ deduplication (tránh fetch cùng một dữ liệu nhiều lần), pagination, infinite loading...
- Hỗ trợ TypeScript Xuất Sắc: Đây là điểm chúng ta sẽ tập trung. TypeScript giúp đảm bảo bạn làm việc với dữ liệu có cấu trúc rõ ràng, tránh lỗi runtime liên quan đến kiểu dữ liệu.
Bắt Đầu Với SWR (Vẫn Dùng TypeScript Nhé!)
Để sử dụng SWR, bạn cần cài đặt nó:
npm install swr typescript @types/react
# hoặc
yarn add swr typescript @types/react
Cú pháp cơ bản của useSWR là:
import useSWR from 'swr';
const { data, error, isLoading } = useSWR(key, fetcher, options);
key: Một chuỗi duy nhất (thường là URL của API) hoặc một mảng chứa URL và các tham số. SWR sử dụngkeyđể xác định dữ liệu nào đang được cache và fetch. Nếukeylànullhoặcundefined, SWR sẽ không fetch.fetcher: Một hàmPromisenhậnkeylàm tham số và trả về dữ liệu. Đây là nơi bạn thực hiện logic gọi API của mình (ví dụ: dùngfetchhoặcaxios).options: Một đối tượng tùy chọn để cấu hình hành vi của SWR (ví dụ:revalidateOnFocus,dedupingInterval, v.v.).data: Dữ liệu trả về từfetchersau khi fetch thành công.error: Đối tượng lỗi nếu quá trình fetch thất bại.isLoading: Một boolean cho biết quá trình fetch dữ liệu ban đầu có đang diễn ra hay không. (Trước đây làisValidatingcho cả lần fetch ban đầu và revalidation, giờisLoadingchỉ cho lần đầu,isValidatingcho tất cả các lần fetch).
Hãy xem một ví dụ cơ bản với TypeScript:
// api.ts (hoặc một file fetcher riêng)
export async function fetchJson<JSON = any>(
input: RequestInfo,
init?: RequestInit
): Promise<JSON> {
const res = await fetch(input, init);
if (!res.ok) {
const error = new Error('An error occurred while fetching the data.');
// Attach extra info to the error object.
// error.info = await res.json(); // Có thể thêm thông tin lỗi chi tiết
error.message = res.statusText;
throw error;
}
return res.json();
}
// components/UserProfile.tsx
import React from 'react';
import useSWR from 'swr';
import { fetchJson } from '../api'; // Import fetcher của bạn
// Định nghĩa kiểu dữ liệu mong muốn
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
// Sử dụng useSWR với kiểu dữ liệu
// key có thể là null hoặc undefined nếu userId chưa có, giúp bỏ qua fetch
const { data: user, error, isLoading } = useSWR<User, Error>(
userId ? `/api/users/${userId}` : null, // key phụ thuộc vào userId
fetchJson // hàm fetcher của bạn
);
if (isLoading) {
return <div>Đang tải thông tin người dùng...</div>;
}
if (error) {
// error được tự động typed là Error nhờ kiểu dữ liệu thứ 2 trong useSWR<User, Error>
return <div>Lỗi tải thông tin người dùng: {error.message}</div>;
}
// data được tự động typed là User
if (!user) {
// Điều này có thể xảy ra nếu key ban đầu là null hoặc undefined
return <div>Chọn một người dùng để xem.</div>;
}
return (
<div>
<h2>Thông tin người dùng</h2>
<p>ID: {user.id}</p>
<p>Tên: {user.name}</p>
<p>Email: {user.email}</p>
</div>
);
}
export default UserProfile;
Giải thích Code:
- Chúng ta định nghĩa một interface
Userđể mô tả cấu trúc dữ liệu mà API trả về. Điều này cực kỳ quan trọng khi làm việc với TypeScript và SWR. - Hàm
fetchJsonlà một fetcher đơn giản sử dụngfetchAPI. Nó được typed để trả về mộtPromise<JSON>nơiJSONlà kiểu dữ liệu mong muốn. Chúng ta cũng thêm logic kiểm trares.okđể xử lý lỗi. - Trong component
UserProfile, chúng ta gọiuseSWR<User, Error>(...).User: Kiểu dữ liệu chodatatrả về thành công.Error: Kiểu dữ liệu choerrornếu quá trình fetch thất bại.
keylà/api/users/${userId}. Chúng ta sử dụng toán tử ba ngôiuserId ? ... : nullđể đảm bảo SWR chỉ fetch khiuserIdcó giá trị (khác null/undefined).fetchJsonđược truyền vào làmfetcher.- Kết quả trả về là
data,error, vàisLoading. Nhờ TypeScript,datasẽ có kiểuUser | undefined,errorcó kiểuError | undefined, vàisLoadinglàboolean. - Chúng ta sử dụng
isLoading,error, vàuserđể hiển thị giao diện tương ứng (đang tải, lỗi, hoặc dữ liệu người dùng). TypeScript giúp chúng ta biết chắc chắnusersẽ có các thuộc tínhid,name,emailkhi nó không phải làundefined.
Xử Lý Trạng Thái Loading và Error Chi Tiết Hơn
SWR cung cấp isLoading (cho lần fetch đầu tiên) và isValidating (cho mọi lần fetch, bao gồm revalidation) để giúp bạn quản lý trạng thái tải. error chứa thông tin lỗi.
// components/ArticleList.tsx
import React from 'react';
import useSWR from 'swr';
import { fetchJson } from '../api';
interface Article {
id: number;
title: string;
summary: string;
}
interface ArticlesResponse {
articles: Article[];
total: number;
}
function ArticleList() {
// Định nghĩa key và fetcher, type cho dữ liệu thành công và lỗi
const { data, error, isLoading, isValidating, mutate } = useSWR<ArticlesResponse, Error>(
'/api/articles',
fetchJson,
{
// Tùy chọn cấu hình SWR
revalidateOnFocus: true, // Tự động fetch lại khi focus vào tab
// revalidateIfStale: false, // Ngăn fetch lại nếu dữ liệu cache vẫn còn
// dedupingInterval: 5000 // Chỉ fetch tối đa 1 lần trong 5 giây cho cùng key
}
);
// Kiểm tra trạng thái loading ban đầu
if (isLoading) {
return <div><p>Đang tải danh sách bài viết...</p></div>;
}
// Kiểm tra trạng thái lỗi
if (error) {
return <div><p>Không thể tải danh sách bài viết: {error.message}</p></div>;
}
// Khi dữ liệu đã tải xong
// TypeScript biết data có kiểu ArticlesResponse | undefined
// Vì đã check error và isLoading, data chắc chắn không phải undefined ở đây
const articles = data?.articles || []; // Sử dụng optional chaining và fallback
// Bạn có thể hiển thị trạng thái revalidating (đang cập nhật dữ liệu trong nền)
const revalidatingIndicator = isValidating && !isLoading ? (
<small>(Đang cập nhật...)</small>
) : null;
return (
<div>
<h1>Danh sách bài viết {revalidatingIndicator}</h1>
{articles.length === 0 && !isLoading && !error ? (
<p>Không có bài viết nào được tìm thấy.</p>
) : (
<ul>
{articles.map(article => (
<li key={article.id}>
<h3>{article.title}</h3> {/* TypeScript đảm bảo article có title */}
<p>{article.summary}</p>
</li>
))}
</ul>
)}
</div>
);
}
export default ArticleList;
Giải thích Code:
- Chúng ta định nghĩa các interface
ArticlevàArticlesResponse. useSWR<ArticlesResponse, Error>đảm bảodatalàArticlesResponse | undefinedvàerrorlàError | undefined.- Chúng ta xử lý các trường hợp
isLoadingvàerrortrước khi render danh sách bài viết. isValidatingcho phép chúng ta hiển thị một indicator nhỏ khi SWR đang fetch dữ liệu trong nền (revalidation).- Khi
datađã có, TypeScript biết cấu trúc của nó, cho phép chúng ta truy cậpdata.articlesmột cách an toàn (hoặc dùng optional chainingdata?.articles).
Fetch Dữ Liệu Phụ Thuộc và Dynamic Keys
Rất nhiều trường hợp bạn cần fetch dữ liệu phụ thuộc vào một ID, một tham số URL, hoặc trạng thái khác của component. SWR xử lý điều này rất tốt bằng cách sử dụng key động. key có thể là một hàm hoặc một mảng. Khi bất kỳ phần tử nào trong mảng key thay đổi, SWR sẽ tự động fetch lại.
// components/PostDetails.tsx
import React from 'react';
import { useRouter } from 'next/router'; // Ví dụ dùng Next.js router
import useSWR from 'swr';
import { fetchJson } from '../api';
interface Post {
id: number;
title: string;
body: string;
authorId: number;
}
interface Author {
id: number;
name: string;
}
// Key có thể là mảng chứa URL và ID
type PostKey = ['/api/posts', number] | null;
type AuthorKey = ['/api/users', number] | null;
function PostDetails() {
const router = useRouter();
const postId = router.query.postId ? Number(router.query.postId) : undefined; // Lấy postId từ URL
// Fetch bài viết
const postKey: PostKey = postId ? ['/api/posts', postId] : null;
const { data: post, error: postError, isLoading: isPostLoading } = useSWR<Post, Error>(
postKey,
([url, id]) => fetchJson(`${url}/${id}`) // fetcher nhận mảng key
);
// Fetch thông tin tác giả (phụ thuộc vào post)
const authorId = post?.authorId; // authorId chỉ có khi post đã được tải
const authorKey: AuthorKey = authorId ? ['/api/users', authorId] : null;
const { data: author, error: authorError, isLoading: isAuthorLoading } = useSWR<Author, Error>(
authorKey,
([url, id]) => fetchJson(`${url}/${id}`) // fetcher nhận mảng key
);
const isLoading = isPostLoading || isAuthorLoading;
const error = postError || authorError;
if (isLoading) {
return <div>Đang tải chi tiết bài viết...</div>;
}
if (error) {
return <div>Lỗi tải bài viết: {error.message}</div>;
}
if (!post) {
// Có thể xảy ra nếu postId ban đầu không hợp lệ hoặc fetch lỗi nhưng không có error object
return <div>Bài viết không tồn tại hoặc đã xảy ra lỗi không xác định.</div>;
}
return (
<div>
<h1>{post.title}</h1> {/* TypeScript biết post có title */}
{author ? (
<p>Tác giả: {author.name}</p> // TypeScript biết author có name
) : (
<p>Đang tải thông tin tác giả...</p>
)}
<p>{post.body}</p>
</div>
);
}
export default PostDetails;
Giải thích Code:
- Chúng ta lấy
postIdtừ router. postKeyđược tạo dưới dạng một mảng['/api/posts', postId]. SWR sẽ theo dõi sự thay đổi của cả hai phần tử trong mảng này. NếupostIdthay đổi,postKeythay đổi, và SWR sẽ fetch lại dữ liệu bài viết. NếupostIdlàundefined,postKeylànull, và SWR sẽ không fetch.fetchercho bài viết nhận[url, id]từ mảngkeyvà xây dựng URL hoàn chỉnh.authorIdchỉ được lấy sau khipostđã được fetch thành công (post?.authorId).authorKeycũng là một mảng phụ thuộc vàoauthorId. SWR sẽ chỉ fetch thông tin tác giả sau khiauthorIdcó giá trị (tức là sau khi bài viết đã tải xong). SWR tự động quản lý sự phụ thuộc này.- Chúng ta kết hợp các trạng thái loading và error từ cả hai hook
useSWR. - TypeScript giúp đảm bảo rằng khi chúng ta truy cập
post.titlehoặcauthor.name, các đối tượngpostvàauthorđã có cấu trúc dữ liệu chính xác (hoặc chúng ta đã kiểm tra điều kiện!posthay!author).
Cập Nhật Dữ Liệu với mutate
Một trong những tính năng mạnh mẽ của SWR là khả năng cập nhật dữ liệu trong cache và kích hoạt revalidation bằng hàm mutate. Hook useSWR trả về hàm mutate.
Bạn có thể dùng mutate theo nhiều cách:
mutate(): Kích hoạt revalidation chokeyhiện tại. SWR sẽ fetch lại dữ liệu trong nền.mutate(newData, false): Cập nhật cache vớinewDatangay lập tức và không kích hoạt revalidation. Hữu ích cho việc cập nhật UI tức thì sau một hành động (optimistic update).mutate(newData, true): Cập nhật cache vớinewDatangay lập tức và kích hoạt revalidation để xác nhận dữ liệu mới từ server.
Ví dụ về việc "Like" một bài viết và cập nhật số lượt like:
// Giả định API endpoint: POST /api/posts/{postId}/like để tăng like
// Trả về số lượt like mới
// components/LikeButton.tsx
import React from 'react';
import useSWR from 'swr';
import { fetchJson } from '../api';
interface PostLikes {
likes: number;
}
// Hàm fetcher cho số lượt like
const fetchLikes = async (url: string) => {
const data = await fetchJson<PostLikes>(url);
return data.likes; // Chỉ trả về số likes
}
// Hàm để gửi request like lên API
const sendLike = async (postId: number): Promise<number> => {
// Giả định API trả về số like mới sau khi thành công
const response = await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
if (!response.ok) {
throw new Error('Failed to like post');
}
const data: PostLikes = await response.json();
return data.likes;
};
function LikeButton({ postId }: { postId: number }) {
// Key cho số lượt like của bài viết cụ thể
const likesKey = `/api/posts/${postId}/likes`;
const { data: likes, error, isLoading, mutate } = useSWR<number, Error>(
likesKey,
fetchLikes // Sử dụng fetcher chỉ lấy số like
);
const handleLike = async () => {
// Bắt đầu optimistic update: Cập nhật UI ngay lập tức với số like tăng thêm 1
// mutate(newLikes, false) cập nhật cache và UI, không fetch lại
// currentLikes là giá trị hiện tại trong cache, mặc định là undefined nếu chưa có
const currentLikes = likes;
// Đặt trạng thái loading hoặc disabled nút bấm nếu cần ở đây
// Nếu có dữ liệu cache, cập nhật tạm thời lên UI
if (currentLikes !== undefined) {
mutate(currentLikes + 1, false);
}
try {
// Gửi request like lên API
const newLikes = await sendLike(postId);
// Sau khi API thành công, cập nhật cache với dữ liệu thật từ API
// mutate(newLikes, true) cập nhật cache VÀ fetch lại để xác nhận
// Việc fetch lại có thể cần thiết nếu logic server phức tạp hơn việc chỉ tăng 1
// hoặc nếu optimistic update có thể sai lệch.
// Nếu chỉ đơn giản là số like tăng 1 và bạn tin tưởng optimistic update,
// có thể không cần fetch lại (chỉ cần mutate(newLikes, false) lần nữa hoặc bỏ qua).
// Trong ví dụ này, ta dùng revalidation (true) để chắc chắn.
mutate(newLikes, true); // Cập nhật cache và kích hoạt revalidation
} catch (err) {
// Nếu API thất bại, rollback lại dữ liệu cache ban đầu (trước optimistic update)
// mutate(currentLikes, false) cập nhật cache trở lại giá trị ban đầu, không fetch lại
if (currentLikes !== undefined) {
mutate(currentLikes, false);
}
alert(`Lỗi khi like bài viết: ${err instanceof Error ? err.message : 'Unknown error'}`); // Thông báo lỗi
}
};
if (isLoading) return <button disabled>Loading Likes...</button>;
if (error) return <button disabled>Error Loading Likes</button>;
// TypeScript biết likes có kiểu number | undefined
const displayLikes = likes !== undefined ? likes : 0; // Default về 0 nếu chưa tải xong hoặc lần đầu
return (
<button onClick={handleLike} disabled={isLoading || likes === undefined}>
Like ({displayLikes})
</button>
);
}
export default LikeButton;
Giải thích Code:
- Chúng ta fetch số lượt like ban đầu bằng
useSWR<number, Error>(...). - Interface
PostLikesđịnh nghĩa cấu trúc dữ liệu từ API.fetchLikestrích xuấtlikesvà trả vềnumber. - Hàm
sendLikemô phỏng việc gọi API POST để tăng like. - Hàm
handleLikelà nơi xử lý logic khi người dùng click nút. - Optimistic Update: Ngay lập tức cập nhật UI với số like tăng 1 (
mutate(currentLikes + 1, false)). Điều này làm cho ứng dụng phản hồi nhanh chóng, người dùng thấy số like tăng lên ngay lập tức. - Sau đó, gọi
sendLikeđể thực sự gửi request lên server. - Nếu request thành công, chúng ta lấy số like mới từ server (
newLikes) và gọimutate(newLikes, true).trueở đây kích hoạt revalidation, đảm bảo dữ liệu hiển thị là chính xác từ server, đề phòng trường hợp optimistic update có sai sót. - Nếu request thất bại, chúng ta rollback lại UI bằng cách gọi
mutate(currentLikes, false), khôi phục số like về giá trị trước khi optimistic update. - TypeScript đảm bảo chúng ta làm việc với kiểu
numbercho số lượt like và kiểuErrorcho lỗi.
Global Configuration với SWRConfig
Bạn có thể thiết lập cấu hình SWR mặc định (như fetcher mặc định, các tùy chọn revalidation chung) bằng cách sử dụng component SWRConfig. Điều này rất hữu ích để tránh lặp lại fetcher trong mỗi lần gọi useSWR.
// pages/_app.tsx (trong Next.js) hoặc root component của ứng dụng React
import type { AppProps } from 'next/app';
import { SWRConfig } from 'swr';
import { fetchJson } from '../api'; // Sử dụng fetcher chung
function MyApp({ Component, pageProps }: AppProps) {
return (
<SWRConfig
value={{
// Thiết lập fetcher mặc định
fetcher: fetchJson,
// Các tùy chọn SWR mặc định khác
revalidateOnFocus: true,
revalidateIfStale: true,
// errorRetryInterval: 10000, // Thử lại sau 10 giây nếu lỗi
}}
>
<Component {...pageProps} />
</SWRConfig>
);
}
export default MyApp;
Khi fetcher mặc định đã được thiết lập bằng SWRConfig, bạn có thể gọi useSWR mà không cần truyền fetcher:
// components/MyComponent.tsx
import useSWR from 'swr';
interface Data {
message: string;
}
function MyComponent() {
// Không cần truyền fetcher vì đã cấu hình global
const { data, error } = useSWR<Data, Error>('/api/some-data');
if (error) return <div>Failed to load</div>;
if (!data) return <div>Loading...</div>;
return <div>{data.message}</div>;
}
Tuyệt vời phải không? TypeScript vẫn hoạt động hoàn hảo, và code của bạn trở nên gọn gàng hơn.
Tóm Lược và Lời Khuyên
SWR là một hook fetch dữ liệu mạnh mẽ giúp đơn giản hóa việc quản lý cache, loading, error và revalidation trong ứng dụng React/Next.js. Khi kết hợp với TypeScript, bạn không chỉ tận dụng được sức mạnh của SWR mà còn có thêm lớp an toàn kiểu, giảm thiểu lỗi và cải thiện trải nghiệm phát triển.
Một vài lời khuyên khi sử dụng SWR với TypeScript:
- Luôn Định Nghĩa Kiểu Dữ Liệu: Dành thời gian tạo interface hoặc type cho cấu trúc dữ liệu mà API của bạn trả về. Điều này là xương sống của việc sử dụng SWR hiệu quả với TypeScript.
- Type Cho
useSWR: Cung cấp kiểu dữ liệu cho cảdatavàerrortrong generic type củauseSWR<DataType, ErrorType>. - Type Cho Fetcher: Đảm bảo hàm
fetchercủa bạn được typed đúng để trả vềPromise<DataType>. - Quản Lý
key: Sử dụngkeyđộng (chuỗi hoặc mảng) để kích hoạt fetch lại khi dependencies thay đổi. Sử dụngnullhoặcundefinedlàmkeyđể bỏ qua việc fetch. - Leverage
mutate: Sử dụng hàmmutateđể cập nhật UI tức thì (optimistic update) và/hoặc kích hoạt revalidation sau các hành động thay đổi dữ liệu (POST, PUT, DELETE). - Global Config: Sử dụng
SWRConfigđể thiết lập fetcher mặc định và các tùy chọn chung, làm cho code gọn gàng hơn.
Việc nắm vững SWR và cách tích hợp nó với TypeScript sẽ giúp bạn xây dựng các ứng dụng front-end nhanh hơn, ổn định hơn và dễ bảo trì hơn. Hãy thử nghiệm và áp dụng nó vào các dự án của bạn!
Comments