Skip to main content

State ownership: server, URL hay local UI?

Phần lớn bug state trong ứng dụng Inertia không đến từ thiếu state manager. Chúng đến từ việc một giá trị có hai hoặc ba owner cùng lúc. Ví dụ xấu:
Khi ba nguồn đều có vẻ “đúng”, code sẽ cần useEffect để đồng bộ lẫn nhau và rất nhanh trở thành vòng lặp stale state.

1. Nguyên tắc single owner

Mỗi state quan trọng nên có một canonical owner. Các tầng khác chỉ giữ projection/draft/cache có lifecycle rõ.

2. Decision tree thực chiến

Khi chuẩn bị tạo useState, hỏi:

3. Server state: đừng mirror props nếu không có lý do

Anti-pattern:
Sau filter:
products mới về nhưng rows vẫn snapshot cũ.

Đúng khi chỉ render server state

Local copy chỉ hợp lý khi có lifecycle khác

Ví dụ drag reorder chưa save:
Ở đây draftOrder có semantics rõ: unsaved local draft.

4. URL state: state nào user kỳ vọng share được thì nên lên URL

Ví dụ product list:
Backend:
Frontend draft search:
Ở đây có hai state nhưng không duplicate ownership:

5. URL hay local cho tab?

Không phải tab nào cũng giống nhau.

Tab dữ liệu nên URL-owned

Nếu reload/share link phải vẫn mở Inventory, URL là owner.

Tab cosmetic có thể local

Nếu tab chỉ thay bố cục tạm thời và không đáng bookmark, local state đơn giản hơn.

6. Form state là draft, không phải server truth

Server prop:
Form draft:
Sau khi user gõ:
Đây không phải inconsistency. Hai state có semantics khác nhau.

Sai: sync prop vào form mỗi render/effect

Effect này có thể xóa draft user đang nhập khi prop refresh.

Tốt hơn

  • khởi tạo draft khi component/entity identity thay đổi;
  • dùng key theo entity;
  • reset có chủ đích sau success.

7. History state: khác server cache

useRemember giải quyết:
Ví dụ:
Không dùng useRemember để “cache products khỏi phải query lại”. Server data freshness là bài toán khác.

8. Shared props: context server-driven, không phải Redux replacement

Phù hợp:
Không phù hợp:
Lý do không chỉ là payload. Shared props làm dependency trở nên implicit ở mọi page.

9. Derived state: thường không cần owner riêng

Sai:
Nếu đây chỉ là derived UI value:
Hoặc nếu count cần đúng toàn dataset, server phải sở hữu:

10. Optimistic state: temporary projection, không được chiếm canonical ownership

Mental model:
Nếu bạn bắt đầu thêm một global optimisticProductsStore song song với Inertia props, hãy dừng và xác định lại ownership.

11. Modal state: URL hay local?

Confirm delete -> local

Không cần URL vì refresh không cần giữ modal. Nếu /products?edit=42 phải share được, server/URL nên biết entity đang edit. Có thể render drawer dựa trên query prop:
Trade-off: URL-owned drawer phức tạp hơn nhưng có deep-link/back-button semantics tốt hơn.

12. Pagination: URL là owner, selected rows thường local

Nếu selection phải sống khi chuyển page, local state ở page component có thể chưa đủ. Hãy quyết định rõ:
  • reset selection khi page thay đổi;
  • remember selection qua history;
  • hoặc server-own một batch selection token nếu workflow lớn.
Đừng vô thức giữ selected IDs của row không còn visible.

13. Permission state: server là owner

Server resource:
Frontend chỉ dùng cho UX:
Policy vẫn enforce lại endpoint. Frontend permission prop không bao giờ là security truth.

14. Loading state cũng có owner

Đừng tạo một global isLoading cho mọi async interaction. Loading state nên cùng lifecycle với operation sinh ra nó.

15. Error state cũng có scope

Validation field error:
Page-level 403/404:
Network failure:
External API error:
Dồn mọi lỗi vào globalError khiến UI khó biết lỗi thuộc interaction nào.

16. Race condition là ownership theo thời gian

Giả sử:
Nếu A về sau B và overwrite UI thì “request cũ” đã chiếm ownership sai thời điểm. Giải pháp không nhất thiết là global store. Trước hết:
  • debounce;
  • dùng Inertia visit lifecycle/cancellation semantics;
  • commit query vào URL;
  • không mirror prop vào local store.

17. Anti-pattern: prop -> state -> effect -> router -> prop loop

Đây là dấu hiệu ownership không rõ. Tốt hơn dùng event explicit:
Input có thể lấy trực tiếp filters.status nếu không cần draft phase.

18. Case study: Product Index hoàn chỉnh

Phân loại state:
Khi ownership rõ, component gần như không cần synchronization effects.

19. Case study: Product Edit có image upload

Không nên set product.image_url = previewUrl. Preview không phải persisted image.

20. Khi nào global client store thực sự hợp lý?

Có, nhưng ít hơn bạn nghĩ. Ví dụ:
  • rich client editor có undo/redo phức tạp độc lập navigation;
  • realtime collaborative state cần merge event liên tục;
  • offline-first workflow;
  • highly interactive canvas/designer;
  • state cross-page không phù hợp URL, server props hay history state.
Ngay cả lúc đó, vẫn nên định nghĩa boundary:
Không để cùng một entity vừa canonical trong page props vừa canonical trong store.

21. Ownership review checklist

Khi review PR, hỏi:

22. Heuristic cuối cùng

Nếu bạn thấy rất nhiều effect dạng:
đó thường không phải “React cần effect”. Đó là dấu hiệu state ownership chưa được quyết định dứt khoát. Một application Inertia dễ maintain thường có flow một chiều:
Chỉ thêm lớp state khác khi lớp đó có lifecycle và trách nhiệm khác thật sự.

Tài liệu chính thức

Nội dung thực chiến trong bài được xây dựng dựa trên API và nguyên lý của Inertia.js v3 Documentation. Khi áp dụng vào dự án, hãy đối chiếu API cụ thể với tài liệu chính thức theo phiên bản bạn đang sử dụng.