Ngày xưa: Bootstrap (Cung cấp sẵn UI + Logic). -> Khó sửa UI.
Ngày nay: Headless UI (Chỉ cung cấp Logic). -> UI tự do 100%.
1. Render Props (Function as Child)
Thay vì render JSX cụ thể, component sẽ gọi một hàm prop trả về JSX.
tsx:
// Logic di chuyển chuột
function MouseTracker({ render }) {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handler = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handler);
return () => window.removeEventListener('mousemove', handler);
}, []);
return render(pos); // 🔥 Trao quyền hiển thị lại cho cha
}
// Usage
<MouseTracker
render={({ x, y }) => (
<h1>Mouse is at ({x}, {y})</h1>
)}
/>2. Custom Hooks (Modern Headless)
Render Props hơi khó đọc (Callback Hell). Custom Hook là bản nâng cấp.
tsx:
function useMouse() {
const [pos, setPos] = useState({ x: 0, y: 0 });
// logic...
return pos;
}
// Usage
function App() {
const { x, y } = useMouse();
return <h1>({x}, {y})</h1>;
}3. Prop Getters (Pattern siêu việt)
Ví dụ useTable của TanStack Table. Hook trả về không chỉ data (state) mà trả về cả hàm gắn props (getTableProps).
tsx:
function useToggle() {
const [on, setOn] = useState(false);
const getButtonProps = () => ({
onClick: () => setOn(!on),
"aria-pressed": on,
role: "button"
});
return { on, getButtonProps };
}
// Usage
function App() {
const { on, getButtonProps } = useToggle();
return (
// Spread props vào button: Tự động có onClick và aria
<button {...getButtonProps()}>
{on ? "ON" : "OFF"}
</button>
);
}Kết luận
Khi xây dựng thư viện nội bộ (Design System):
- Nếu cần tái sử dụng Logic nhưng UI thay đổi liên tục -> Dùng Custom Hooks.
- Nếu cần tái sử dụng Cấu trúc DOM nhưng content thay đổi -> Dùng Composition.
- Nếu cần sự linh hoạt tối đa cho người dùng -> Headless (Prop Getters).