Custom hook là một hàm có tên bắt đầu bằng use và gọi các hook khác. Bạn viết một custom hook khi hai component cần cùng một logic có state: chuyển các lệnh gọi useState và useEffect vào hàm, trả về thứ component cần, rồi gọi nó từ từng component.
Cả hai spoiler đều gọi useToggle, và mở một cái thì cái kia vẫn đóng. Mỗi lần gọi chạy useState riêng, nên mỗi component có giá trị riêng. Custom hook chia sẻ logic, không chia sẻ state.
Quy tắc khi viết custom hook
- Tên bắt đầu bằng
usevà một chữ in hoa:useToggle,useOnlineStatus. Quy tắc lint cho hook dựa vào tiền tố này để tìm các lệnh gọi hook và kiểm tra chúng. - Nó gọi ít nhất một hook. Một hàm hỗ trợ không gọi hook nào chỉ là hàm thông thường; hãy đặt tên là
formatDate, không phảiuseFormatDate, để có thể gọi nó ở bất kỳ đâu, kể cả bên trong điều kiện. - Nó tuân theo quy tắc của hook: gọi hook ở cấp cao nhất của nó, và chỉ gọi nó từ component hoặc từ custom hook khác.
- Nó trả về bất cứ thứ gì hữu ích. Mảng (
[on, toggle]) tiện khi nơi gọi muốn đổi tên các giá trị; object ({ data, loading, error }) dễ đọc hơn khi có từ ba giá trị trở lên.
useLocalStorage: state còn nguyên sau khi tải lại trang
useState quên mọi thứ khi tải lại trang. Hook này đọc giá trị ban đầu từ localStorage và ghi lại mọi thay đổi vào đó.
Phần xem trước chạy trong một frame sandbox nơi trình duyệt chặn localStorage, nên ở đây bạn sẽ thấy cảnh báo trong Console và hook quay về dùng state thông thường. Trên một trang bình thường, cái tên vẫn còn sau khi tải lại. Khối try/catch không chỉ dành cho phần xem trước này: chế độ duyệt web riêng tư, cookie bị chặn hay hết dung lượng đều khiến storage ném lỗi trong ứng dụng thật. Truyền một hàm vào useState nghĩa là storage chỉ được đọc một lần, ở lần render đầu tiên.
useDebounce: chờ tới khi người dùng ngừng gõ
Một ô tìm kiếm không nên gửi request sau mỗi lần gõ phím. useDebounce trả về một bản sao của giá trị, bản sao này chỉ cập nhật sau khi giá trị ngừng thay đổi trong một khoảng thời gian định sẵn.
Gõ nhanh một từ: Typed bám theo từng phím, Debounced bắt kịp nửa giây sau khi bạn dừng, và Console ghi một lần tìm kiếm. Mỗi lần gõ phím làm value thay đổi, nên React chạy cleanup (clearTimeout) trước khi bắt đầu một timer mới. Đổi 500 thành 0, mỗi lần gõ phím sẽ ghi một lần tìm kiếm riêng.
useWindowWidth: subscribe kèm cleanup
Một hook lắng nghe trình duyệt phải gỡ listener của nó khi component biến mất, nếu không mỗi lần mount sẽ để lại một listener.
Thay đổi kích thước cửa sổ trình duyệt, con số sẽ thay đổi theo. Bấm nút, Console cho thấy listener đang được gỡ bỏ. Xóa return khỏi effect, rồi mount và unmount vài lần: mỗi lần bạn nhận được một dòng added mà không có removed, nên các listener cũ chồng chất lên nhau. Với những giá trị của trình duyệt như thế này, useSyncExternalStore là hook React tạo ra đúng cho việc đó, và rất nên gọi nó bên trong một custom hook.
Chia sẻ logic, không chia sẻ state
Hai component gọi cùng một hook sẽ nhận hai bản sao riêng biệt của mọi thứ bên trong nó. Điều này khiến những ai nghĩ hook hoạt động như một biến toàn cục bất ngờ.
Bấm bất kỳ nút nào, chỉ bộ đếm đó thay đổi. Nếu cả hai cần hiển thị cùng một con số, hãy chuyển state vào App rồi truyền xuống, hoặc đặt nó trong context. Một custom hook có thể bọc state dùng chung đó cho tiện (useCart() đọc một CartContext), nhưng việc chia sẻ đến từ context, không phải từ hook.
Khi nào nên viết custom hook
Hãy tách ra một hook khi cùng một nhóm lệnh gọi hook xuất hiện ở hai component, hoặc khi logic effect của một component dài tới mức một cái tên sẽ giải thích nó tốt hơn (useOnlineStatus() dễ đọc hơn hai mươi dòng listener). Đừng bọc một lệnh useState duy nhất trong hook chỉ để đổi tên nó, và đừng viết những hook chỉ gọi useEffect để đặt state từ props; hãy tính giá trị đó ngay trong lúc render.
Câu hỏi thường gặp
Custom hook trong React là gì?
Là một hàm JavaScript có tên bắt đầu bằng use và gọi một hoặc nhiều hook khác. Nó đóng gói logic có state (state, effect, ref) để bất kỳ component nào cũng dùng lại được bằng cách gọi hàm đó.
Custom hook có chia sẻ state giữa các component không?
Không. Mỗi lần gọi một custom hook sẽ chạy các lệnh useState và useEffect riêng của nó, nên mỗi component có state độc lập. Để chia sẻ cùng một giá trị giữa các component, hãy đưa state lên component cha, hoặc đặt nó trong context hay một store bên ngoài.
Vì sao tên custom hook phải bắt đầu bằng use?
Cái tên báo cho các quy tắc lint của React và cho các lập trình viên khác biết hàm này gọi hook, nên các quy tắc của hook áp dụng cho nó: gọi ở cấp cao nhất, không bao giờ gọi bên trong điều kiện hay vòng lặp. Thiếu tiền tố này, linter không thể kiểm tra các lệnh gọi bên trong.
Custom hook có thể trả về JSX không?
Nó có thể trả về bất cứ thứ gì, nhưng nếu trả về JSX thì thường nên viết thành một component. Custom hook thường trả về giá trị và hàm, dưới dạng mảng ([value, toggle]) hoặc object ({ data, error, loading }).
Có thể gọi custom hook bên trong useEffect hoặc trình xử lý sự kiện không?
Không. Custom hook vẫn là một hook, nên nó tuân theo các quy tắc của hook: hãy gọi nó ở cấp cao nhất của một component hoặc của một custom hook khác.