Part 07 · Reusable Logic · 7.1.06

Custom Hooks và external stores

Custom Hook đóng gói stateful logic với một contract rõ ràng; nó không tự chia sẻ state giữa các component. Khi dữ liệu sống ngoài React, useSyncExternalStore tạo subscription nhất quán với concurrent rendering và hydration.

Mental model: component mô tả UI, Hook mô tả reusable React logic, Context vận chuyển dependency trong tree, external store sở hữu state bên ngoài React. Chọn đúng abstraction trước khi tối ưu API.

1. Rules of Hooks và call order

Chỉ gọi Hook ở top level của function component hoặc custom Hook. Không gọi trong condition, loop, nested callback, event handler, class hay try/catch. React gắn state với thứ tự call; nếu control flow thay đổi thứ tự đó, state slot không còn ánh xạ đúng.

function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const sync = () => setOnline(navigator.onLine);
    window.addEventListener('online', sync);
    window.addEventListener('offline', sync);
    return () => {
      window.removeEventListener('online', sync);
      window.removeEventListener('offline', sync);
    };
  }, []);

  return online;
}

Tên custom Hook phải bắt đầu bằng use để con người và eslint plugin nhận ra nó chịu Rules of Hooks. Đừng đặt tên use... cho utility thuần không gọi Hook, vì tên đó tạo contract sai.

2. Custom Hook chia sẻ logic, không chia sẻ state instance

Mỗi lần gọi Hook có state và Effect riêng. Hai component gọi useOnlineStatus() tái sử dụng cách subscribe nhưng không dùng chung state slot. State chỉ thực sự shared nếu Hook đọc cùng Context, module singleton, cache hoặc external store.

Nhu cầuAbstractionLưu ý
Reuse calculation thuầnFunction thườngKhông cần Hook
Reuse state/effect behaviorCustom HookMỗi call độc lập
Inject value theo subtreeContext + Hook facadeProvider xác định scope
Subscribe state ngoài ReactuseSyncExternalStoreCần snapshot contract
Remote cacheFramework/query libraryFreshness và async lifecycle riêng

3. Thiết kế contract của Hook

Input nên diễn tả configuration và dependency thật; output nên mang domain semantics. Tuple phù hợp cho cặp ngắn, quen thuộc; object dễ đọc hơn khi có nhiều action hoặc field optional. Đừng che lifecycle và failure quan trọng: caller cần biết pending, error, retry, ownership và khi nào cleanup xảy ra.

Abstraction test: nếu tên Hook mô tả timing implementation như useMount thay vì mục đích domain, hãy kiểm tra lại. Lifecycle React không phải business contract và dễ che dependency thay đổi.

4. useSyncExternalStore contract

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?) đọc một store mutable bên ngoài và khiến component cập nhật khi snapshot thay đổi. React dùng contract này để tránh tearing — các component trong cùng một commit nhìn thấy các phiên bản store khác nhau.

function subscribe(callback) {
  store.addListener(callback);
  return () => store.removeListener(callback);
}

function getSnapshot() {
  return store.getState();
}

function useStore() {
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

subscribe nên khai báo ngoài component hoặc giữ identity ổn định; nếu function đổi, React phải resubscribe. getSnapshot phải trả cùng reference khi store không đổi. Với store mutable, cache immutable snapshot thay vì tạo object mới mỗi call, nếu không React có thể render liên tục.

5. SSR, hydration và selectors

getServerSnapshot chạy khi server render và trong hydration. Snapshot server/client ban đầu phải tương đương; nếu serialize store vào HTML, đọc đúng dữ liệu đó ở client trước khi nhận update mới. Nếu không có meaningful server value, có thể bỏ tham số để component chỉ render client-side, nhưng cần chấp nhận trade-off.

Store lớn nên hỗ trợ selector để component chỉ nhận slice cần thiết. Selector/equality phải giữ correctness trước performance: result không đổi nên reuse reference, derived collection cần memoization phù hợp, và subscription vẫn phải thông báo khi input của slice thay đổi.

6. Chọn library và kiểm thử

Reducer + Context đủ cho workflow vừa và update không quá rộng. Dedicated client store hợp lý khi cần selectors, granular subscriptions, middleware, persistence hoặc devtools. Query cache dành cho server state với freshness, deduplication, invalidation và retry; đừng dùng một client store như cache mạng tự chế nếu không có lý do rõ.

Test Hook qua component/harness công khai: initial snapshot, update notification, cleanup, error và SSR/hydration match. Với external store, test rằng unchanged snapshot giữ identity, nhiều subscriber thấy cùng version, unsubscribe thực sự dừng update và transition không tạo tearing.

Review checklist: Rules of Hooks đạt; abstraction mang domain meaning; mỗi call có ownership rõ; subscribe ổn định; snapshot cached/immutable; server snapshot hydrate khớp; library được chọn theo lifecycle và failure model.
Tài liệu: Reusing Logic with Custom Hooks · Rules of Hooks · useSyncExternalStore API · Subscribing to a browser API · Server rendering support