Hooks
Custom React hooks for common functionality patterns in the WorkPayCore frontend application.
Performance Hooks
useDebounce(value, delay?)
Debounces a value to prevent excessive updates, useful for search inputs and API calls.
Parameters:
value(any): The value to debouncedelay(number, optional): Debounce delay in milliseconds (default: 500)
Returns:
any: The debounced value
Example:
import { useDebounce } from '@/utils/hooks';
const SearchComponent = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
useEffect(() => {
if (debouncedSearchTerm) {
// This will only run 300ms after the user stops typing
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type='text'
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder='Search users...'
/>
);
};
Use Cases:
- Search input optimization
- API call throttling
- Preventing excessive re-renders
- Form validation delays
UI Interaction Hooks
useOnClickOutside(ref, handler)
Detects clicks outside a specified element, commonly used for modals and dropdowns.
Parameters:
ref(React.RefObject): React ref pointing to the elementhandler(function): Callback function to execute when clicking outside
Returns:
void: No return value
Example:
import { useOnClickOutside } from '@/utils/hooks';
const DropdownMenu = ({ isOpen, onClose }) => {
const dropdownRef = useRef(null);
useOnClickOutside(dropdownRef, () => {
if (isOpen) {
onClose();
}
});
if (!isOpen) return null;
return (
<div ref={dropdownRef} className='dropdown-menu'>
<ul>
<li>Menu Item 1</li>
<li>Menu Item 2</li>
<li>Menu Item 3</li>
</ul>
</div>
);
};
Use Cases:
- Modal/dialog auto-closing
- Dropdown menu interactions
- Popover components
- Context menu handling
Advanced Usage Examples
Search with Debounce
import { useDebounce } from '@/utils/hooks';
const UserSearch = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const debouncedQuery = useDebounce(query, 500);
useEffect(() => {
const searchUsers = async () => {
if (!debouncedQuery.trim()) {
setResults([]);
return;
}
setLoading(true);
try {
const response = await fetch(`/api/users/search?q=${debouncedQuery}`);
const users = await response.json();
setResults(users);
} catch (error) {
console.error('Search failed:', error);
setResults([]);
} finally {
setLoading(false);
}
};
searchUsers();
}, [debouncedQuery]);
return (
<div className='user-search'>
<input
type='text'
value={query}
onChange={e => setQuery(e.target.value)}
placeholder='Search users...'
/>
{loading && <div>Searching...</div>}
<ul className='search-results'>
{results.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
};
Complex Dropdown with Outside Click
import { useOnClickOutside } from '@/utils/hooks';
const AdvancedDropdown = ({ trigger, children, onStateChange }) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
const triggerRef = useRef(null);
const closeDropdown = useCallback(() => {
setIsOpen(false);
onStateChange?.(false);
}, [onStateChange]);
const toggleDropdown = useCallback(() => {
const newState = !isOpen;
setIsOpen(newState);
onStateChange?.(newState);
}, [isOpen, onStateChange]);
useOnClickOutside(dropdownRef, event => {
// Don't close if clicking the trigger
if (triggerRef.current?.contains(event.target)) {
return;
}
closeDropdown();
});
// Close on escape key
useEffect(() => {
const handleEscape = event => {
if (event.key === 'Escape' && isOpen) {
closeDropdown();
}
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen, closeDropdown]);
return (
<div className='advanced-dropdown'>
<div
ref={triggerRef}
onClick={toggleDropdown}
className='dropdown-trigger'
>
{trigger}
</div>
{isOpen && (
<div ref={dropdownRef} className='dropdown-content'>
{children}
</div>
)}
</div>
);
};
Form Input with Debounced Validation
import { useDebounce } from '@/utils/hooks';
const ValidatedInput = ({ value, onChange, validate, ...props }) => {
const [error, setError] = useState('');
const debouncedValue = useDebounce(value, 300);
useEffect(() => {
const validateField = async () => {
if (!debouncedValue) {
setError('');
return;
}
try {
await validate(debouncedValue);
setError('');
} catch (validationError) {
setError(validationError.message);
}
};
validateField();
}, [debouncedValue, validate]);
return (
<div className='validated-input'>
<input
{...props}
value={value}
onChange={onChange}
className={error ? 'error' : ''}
/>
{error && <span className='error-message'>{error}</span>}
</div>
);
};
// Usage
const EmailInput = ({ email, setEmail }) => {
const validateEmail = async value => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new Error('Invalid email format');
}
// Optional: Check if email exists
const response = await fetch(`/api/check-email?email=${value}`);
if (!response.ok) {
throw new Error('Email already exists');
}
};
return (
<ValidatedInput
type='email'
value={email}
onChange={e => setEmail(e.target.value)}
validate={validateEmail}
placeholder='Enter your email'
/>
);
};
Modal with Outside Click
import { useOnClickOutside } from '@/utils/hooks';
const Modal = ({ isOpen, onClose, children }) => {
const modalRef = useRef(null);
useOnClickOutside(modalRef, onClose);
if (!isOpen) return null;
return (
<div className='modal-overlay'>
<div
ref={modalRef}
className='modal-content'
onClick={e => e.stopPropagation()}
>
<button
className='modal-close'
onClick={onClose}
aria-label='Close modal'
>
×
</button>
{children}
</div>
</div>
);
};
Performance Considerations
useDebounce
- Memory: O(1) - maintains single timeout reference
- Performance: Prevents excessive function calls
- Cleanup: Automatically clears timeouts on unmount
useOnClickOutside
- Memory: O(1) - single event listener
- Performance: Minimal overhead with proper cleanup
- Event delegation: Uses document-level listeners
Best Practices
Debounce Hook
- Choose appropriate delay: 300-500ms for search, 100-200ms for validation
- Clean up: Hook automatically handles cleanup
- Use for expensive operations: API calls, complex calculations
- Consider user experience: Don't make delays too long
Outside Click Hook
- Ref management: Ensure refs are properly attached
- Event handling: Consider stopping propagation where needed
- Accessibility: Provide alternative ways to close (ESC key)
- Conditional logic: Check element existence before accessing
Common Patterns
Combined Hooks
const useDropdown = () => {
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState('');
const dropdownRef = useRef(null);
const debouncedQuery = useDebounce(query, 200);
useOnClickOutside(dropdownRef, () => setIsOpen(false));
return {
isOpen,
setIsOpen,
query,
setQuery,
debouncedQuery,
dropdownRef,
};
};
Hook Composition
const useSearchableDropdown = (searchFn, options = {}) => {
const { delay = 300 } = options;
const dropdown = useDropdown();
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const search = async () => {
if (!dropdown.debouncedQuery) {
setResults([]);
return;
}
setLoading(true);
try {
const data = await searchFn(dropdown.debouncedQuery);
setResults(data);
} finally {
setLoading(false);
}
};
search();
}, [dropdown.debouncedQuery, searchFn]);
return {
...dropdown,
results,
loading,
};
};
Related Utilities
- Browser Utils - For environment-specific hook behavior
- Object & Array Utilities - For processing hook data
TypeScript Definitions
export function useDebounce<T>(value: T, delay?: number): T;
export function useOnClickOutside(
ref: React.RefObject<HTMLElement>,
handler: (event: Event) => void,
): void;
Dependencies
These hooks use standard React hooks and browser APIs:
import React from 'react';
// Uses: useState, useEffect, useRef (implicitly)