Phone:

+84 344184570

Email:

nguyentamhoang12a@gmail.com

© 2025 Hoang Nguyen.

Posted by:

Hoang Nguyen

Category:

Technical

Posted on:

01 August 2025

Tối ưu Zod Refinements trong React Forms: Câu chuyện về useRefinement Hook

Tối ưu Zod Refinements trong React Forms: Câu chuyện về useRefinement Hook

Vấn đề thực tế: Form đăng ký tài khoản "lag" không chịu nổi

Hôm qua, tôi đang review code của một junior developer trong team. Anh ấy implement một form đăng ký tài khoản với validation real-time khá đơn giản:

1// Form đăng ký tài khoản - Version "thảm họa"
2const RegisterForm = () => {
3  const schema = z.object({
4    username: z.string()
5      .min(3, "Username phải ít nhất 3 ký tự")
6      .refine(async (username) => {
7        // Gọi API check username có tồn tại chưa
8        const response = await fetch(`/api/check-username/${username}`);
9        const data = await response.json();
10        return !data.exists;
11      }, "Username đã tồn tại"),
12    
13    email: z.string()
14      .email("Email không hợp lệ")
15      .refine(async (email) => {
16        // Gọi API check email có tồn tại chưa
17        const response = await fetch(`/api/check-email/${email}`);
18        const data = await response.json();
19        return !data.exists;
20      }, "Email đã tồn tại"),
21  });
22
23  const form = useForm({
24    resolver: zodResolver(schema),
25    mode: "onChange" // Validate mỗi khi user typing
26  });
27
28  // ... rest of form
29};

Nhìn qua thì code cũng ổn, nhưng khi test thực tế thì... thảm họa!

Thảm họa khi user typing

Khi user gõ username "john123", điều gì xảy ra?

  • Gõ "j" → API call check "j"

  • Gõ "jo" → API call check "jo"

  • Gõ "joh" → API call check "joh"

  • Gõ "john" → API call check "john"

  • Gõ "john1" → API call check "john1"

  • Gõ "john12" → API call check "john12"

  • Gõ "john123" → API call check "john123"

7 ký tự = 7 API calls! Network tab trông như pháo hoa 🎆

Không chỉ vậy, vì Zod chạy toàn bộ schema validation mỗi khi có thay đổi, nên khi user sửa email thì refinement của username cũng chạy lại, dù username không đổi.

Dev Tools cho thấy sự thật đau lòng

Mở Performance tab trong Chrome DevTools:

  • CPU spike liên tục

  • Memory usage tăng dần

  • Network requests như mưa rào

  • User experience: "Sao form này lag thế?"

Giải pháp: useRefinement Hook

Sau một hồi research và brainstorm, tôi quyết định viết một custom hook để giải quyết vấn đề này:

1// Version được cứu rỗi
2const RegisterForm = () => {
3  const checkUsername = useRefinement(
4    async (username, { signal }) => {
5      const response = await fetch(`/api/check-username/${username}`, {
6        signal // Có thể cancel request nếu cần
7      });
8      const data = await response.json();
9      return !data.exists;
10    },
11    { debounce: 500 } // Chờ 500ms sau khi user ngừng typing
12  );
13
14  const checkEmail = useRefinement(
15    async (email, { signal }) => {
16      const response = await fetch(`/api/check-email/${email}`, {
17        signal
18      });
19      const data = await response.json();
20      return !data.exists;
21    },
22    { debounce: 500 }
23  );
24
25  const schema = z.object({
26    username: z.string()
27      .min(3, "Username phải ít nhất 3 ký tự")
28      .refine(checkUsername, "Username đã tồn tại"),
29    
30    email: z.string()
31      .email("Email không hợp lệ")
32      .refine(checkEmail, "Email đã tồn tại"),
33  });
34
35  const form = useForm({
36    resolver: zodResolver(schema),
37    mode: "onChange"
38  });
39
40  return (
41    <form onSubmit={form.handleSubmit(onSubmit)}>
42      <input 
43        {...form.register("username")}
44        onChange={(e) => {
45          form.register("username").onChange(e);
46          // Invalidate cache khi user typing để cho phép validation mới
47          checkUsername.invalidate();
48        }}
49      />
50      <input 
51        {...form.register("email")}
52        onChange={(e) => {
53          form.register("email").onChange(e);
54          checkEmail.invalidate();
55        }}
56      />
57      <button type="submit">Đăng ký</button>
58    </form>
59  );
60};

Kết quả sau khi áp dụng

Trước:

  • 7 ký tự = 7 API calls

  • Refinement chạy lại không cần thiết

  • Form lag, user experience tệ

Sau:

  • User gõ "john123" → Chờ 500ms → 1 API call duy nhất

  • Refinement được cache, không chạy lại nếu data không đổi

  • Form mượt mà, user hài lòng

Deep Dive: Cách useRefinement Hook hoạt động

1. Interface Definitions

typescript

1export interface Refinement<T> {
2  (data: T): boolean | Promise<boolean>; 

Refinement<T> là một function với thêm method invalidate(). Đây là pattern mở rộng function trong TypeScript - function vừa có thể gọi được vừa có thêm properties.

2. Hook chính

typescript

1export default function useRefinement<T>(
2  callback: RefinementCallback<T>,
3  { debounce }: { debounce?: number } = {}
4): Refinement<T> {
5  const ctxRef = useRef() as MutableRefObject<RefinementContext<T>>;
6  const refinementRef = useRef() as MutableRefObject<{
7    refine: Refinement<T>;
8    abort(): void;
9  }>;
10
11  ctxRef.current = { callback, debounce };
12
13  if (refinementRef.current == null) {
14    refinementRef.current = createRefinement(ctxRef);
15  }
16
17  useEffect(() => () => refinementRef.current.abort(), []);
18
19  return refinementRef.current.refine;
20}

Giải thích từng dòng:

  • ctxRef: Lưu trữ callback và debounce options. Cập nhật mỗi render để đảm bảo luôn có reference mới nhất.

  • refinementRef: Lưu trữ refinement function và abort function. Chỉ tạo một lần.

  • createRefinement chỉ được gọi một lần duy nhất khi hook được khởi tạo.

  • useEffect cleanup: Khi component unmount, abort mọi operation đang chạy.

3. Core Logic: createRefinement

typescript

1function createRefinement<T>(ctxRef: MutableRefObject<RefinementContext<T>>) {
2  let abortController: AbortController | null = null;
3  let result: Promise<boolean> | null = null;
4  let timeout: ReturnType<typeof setTimeout> | null = null;

Ba biến state quan trọng:

  • abortController: Để cancel requests đang chạy

  • result: Cache kết quả refinement (là Promise)

  • timeout: Cho debounce functionality

4. Start Function - Nơi magic xảy ra

typescript

1const start = async (data: T) => {
2  abortController = new AbortController();
3
4  

Debounce implementation:

  • Sử dụng setTimeout wrapped trong Promise

  • Await Promise này sẽ pause execution trong khoảng thời gian debounce

  • Nếu có abort signal, Promise sẽ bị cancel

AbortSignal:

  • Truyền vào callback để cho phép cancel request

  • Reset về null sau khi hoàn thành

5. Refine Function - Caching Layer

typescript

1const refine = async (data: T) => {
2  if (result != null) {
3    return result; 

Caching strategy:

  • Nếu result đã có giá trị (không null), trả về luôn

  • Nếu chưa có, gọi start() và cache Promise vào result

  • Lưu ý: Cache cả Promise, không phải resolved value

6. Utility Functions

typescript

1const abort = () => {
2  if (timeout != null) {
3    clearTimeout(timeout);
4  }
5  timeout = null;
6  abortController?.abort();
7  abortController = null;
8};
9
10refine.invalidate = () => {
11  abort();
12  result = null; 

abort(): Cleanup mọi thứ đang chạy invalidate(): Reset cache để cho phép refinement chạy lại

Những điểm hay của thiết kế

Promise Caching thay vì Value Caching

1// Không cache value

Tại sao cache Promise? Vì nếu refinement được gọi nhiều lần trong khi async operation đang chạy, tất cả sẽ nhận về cùng một Promise thay vì tạo multiple requests.

AbortController Integration

1const result = await ctxRef.current.callback(data, {
2  signal: abortController.signal,
3});

Cho phép callback cancel network requests khi cần thiết. Rất hữu ích khi user typing nhanh hoặc component unmount.

Flexible Debouncing

1if (ctxRef.current.debounce != null) {
2  await new Promise((resolve) => {
3    timeout = setTimeout(resolve, ctxRef.current.debounce);
4  });
5}

Debounce được implement ở level hook, không cần phụ thuộc external library.

Lessons Learned

  • 1.Performance optimization không chỉ là algorithm - Đôi khi là về việc tránh unnecessary work

  • 2.Caching strategies rất đa dạng - Cache Promise vs Cache Value có impact khác nhau

  • 3.User experience bị ảnh hưởng bởi technical decisions - 7 API calls vs 1 API call

  • 4.AbortController là friend - Luôn cleanup async operations

  • 5.Custom hooks giải quyết domain-specific problems - Không phải lúc nào cũng cần external library

Kết luận

useRefinement hook không chỉ giải quyết vấn đề performance mà còn cải thiện đáng kể user experience. Từ một form "lag lag" thành một form responsive và mượt mà.

Đây là một ví dụ điển hình cho việc "measure first, optimize second". Đôi khi solution không phức tạp, chỉ cần hiểu rõ vấn đề và áp dụng đúng pattern.

P/S: Junior developer trong team giờ đã thành expert về React performance optimization. Mission accomplished! 🚀