Skip to main content

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 debounce
  • delay (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 element
  • handler (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'
/>
);
};
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

  1. Choose appropriate delay: 300-500ms for search, 100-200ms for validation
  2. Clean up: Hook automatically handles cleanup
  3. Use for expensive operations: API calls, complex calculations
  4. Consider user experience: Don't make delays too long

Outside Click Hook

  1. Ref management: Ensure refs are properly attached
  2. Event handling: Consider stopping propagation where needed
  3. Accessibility: Provide alternative ways to close (ESC key)
  4. 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,
};
};


TypeScript Definitions

export function useDebounce&lt;T&gt;(value: T, delay?: number): T;
export function useOnClickOutside(
ref: React.RefObject&lt;HTMLElement&gt;,
handler: (event: Event) => void,
): void;

Dependencies

These hooks use standard React hooks and browser APIs:

import React from 'react';
// Uses: useState, useEffect, useRef (implicitly)