Hooks for creating stable event callbacks and managing DOM event listeners with automatic cleanup.
Modified useCallback that returns the same function reference every time, but internally calls the most-recently passed callback implementation. This solves common issues with event handler dependencies that change too frequently.
/**
* Modified useCallback that returns the same function reference every time, but internally calls
* the most-recently passed callback implementation. Can be useful in situations such as:
* - Event handler dependencies change too frequently, such as user props which might change on every render
* - Callback must be referenced in a captured context but needs access to the latest props
*
* In general, prefer useCallback unless you've encountered one of the problems above.
* @param fn - The callback function that will be used
* @returns A function which is referentially stable but internally calls the most recently passed callback
*/
function useEventCallback<Args extends unknown[], Return>(
fn: (...args: Args) => Return
): (...args: Args) => Return;Usage Examples:
import { useEventCallback } from "@fluentui/react-hooks";
function SearchComponent({ onSearch, searchTerm, filters }) {
// ❌ Problem: useCallback depends on frequently changing values
const handleSearch = useCallback(() => {
onSearch(searchTerm, filters);
}, [onSearch, searchTerm, filters]); // Dependencies change on every render
// ✅ Solution: useEventCallback always has stable reference
const handleSearch = useEventCallback(() => {
onSearch(searchTerm, filters); // Always uses latest values
});
return <button onClick={handleSearch}>Search</button>;
}
// Perfect for window event listeners
function WindowEventComponent() {
const [windowSize, setWindowSize] = useState({ width: 0, height: 0 });
const handleResize = useEventCallback(() => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
});
useEffect(() => {
// Callback reference never changes, so this effect only runs once
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [handleResize]);
return <div>Window size: {windowSize.width} x {windowSize.height}</div>;
}Hook to attach an event handler on mount and handle cleanup automatically. Provides a declarative way to manage DOM event listeners.
/**
* Hook to attach an event handler on mount and handle cleanup.
* @param element - Element (or ref to an element) to attach the event handler to
* @param eventName - The event to attach a handler for
* @param callback - The handler for the event
* @param useCapture - Whether or not to attach the handler for the capture phase
*/
function useOnEvent<TElement extends Element, TEvent extends Event>(
element: React.RefObject<TElement | undefined | null> | TElement | Window | Document | undefined | null,
eventName: string,
callback: (ev: TEvent) => void,
useCapture?: boolean
): void;Usage Examples:
import { useOnEvent } from "@fluentui/react-hooks";
function ClickOutsideComponent() {
const containerRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
// Close dropdown when clicking outside
useOnEvent(
document,
'click',
(event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
);
return (
<div ref={containerRef}>
<button onClick={() => setIsOpen(true)}>Open Dropdown</button>
{isOpen && (
<div className="dropdown">
<div>Dropdown content</div>
</div>
)}
</div>
);
}
// Keyboard event handling
function KeyboardNavigationComponent() {
const [currentIndex, setCurrentIndex] = useState(0);
const items = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
useOnEvent(
window,
'keydown',
(event: KeyboardEvent) => {
switch (event.key) {
case 'ArrowUp':
event.preventDefault();
setCurrentIndex(prev => Math.max(0, prev - 1));
break;
case 'ArrowDown':
event.preventDefault();
setCurrentIndex(prev => Math.min(items.length - 1, prev + 1));
break;
}
}
);
return (
<ul>
{items.map((item, index) => (
<li
key={index}
style={{
backgroundColor: index === currentIndex ? '#blue' : 'transparent'
}}
>
{item}
</li>
))}
</ul>
);
}
// Event handling on specific element refs
function ElementEventComponent() {
const buttonRef = useRef<HTMLButtonElement>(null);
const [clickCount, setClickCount] = useState(0);
// Handle events directly on the button element
useOnEvent(
buttonRef,
'click',
(event: MouseEvent) => {
setClickCount(prev => prev + 1);
console.log('Button clicked!', event);
}
);
// Handle mouse enter/leave
useOnEvent(
buttonRef,
'mouseenter',
() => console.log('Mouse entered button')
);
useOnEvent(
buttonRef,
'mouseleave',
() => console.log('Mouse left button')
);
return (
<div>
<button ref={buttonRef}>
Click me! (Clicked {clickCount} times)
</button>
</div>
);
}