A collection of custom React hooks for common UI patterns and functionality. Provides reusable logic for state management, user interactions, and component behavior.
- Custom hooks - Reusable logic for common UI patterns
- TypeScript support - Full type safety with comprehensive definitions
- Performance optimized - Efficient implementations with proper cleanup
- Accessibility focused - Hooks that support accessibility best practices
- Framework agnostic - Works with any React setup
pnpm add @foundrykit/hooks
- useLocalStorage - Persist state in localStorage
- useDebouncedValue - Debounce values for performance
- useMediaQuery - React to media query changes
- useClickOutside - Detect clicks outside elements
- useForm - Form state management
- useField - Individual field management
- useValidation - Form validation logic
- useDialog - Dialog/modal state management
- usePopover - Popover positioning and state
- useTooltip - Tooltip positioning and visibility
- usePrevious - Access previous value
- useInterval - Set up intervals with cleanup
- useTimeout - Set up timeouts with cleanup
Persist state in localStorage with automatic serialization:
import { useLocalStorage } from '@foundrykit/hooks';
function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'en');
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value='light'>Light</option>
<option value='dark'>Dark</option>
</select>
<select value={language} onChange={e => setLanguage(e.target.value)}>
<option value='en'>English</option>
<option value='es'>Spanish</option>
</select>
</div>
);
}
Debounce values to improve performance:
import { useDebouncedValue } from '@foundrykit/hooks';
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 300);
// This effect only runs when debouncedQuery changes
useEffect(() => {
if (debouncedQuery) {
searchAPI(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder='Search...'
/>
);
}
React to media query changes:
import { useMediaQuery } from '@foundrykit/hooks';
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
<div>
{isMobile && <MobileLayout />}
{isTablet && <TabletLayout />}
{isDesktop && <DesktopLayout />}
</div>
);
}
Detect clicks outside an element:
import { useClickOutside } from '@foundrykit/hooks';
function Dropdown({ isOpen, onClose, children }) {
const ref = useRef(null);
useClickOutside(ref, () => {
if (isOpen) {
onClose();
}
});
if (!isOpen) return null;
return (
<div ref={ref} className='dropdown'>
{children}
</div>
);
}
Manage dialog/modal state:
import { useDialog } from '@foundrykit/hooks';
function ModalExample() {
const { isOpen, open, close, toggle } = useDialog();
return (
<div>
<button onClick={open}>Open Modal</button>
{isOpen && (
<div className='modal-overlay' onClick={close}>
<div className='modal-content' onClick={e => e.stopPropagation()}>
<h2>Modal Content</h2>
<button onClick={close}>Close</button>
</div>
</div>
)}
</div>
);
}
Complete form state management:
import { useForm } from '@foundrykit/hooks';
function ContactForm() {
const {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
} = useForm({
initialValues: {
name: '',
email: '',
message: '',
},
validate: values => {
const errors = {};
if (!values.name) errors.name = 'Name is required';
if (!values.email) errors.email = 'Email is required';
if (!values.message) errors.message = 'Message is required';
return errors;
},
onSubmit: async values => {
await submitForm(values);
},
});
return (
<form onSubmit={handleSubmit}>
<input
name='name'
value={values.name}
onChange={handleChange}
onBlur={handleBlur}
className={touched.name && errors.name ? 'error' : ''}
/>
{touched.name && errors.name && <span>{errors.name}</span>}
<input
name='email'
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
className={touched.email && errors.email ? 'error' : ''}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
<textarea
name='message'
value={values.message}
onChange={handleChange}
onBlur={handleBlur}
className={touched.message && errors.message ? 'error' : ''}
/>
{touched.message && errors.message && <span>{errors.message}</span>}
<button type='submit' disabled={isSubmitting}>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
}
Access the previous value:
import { usePrevious } from '@foundrykit/hooks';
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {previousCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Set up intervals with automatic cleanup:
import { useInterval } from '@foundrykit/hooks';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useInterval(() => setSeconds(seconds + 1), isRunning ? 1000 : null);
return (
<div>
<p>Seconds: {seconds}</p>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'Pause' : 'Start'}
</button>
</div>
);
}
Combine multiple hooks for complex functionality:
import {
useLocalStorage,
useDebouncedValue,
useMediaQuery,
} from '@foundrykit/hooks';
function useResponsiveSearch() {
const [query, setQuery] = useLocalStorage('search-query', '');
const debouncedQuery = useDebouncedValue(query, 300);
const isMobile = useMediaQuery('(max-width: 768px)');
const searchResults = useMemo(() => {
if (!debouncedQuery) return [];
return performSearch(debouncedQuery, { limit: isMobile ? 5 : 10 });
}, [debouncedQuery, isMobile]);
return {
query,
setQuery,
debouncedQuery,
searchResults,
isMobile,
};
}
import { useClickOutside } from '@foundrykit/hooks';
function useDropdown(initialState = false) {
const [isOpen, setIsOpen] = useState(initialState);
const ref = useRef(null);
useClickOutside(ref, () => setIsOpen(false));
const open = useCallback(() => setIsOpen(true), []);
const close = useCallback(() => setIsOpen(false), []);
const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
return {
isOpen,
ref,
open,
close,
toggle,
};
}
import { useDebouncedValue } from '@foundrykit/hooks';
function OptimizedSearch() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 500);
// Memoize expensive operation
const searchResults = useMemo(() => {
if (!debouncedQuery) return [];
return expensiveSearch(debouncedQuery);
}, [debouncedQuery]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder='Search...'
/>
<SearchResults results={searchResults} />
</div>
);
}
All hooks include comprehensive TypeScript definitions:
import { useLocalStorage, useDebouncedValue } from '@foundrykit/hooks';
// Type-safe localStorage hook
const [user, setUser] = useLocalStorage<User>('user', null);
// Type-safe debounced value
const [searchTerm, setSearchTerm] = useState<string>('');
const debouncedSearchTerm = useDebouncedValue<string>(searchTerm, 300);
// ✅ Good - Stable callback
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// ❌ Avoid - Unstable callback
const handleClick = () => {
console.log('clicked');
};
// ✅ Good - Proper cleanup
useEffect(() => {
const timer = setTimeout(() => {
// do something
}, 1000);
return () => clearTimeout(timer);
}, []);
// ❌ Avoid - Missing cleanup
useEffect(() => {
setTimeout(() => {
// do something
}, 1000);
}, []);
// ✅ Good - Memoized expensive operations
const expensiveValue = useMemo(() => {
return computeExpensiveValue(data);
}, [data]);
// ❌ Avoid - Recomputing on every render
const expensiveValue = computeExpensiveValue(data);
When adding new hooks:
- Follow React hooks best practices
- Include comprehensive TypeScript definitions
- Add JSDoc documentation
- Write unit tests
- Include usage examples
- Update this README
MIT