Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 23 — Empty / Loading / Error / Success states

Viết PRD & Product Spec cho PM Bài 23/60

Mở đầu — vì sao bài này quan trọng

Hãy thử nhớ lại lần cuối bạn mở một app mới tải về. Màn hình hiện ra trắng trơn, có dòng chữ "No data" ở giữa, và bạn chẳng biết phải làm gì tiếp theo. Hoặc bạn bấm nút "Tải lên", màn hình đứng im vài giây, bạn không biết app đang xử lý hay đã treo, thế là bạn bấm lại lần nữa — và vô tình tạo ra hai bản ghi trùng. Những trải nghiệm khó chịu đó không phải lỗi của developer. Chúng là lỗi của PM — người đã viết spec mà quên mất rằng một màn hình không chỉ có "trạng thái bình thường khi mọi thứ chạy tốt".

Đây chính là điểm phân biệt giữa một PRD nghiệp dư và một spec chuyên nghiệp. PM mới vào nghề thường chỉ mô tả "happy path" — kịch bản lý tưởng khi user có đầy đủ dữ liệu, mạng nhanh, server phản hồi tức thì. Nhưng trong thực tế, mỗi màn hình hiển thị dữ liệu đều phải sống qua bốn trạng thái: Empty (chưa có gì), Loading (đang tải), Error (có lỗi), và Success (có dữ liệu, hiển thị bình thường). Nếu bạn không spec rõ cả bốn, engineer sẽ tự đoán — và họ thường đoán theo cách rẻ nhất, nhanh nhất, không phải cách tốt nhất cho người dùng.

Trong bài này, chúng ta sẽ học cách spec đầy đủ bốn trạng thái UI cho mỗi màn hình hiển thị dữ liệu. Đây là kỹ năng giúp PRD của bạn "không có lỗ hổng" để team không phải quay lại hỏi, và quan trọng hơn, giúp sản phẩm cuối cùng trông tử tế ở mọi tình huống chứ không chỉ trong demo.

Khái niệm cốt lõi

Quy tắc nền tảng rất đơn giản: bất kỳ thành phần UI nào hiển thị dữ liệu lấy từ server hoặc database đều có thể tồn tại ở bốn trạng thái. Là PM, nhiệm vụ của bạn trong spec là mô tả từng trạng thái đó hiển thị cái gì, nói gì với người dùng, và cho họ làm gì tiếp theo.

1. Empty state — khi chưa có dữ liệu

Empty state xảy ra khi truy vấn thành công nhưng kết quả trả về rỗng. Có hai loại empty state mà bạn cần phân biệt rõ trong spec:

  • First-use empty (lần đầu rỗng): User mới, chưa từng tạo dữ liệu nào. Ví dụ: hộp thư mới chưa có email, ví điện tử mới chưa có giao dịch. Đây là cơ hội vàng để hướng dẫn (onboarding), không phải để báo lỗi.
  • User-cleared empty (rỗng do người dùng): User từng có dữ liệu nhưng đã xóa hết, hoặc bộ lọc/tìm kiếm không ra kết quả. Ví dụ: tìm "áo khoác size XXL" mà shop hết hàng.
Sai lầm kinh điển là hiển thị một dòng chữ xám "Không có dữ liệu" cho cả hai trường hợp. Empty state tốt phải làm ba việc: giải thích vì sao trống, nêu giá trị (value proposition — tại sao user nên tạo dữ liệu ở đây), và đưa một CTA rõ ràng (call-to-action — nút hành động cụ thể). Với first-use, hãy biến màn hình trống thành lời mời. Với search-empty, hãy gợi ý cách điều chỉnh (xóa bộ lọc, thử từ khóa khác).

2. Loading state — khi đang tải

Loading state che lấp khoảng thời gian chờ dữ liệu về. Vấn đề tâm lý ở đây là: người dùng cảm nhận thời gian chờ dài hơn thực tế nếu không có phản hồi. Có ba kỹ thuật chính cần spec rõ:

  • Skeleton screen (khung xương): hiển thị các khối xám mờ có hình dạng giống nội dung sắp tải. Đây là lựa chọn tốt nhất cho màn hình có cấu trúc cố định (danh sách, card, profile) vì nó tạo cảm giác "sắp xong rồi" và giảm cảm giác chờ đợi. Skeleton screen được ưu tiên hơn spinner xoay tròn vô định.
  • Spinner: phù hợp cho hành động ngắn (< 1 giây) hoặc khi không đoán được hình dạng nội dung, ví dụ nút "Lưu" đang xử lý.
  • Progress bar: dùng khi biết được tiến độ (upload file, tải xuống), vì nó cho user biết còn bao lâu.
Spec của bạn cần ghi rõ ngưỡng thời gian: ví dụ "hiển thị skeleton ngay lập tức; nếu sau 10 giây vẫn chưa có dữ liệu, chuyển sang error state với thông điệp timeout."

3. Error state — khi có lỗi

Error state là nơi PM hay bỏ quên nhất, nhưng lại quyết định việc user có quay lại hay không. Một error state tốt cần: nói rõ chuyện gì đã xảy ra bằng ngôn ngữ con người (không phải "Error 500"), không đổ lỗi cho user, và cho một lối thoát (nút "Thử lại", hoặc hướng dẫn liên hệ hỗ trợ). Bạn cũng nên phân loại lỗi trong spec: lỗi mạng (mất kết nối), lỗi server (5xx), lỗi do dữ liệu đầu vào (4xx), lỗi quyền truy cập (403). Mỗi loại cần một thông điệp và một hành động khác nhau.

4. Success state — khi mọi thứ chạy bình thường

Đây là "happy path" mà ai cũng nghĩ tới đầu tiên: dữ liệu về đầy đủ, hiển thị danh sách/nội dung như thiết kế. Nhưng đừng quên các biến thể: dữ liệu một phần (đã tải 50/200 dòng), hoặc success kèm thông báo (lưu thành công, hiện toast xác nhận). Với hành động ghi dữ liệu, success state cần phản hồi rõ ràng — đừng để user tự hỏi "xong chưa?".

Tình huống thực tế

Ví dụ 1: Ví điện tử MoMo và màn hình lịch sử giao dịch

Hãy tưởng tượng team PM của một ví điện tử như MoMo đang spec màn hình "Lịch sử giao dịch". Phiên bản đầu của spec chỉ viết: "Hiển thị danh sách các giao dịch theo thứ tự thời gian giảm dần." Nghe đủ rồi, đúng không? Không hề.

Khi build xong, đội QA phát hiện ba lỗ hổng. Với người dùng vừa tạo ví xong (first-use), màn hình hiện dòng chữ "Không có giao dịch" — khiến họ tưởng app bị lỗi. Khi mạng 3G chập chờn ở vùng quê, màn hình đứng trắng 8 giây không có gì, người dùng thoát app vì nghĩ treo. Và khi server bảo trì, app hiện "null" ở giữa màn hình.

Đội PM viết lại spec với đầy đủ bốn trạng thái: Empty (first-use) hiển thị minh họa cùng dòng "Bạn chưa có giao dịch nào. Nạp tiền hoặc thanh toán để bắt đầu" kèm nút "Nạp tiền ngay". Loading dùng skeleton 5 dòng giao dịch giả màu xám, xuất hiện tức thì. Error mạng hiện "Không tải được lịch sử. Kiểm tra kết nối và thử lại" kèm nút "Thử lại". Success hiển thị danh sách bình thường, có pull-to-refresh.

Bài học: Cùng một màn hình, nhưng spec từ 1 dòng thành 4 trạng thái rõ ràng. Sự khác biệt giữa app "trông lỗi" và app "trông tin cậy" nằm hết ở đây — và nó hoàn toàn là trách nhiệm của PM, không phải designer hay engineer.

Ví dụ 2: Tiki và kết quả tìm kiếm rỗng

Một sàn thương mại điện tử như Tiki từng đo được rằng trang "không tìm thấy sản phẩm" có tỷ lệ thoát (bounce rate) lên tới 70%. Khi user gõ "máy lọc nước Kangaroo 9 lõi" mà không khớp sản phẩm nào, màn hình cũ chỉ hiện "Không tìm thấy kết quả nào" — cụt lủn, và user rời đi.

PM phụ trách search viết lại spec cho empty state của trang kết quả. Phiên bản mới phân biệt rõ: nếu là lỗi đánh máy hoặc từ khóa quá hẹp, hiển thị "Không có sản phẩm khớp với 'máy lọc nước Kangaroo 9 lõi'" cùng ba hành động — gợi ý từ khóa gần đúng ("Có phải bạn muốn tìm: máy lọc nước Kangaroo?"), nút xóa bộ lọc đang áp dụng, và một dải sản phẩm gợi ý cùng danh mục. Quan trọng nhất, spec ghi rõ phải log lại các truy vấn rỗng để team phân tích và bổ sung hàng hóa.

Sau khi triển khai, tỷ lệ user tiếp tục mua sắm sau khi gặp trang rỗng tăng đáng kể, vì màn hình không còn là "ngõ cụt" mà thành "ngã ba có biển chỉ đường".

Bài học: Empty state không phải để báo "hết". Nó là cơ hội giữ chân user và điều hướng họ sang hành động tiếp theo. Một empty state được spec tốt có thể trực tiếp tác động đến doanh thu.

Ví dụ 3: App giao đồ ăn ShopeeFood và bug double-submit

Một startup giao đồ ăn (tạm gọi theo mô hình ShopeeFood) gặp sự cố nghiêm trọng: khoảng 3% đơn hàng bị tạo trùng. Điều tra ra nguyên nhân: khi user bấm "Đặt đơn", nút không đổi trạng thái loading. Mạng chậm khiến request mất 4-5 giây, user tưởng chưa ăn, bấm lại — và hệ thống tạo hai đơn.

Lỗi gốc nằm ở spec. PM đã viết chi tiết flow đặt hàng nhưng không hề mô tả loading state của chính nút bấm. Engineer mặc định để nút như cũ. Bản fix rất đơn giản về mặt spec: "Ngay khi bấm Đặt đơn, nút chuyển sang trạng thái disabled, đổi label thành 'Đang xử lý...' kèm spinner, chặn mọi lần bấm tiếp theo. Khi server phản hồi: success → chuyển sang màn hình xác nhận; error → bật lại nút và hiện thông báo lỗi cụ thể."

Bài học: Loading state không chỉ là chuyện thẩm mỹ. Thiếu nó có thể gây hậu quả nghiệp vụ thật — đơn trùng, mất tiền, mất uy tín. Mỗi nút thực hiện hành động tốn thời gian đều cần được spec trạng thái loading và khóa chống bấm trùng (debounce/disable).

Hướng dẫn từng bước

Đây là quy trình bạn có thể áp dụng cho mọi màn hình khi viết spec:

Bước 1 — Liệt kê mọi thành phần hiển thị dữ liệu. Với mỗi màn hình, đánh dấu các khối lấy dữ liệu từ server: danh sách, card, biểu đồ, avatar, số liệu. Mỗi khối này là một "ứng viên" cần bốn trạng thái.

Bước 2 — Với mỗi khối, viết bảng bốn trạng thái. Dùng một bảng đơn giản với các cột: Trạng thái | Khi nào xảy ra | Hiển thị gì | Văn bản (copy) | Hành động cho user. Điền đủ bốn hàng: Empty, Loading, Error, Success.

Bước 3 — Tách Empty thành các sub-case. Tự hỏi: first-use rỗng và filter/search rỗng có khác nhau không? Nếu có, mỗi loại cần một thiết kế riêng. Viết rõ value prop và CTA cho từng loại.

Bước 4 — Định nghĩa ngưỡng cho Loading. Ghi rõ: hiển thị loading nào (skeleton/spinner/progress), xuất hiện sau bao lâu, và timeout bao lâu thì chuyển sang error.

Bước 5 — Phân loại Error. Liệt kê các loại lỗi (mạng, server, quyền, dữ liệu) và với mỗi loại: thông điệp bằng ngôn ngữ người dùng + một hành động phục hồi (retry, đăng nhập lại, liên hệ hỗ trợ).

Bước 6 — Viết chính xác văn bản hiển thị (microcopy). Đừng để "TBD" hoặc "thông báo lỗi phù hợp". Viết đúng từng chữ tiếng Việt mà user sẽ đọc. Đây là phần PM phải làm, không đẩy sang ai khác.

Bước 7 — Đối chiếu với designer và engineer. Mang bảng bốn trạng thái vào buổi review để mọi người xác nhận đã có mock/handling cho từng ô.

Lỗi thường gặp & mẹo

Lỗi 1: Chỉ spec happy path. Đây là lỗi phổ biến nhất. Mẹo: tự đặt câu hỏi "Nếu danh sách này rỗng thì sao? Nếu mạng rớt giữa chừng thì sao?" cho mỗi màn hình. Nếu spec không trả lời được, nó chưa xong.

Lỗi 2: Dùng một thông điệp chung chung cho mọi empty/error. "Có lỗi xảy ra" không giúp user làm gì cả. Mẹo: mỗi trạng thái cần đáp ứng "user đang ở đâu, chuyện gì xảy ra, làm gì tiếp theo".

Lỗi 3: Quên loading cho các hành động (action), không chỉ cho màn hình. Nút bấm, form submit, toggle — tất cả đều cần phản hồi tức thì. Mẹo: bất cứ thứ gì gọi API đều phải có trạng thái "đang xử lý" và khóa chống bấm trùng.

Lỗi 4: Không phân biệt first-use empty và cleared empty. User mới cần onboarding; user đã xóa hết dữ liệu cần lời nhắc khác. Mẹo: hỏi engineer xem có cách phân biệt được hai trường hợp này không (thường dựa vào lịch sử tài khoản).

Lỗi 5: Để error state thành ngõ cụt. Báo lỗi mà không có nút thoát khiến user bế tắc. Mẹo: mọi error state phải có ít nhất một hành động — dù chỉ là "Thử lại" hoặc "Quay về trang chủ".

Mẹo vàng: Tạo một "checklist 4 trạng thái" và dán nó vào template PRD của bạn. Mỗi lần spec một màn hình mới, chạy qua checklist. Thói quen nhỏ này giúp bạn không bao giờ bỏ sót, và engineer sẽ rất biết ơn vì không phải đoán.

Bài tập thực hành

Hãy chọn một màn hình quen thuộc — ví dụ trang "Danh sách yêu thích" (wishlist) trong một app thương mại điện tử — và làm các việc sau:

  • Vẽ bảng bốn trạng thái cho màn hình này với năm cột: Trạng thái, Khi nào xảy ra, Hiển thị gì, Văn bản hiển thị (viết tiếng Việt chính xác), Hành động cho user.
  • Tách empty state thành hai sub-case: user chưa từng thêm sản phẩm nào vào wishlist, và user đã xóa hết. Viết value prop + CTA riêng cho mỗi trường hợp.
  • Định nghĩa loading: bạn dùng skeleton hay spinner? Bao nhiêu dòng skeleton? Timeout bao lâu thì chuyển error?
  • Liệt kê ít nhất hai loại error (mạng, server) và viết microcopy + hành động phục hồi cho mỗi loại.
  • Bonus: Mở một app bạn đang dùng, tìm một màn hình có empty hoặc error state làm chưa tốt. Viết lại spec để cải thiện nó. So sánh phiên bản gốc và phiên bản của bạn — bạn sẽ thấy rõ giá trị PM mang lại.
Khi làm xong, hãy mang bảng này ra hỏi một engineer: "Với bốn trạng thái này, anh/chị có đủ thông tin để build không?" Nếu họ không phải hỏi lại gì, bạn đã spec đúng.

Tóm tắt

Mỗi thành phần UI hiển thị dữ liệu đều sống qua bốn trạng thái: Empty, Loading, Error, Success — và nhiệm vụ của PM là spec đầy đủ cả bốn, không chỉ happy path. Empty state là cơ hội onboarding và điều hướng, không phải dòng chữ "No data" khô khan; hãy phân biệt first-use và cleared/search-empty. Loading state quản lý cảm nhận thời gian chờ — ưu tiên skeleton screen, định nghĩa rõ ngưỡng và timeout, và đừng quên loading cho cả các nút hành động để tránh bug double-submit. Error state phải nói ngôn ngữ con người, không đổ lỗi user, và luôn có lối thoát. Success state cần phản hồi rõ ràng, kể cả các biến thể dữ liệu một phần. Hãy biến "bảng bốn trạng thái" thành thói quen trong template PRD của bạn — đó là dấu hiệu rõ nhất phân biệt một spec chuyên nghiệp với một spec nghiệp dư.