Chào anh em, lại là lão già vẫn code tôi đây, đợt này tôi phải làm frontend nhiều quá nên mạn phép lan sang frontend. Anh em làm frontend có thể học, làm nhiều framework frontend rồi, và framework nào cũng có Quản lý State và đó luôn là câu chuyện muôn thủa.
Hnay tôi sẽ cùng anh em mổ xẻ vấn đề này, cụ thể hơn trong Vuejs, từ các thư viện "quốc dân" như Vuex/Pinia, cho đến việc lưu trữ state bền bỉ với Local Storage và IndexedDB. Khi nào nên dùng cái nào? Và làm sao để không tự bắn vào chân mình?
State là gì và tại sao phải quản lý nó?
Hãy tưởng tượng bạn đang xây một căn nhà. "State" chính là bản thiết kế điện nước ngầm. Nó là dữ liệu trung tâm quyết định trạng thái của ứng dụng: người dùng đã đăng nhập chưa, trong giỏ hàng có bao nhiêu sản phẩm, theme đang là sáng hay tối...
Khi ứng dụng nhỏ, việc truyền dữ liệu qua lại giữa các component (props & events) có vẻ ổn. Nhưng khi có hàng chục, hàng trăm component, "sơ đồ điện nước" của bạn sẽ trở thành một mớ dây nhện chằng chịt. Việc tìm lỗi và bảo trì sẽ là một cơn ác mộng.
Đó là lúc các thư viện quản lý state, cụ thể trong Vuejs như Vuex (thế hệ cũ) và Pinia (thế hệ mới, được Vue team khuyên dùng) ra đời. Chúng tạo ra một "kho chứa" (gọi là store) duy nhất, tập trung, nơi mọi component đều có thể lấy dữ liệu và thay đổi dữ liệu một cách có quy tắc.
Nhưng có một vấn đề: Khi người dùng refresh (F5) trình duyệt, toàn bộ state trong Pinia/Vuex sẽ biến mất! Dữ liệu này chỉ tồn tại trong bộ nhớ (memory). Đây là lúc chúng ta cần tìm đến những giải pháp lưu trữ bền bỉ hơn ngay tại trình duyệt.
Local Storage: "cái tủ lạnh" đơn giản và tiện lợi
Local Storage giống như một cái tủ lạnh nhỏ trong căn bếp của bạn. Nó đơn giản, dễ dùng, và hoàn hảo để cất những thứ đồ cần lấy ra nhanh chóng. Nó hoạt động dựa trên cơ chế key-value đơn giản. Bạn chỉ có thể lưu trữ dữ liệu dạng chuỗi (string).
Cách thức hoạt động với Vue/Pinia: Rất đơn giản. Bạn có thể sử dụng một plugin như pinia-plugin-persistedstate. Plugin này sẽ tự động "lắng nghe" sự thay đổi trong store của bạn và đồng bộ hóa nó xuống Local Storage. Khi người dùng tải lại trang, nó sẽ đọc dữ liệu từ Local Storage và khôi phục lại state cho Pinia.
Điểm mạnh khi sử dụng Local Storage:
- Cực kỳ dễ sử dụng: API rất đơn giản (setItem, getItem, removeItem). Tích hợp vào Pinia chỉ mất vài dòng code.
- Nhanh chóng: Truy cập đồng bộ (synchronous), nghĩa là code sẽ dừng lại đợi cho đến khi đọc/ghi xong. Với dữ liệu nhỏ, việc này diễn ra tức thì.
- Được hỗ trợ rộng rãi: Tất cả các trình duyệt hiện đại đều hỗ trợ.
Điểm yếu:
- Dung lượng hạn chế: Chỉ khoảng 5-10MB tùy trình duyệt. Không phù hợp để lưu trữ lượng dữ liệu lớn.
- Chỉ lưu trữ chuỗi (string): Muốn lưu object hay array, bạn phải JSON.stringify() khi lưu và JSON.parse() khi đọc.
- Chặn luồng chính (Blocking): Vì là đồng bộ, nếu bạn cố lưu một file JSON lớn, nó có thể làm "khựng" giao diện người dùng trong giây lát.
- Bảo mật kém: Dữ liệu được lưu dưới dạng text đơn giản, bất kỳ đoạn script nào chạy trên trang (kể cả script từ bên thứ ba) cũng có thể đọc được. Tuyệt đối không lưu thông tin nhạy cảm như token, mật khẩu ở đây.
Các case study:
- Lưu lựa chọn giao diện của người dùng: Dark mode/Light mode.
- Lưu ngôn ngữ người dùng đã chọn: i18n preference (vi/en).
- Lưu trạng thái cơ bản: Tên người dùng đã đăng nhập (chỉ tên, không phải token), các bộ lọc trên một trang danh sách...
VD: Một trang e-commerce nhỏ lưu lại các ID sản phẩm trong giỏ hàng của khách vãng lai (chưa đăng nhập). Khi họ quay lại, giỏ hàng vẫn còn đó. Dữ liệu này nhỏ và không nhạy cảm, hoàn toàn phù hợp.
Kinh nghiệm triển khai thực thế:
- Luôn bọc các thao tác với Local Storage trong một khối try...catch để xử lý các trường hợp người dùng tắt cookie/storage hoặc bộ nhớ đầy.
- Tạo một "wrapper", "service" hoặc "composable" riêng để quản lý việc đọc/ghi vào Local Storage, tránh gọi localStorage.setItem trực tiếp từ component.
IndexedDB: "Nhà Kho" cho Dữ liệu lớn
Nếu Local Storage là cái tủ lạnh, thì IndexedDB là cả một nhà kho có hệ thống kệ, ngăn, và quy trình xuất nhập kho rõ ràng. Nó là một hệ quản trị cơ sở dữ liệu NoSQL thực thụ ngay trong trình duyệt.
Cách thức hoạt động với Vue/Pinia: Việc tích hợp phức tạp hơn nhiều. API của IndexedDB là bất đồng bộ (asynchronous) và dựa trên sự kiện (event-based), khá "khó nhằn" cho người mới. May mắn là chúng ta có các thư viện wrapper như Dexie.js giúp đơn giản hóa mọi thứ. Bạn sẽ phải viết logic để đồng bộ state từ Pinia vào Dexie và ngược lại. Không có giải pháp "plug-and-play" dễ dàng như với Local Storage.
Lợi thế của IndexedDB:
- Dung lượng lưu trữ khổng lồ: Có thể lên tới hàng GB, giới hạn bởi dung lượng đĩa cứng của người dùng.
- Lưu trữ được mọi loại dữ liệu: Hỗ trợ lưu trữ object, array, file, Blob... một cách tự nhiên mà không cần chuyển đổi.
- Hiệu năng cao cho dữ liệu lớn: Hoạt động bất đồng bộ (non-blocking), không làm ảnh hưởng đến luồng chính. Bạn có thể thực hiện các truy vấn phức tạp (query, indexing) để tìm kiếm dữ liệu hiệu quả.
- Hỗ trợ Transaction: Đảm bảo tính toàn vẹn của dữ liệu khi thực hiện nhiều thao tác cùng lúc.
Điểm yếu:
- API phức tạp: Như đã nói, API gốc rất khó sử dụng. Kể cả khi dùng thư viện như Dexie.js, nó vẫn phức tạp hơn Local Storage nhiều.
- Thiết lập ban đầu tốn công sức: Bạn phải định nghĩa schema, version, và xử lý logic đồng bộ hóa thủ công.
Khi nào nên dùng:
- Ứng dụng làm việc ngoại tuyến (Offline-first): Lưu trữ email, tài liệu, tin nhắn để người dùng có thể xem và soạn thảo ngay cả khi không có mạng. Google Docs, Notion đều dùng cơ chế này.
- Lưu trữ dữ liệu lớn, phức tạp: Dữ liệu của một ứng dụng chỉnh sửa ảnh/video, dữ liệu biểu đồ phân tích...
- Caching dữ liệu từ API: Thay vì gọi API liên tục cho những dữ liệu ít thay đổi, bạn có thể cache chúng vào IndexedDB để tăng tốc độ tải trang và giảm tải cho server.
Best Practice:
- Sử dụng thư viện wrapper: Đừng bao giờ làm việc trực tiếp với API IndexedDB gốc. Hãy dùng Dexie.js, nó sẽ cứu bạn khỏi rất nhiều giờ debug.
- Quản lý phiên bản (Versioning): Khi bạn thay đổi cấu trúc dữ liệu, hãy tăng phiên bản của database và viết script migration để cập nhật dữ liệu cho người dùng cũ.
- Chỉ lưu những gì cần thiết: Đừng lạm dụng IndexedDB để lưu mọi state của ứng dụng. Hãy xác định rõ đâu là dữ liệu "bền bỉ" cần cho việc offline hoặc cache.
Ứng dụng thực tế: Một ứng dụng cho phép người dùng tạo và chỉnh sửa thông tin khách hàng trên đường, nơi mạng chập chờn. Toàn bộ dữ liệu được lưu vào IndexedDB. Khi có mạng trở lại, ứng dụng sẽ tự động đồng bộ lên server.
Hoặc lưu trữ lịch sử biểu đồ (kiểu Google Analysis), khi truy cập vào trang dữ liệu sẽ được load lên luôn từ IndexedDB thay vì load từ 1 api lịch sử lớn.
Kết luận: chọn đúng phương án cho đúng việc
Không có lựa chọn nào là "tốt nhất" một cách tuyệt đối. Với kinh nghiệm của mình, tôi khuyên anh em:
- Cần lưu trữ các cài đặt đơn giản, không nhạy cảm của người dùng? => Kết hợp Pinia với pinia-plugin-persistedstate sử dụng Local Storage. Nhanh, gọn, hiệu quả.
- Bạn đang xây dựng một ứng dụng phức tạp, cần hoạt động offline hoặc xử lý một lượng dữ liệu lớn phía client? → Hãy đầu tư thời gian tìm hiểu IndexedDB và một thư viện như Dexie.js. Đây là một khoản đầu tư xứng đáng cho hiệu năng và trải nghiệm người dùng sau này.
Việc hiểu rõ bản chất, điểm mạnh, điểm yếu của từng công cụ sẽ giúp bạn đưa ra những quyết định kiến trúc đúng đắn, tránh được những techdebt phải trả giá đắt trong tương lai.
Anh em đang giải quyết bài toán state persistence này như thế nào? Có "bí kíp" hay thư viện nào hay ho muốn chia sẻ không? Hãy để lại bình luận nhé!
#vuejs #pinia #statemanagement #localstorage #indexeddb #frontenddeveloper #webdevelopment #softwarearchitecture #bestpractices #1percentbetter