Hầu hết mã frontend coi các tác vụ bất đồng bộ như một dạng duy nhất. Bạn khởi chạy một Promise, đợi nó resolve, đổ kết quả vào local state, và để framework thực hiện reconcile sự khác biệt. Mô hình này rất hấp dẫn vì nó hoạt động ở mọi nơi: một lệnh gọi REST, một lần gửi form, hay một tin nhắn WebSocket. Tất cả chúng đều đổ dồn vào cùng một useEffect hoặc trình xử lý sự kiện, đều được đưa qua setState, và đều trông giống như những cơ chế xử lý bất đồng bộ giống hệt nhau. Sự đồng nhất đó là một cái bẫy. Trong một ứng dụng thực tế, không phải mọi tác vụ bất đồng bộ đều giống nhau. Việc giả vờ rằng chúng giống nhau sẽ biến các UI component của bạn thành những kiến trúc sư dữ liệu bất đắc dĩ, được chắp vá bằng các hook useEffect và những sự may rủi.
Các hoạt động bất đồng bộ thực tế chia thành ba loại khác nhau. Mỗi loại có một mối quan hệ khác nhau với thời gian, việc lưu bộ nhớ đệm (caching) và quyền sở hữu (ownership). Học cách phân biệt chúng là điều giúp frontend luôn nhanh, chính xác và ổn định.
Queries: Những sự thật có địa chỉ
Một query không chỉ đơn thuần là một lệnh fetch. Nó là một yêu cầu cho một sự thật có thể định danh được. Bạn đang yêu cầu /user/123, chứ không phải "một số dữ liệu người dùng nào đó". Sự phân biệt này rất quan trọng vì danh tính (identity) chính là thứ giúp việc caching trở nên khả thi. Nếu hai component trên cùng một màn hình đều cần cùng một bản ghi người dùng, chúng nên chia sẻ một câu trả lời duy nhất. Khi mỗi component giữ một bản sao cục bộ riêng trong useState, bạn đang làm phân mảnh sự thật của mình. Ảnh đại diện ở header và tên ở sidebar sẽ bị lệch nhau vì chúng được fetch vào các thời điểm khác nhau, hoặc một cái thất bại trong khi cái kia thành công.
Hãy coi một query như một tài nguyên, không phải một hành động. Nó có một cache key, một chính sách về tính mới của dữ liệu (freshness policy), và một vòng đời tồn tại lâu hơn bất kỳ component đơn lẻ nào. Một lớp query được xây dựng tốt sẽ hiểu rằng việc đọc /projects?page=2 khác với việc đọc /projects?page=3. Mỗi URL và bộ tham số tạo thành một địa chỉ, và dữ liệu tại địa chỉ đó có thể là cũ, mới, hoặc bị thiếu. UI không nên đảm nhận việc quản lý sổ sách này. Nó nên yêu cầu lớp dữ liệu cung cấp user:123 và nhận về một snapshot. Việc snapshot đó đến từ server cách đây hai giây hay từ cache cách đây hai mili giây không phải là việc của component.
Hệ quả thực tế là ngay lập tức. Khi bạn coi mọi lệnh đọc là một lệnh fetch mang tính mệnh lệnh (imperative fetch) bên trong một component, bạn sẽ mất khả năng loại bỏ trùng lặp (deduplication). Bạn mất khả năng làm mới ngầm (background refresh). Bạn mất khả năng hiển thị dữ liệu từ cache ngay lập tức trong khi vẫn đang xác thực nó ở chế độ chạy ngầm. Một query xứng đáng có một "ngôi nhà" bên ngoài cây UI của bạn.
Mutations: Thay đổi thế giới
Nếu queries hỏi về thế giới, thì mutations thay đổi nó. Yêu cầu mạng thực tế—POST, PUT, hoặc DELETE—thường là phần dễ dàng. Phần khó là tất cả những gì xảy ra sau khi server nói "OK."
Giả sử một người dùng cập nhật tên hiển thị của họ. Bản thân mutation chỉ là một yêu cầu duy nhất. Nhưng phạm vi ảnh hưởng (blast radius) thì ở khắp mọi nơi. Trang hồ sơ vẫn giữ tên cũ. Thanh điều hướng hiển thị tên cũ. Lịch sử bình luận có thể vẫn tham chiếu đến nó. Nếu mã mutation của bạn không làm gì ngoài việc bật một cờ isLoading cục bộ và sau đó cập nhật một phần trạng thái, ứng dụng của bạn hiện đang tự lừa dối chính mình. Một số góc của UI giả vờ rằng thay đổi đã xảy ra. Những góc khác thì không hề biết có bất kỳ thay đổi nào.
Một mutation phải tuyên bố tác động của nó lên đồ thị dữ liệu (data graph). Nó nên cho hệ thống biết query nào hiện không còn hợp lệ, các cache key nào cần được fetch lại, và các mối quan hệ nào đã thay đổi. Điều này về cơ bản khác với một query. Một query là chỉ đọc (read-only) và có thể chia sẻ. Một mutation tập trung vào việc ghi (write-focused) và có tính phá hủy đối với các cache hiện có. Việc gộp chúng vào cùng một lớp trừu tượng đồng nghĩa với việc các lập trình viên sẽ phải gọi refetch() một cách thủ công bên trong các component ngẫu nhiên, hoặc tệ hơn, rải rác các hook useEffect khắp cây component để "đồng bộ" trạng thái cục bộ trở lại cho khớp với server.
Mô hình sở hữu cũng khác biệt. Queries thường thuộc về cache. Một mutation thuộc về hành động của người dùng đã kích hoạt nó. Nó có một trạng thái chờ (pending state), một trạng thái lỗi (error state), và có khả năng là một giá trị lạc quan (optimistic value) cần được hoàn tác
