A collection of essential React hooks for building modern web applications with a focus on simplicity, performance, and developer experience. Features TypeScript support, zero dependencies, and comprehensive browser compatibility.
A lightweight TypeScript library that provides a set of commonly used React hooks to enhance your components with powerful functionality.
- 🎯 Type-safe implementations with TypeScript
- 🪶 Zero external dependencies
- ⚡️ Optimized for performance
- 🧪 Well-tested and production-ready
- 📦 Tree-shakeable exports
useBoolean: Toggle boolean states with predefined actionsuseClickOutside: Detect clicks outside a specified elementuseCopyToClipboard: Copy text to clipboard with success stateuseDebounce: Debounce rapidly changing valuesuseFocus: Track focus state of DOM elementsuseLocalStorage: Persist state in localStorage with type safetyuseMediaQuery: React to CSS media query changesuseNumber: Manage numeric values with increment, decrement, and constraintsusePasswordStrength: Calculate password strength and criteriausePrevious: Access the previous value of a state or propuseQueue: Manage a queue with enqueue, dequeue, and reset operationsuseResizeObserver: Observe element size changesuseStack: Manage a stack with push, pop, and reset operationsuseStepper: Manage a stepper with customizable steps and actionsuseVisibility: Track element visibility in the viewportuseWebSocket: Manage WebSocket connections with automatic reconnectionuseWindowSize: Track window dimensions reactively
- React 16.8+ (Hooks support)
- TypeScript 4.x+ (for type definitions)
- npm or bun package manager
Before installing, you need to configure your package manager to access the GitHub Packages registry.
First, create a GitHub Personal Access Token with the read:packages permission. And add it to your ~/.bunfig.toml or ~/.npmrc or ~/.yarnrc or ~/.pnpmrc file if you haven't already.
- Create or edit
$HOME/.bunfig.tomland add:
[install.scopes]
"@paulgeorge35" = { token = "your_github_token", url = "https://npm.pkg.github.com/" }- Install the package:
bun add @paulgeorge35/hooks@latest- Create or edit
$HOME/.npmrcand add:
//npm.pkg.github.com/:_authToken=your_github_token
- Install the package:
npm install @paulgeorge35/hooks@latest- Create or edit
$HOME/.yarnrcand add:
//npm.pkg.github.com/:_authToken=your_github_token
- Install the package:
yarn add @paulgeorge35/hooks@latest- Create or edit
$HOME/.npmrcand add:
//npm.pkg.github.com/:_authToken=your_github_token
- Install the package:
pnpm add @paulgeorge35/hooks@latestimport { useBoolean } from '@paulgeorge35/hooks';
function Component() {
const boolean = useBoolean(false);
return (
<button onClick={boolean.toggle}>
{boolean.value ? 'On' : 'Off'}
</button>
);
}import { useClickOutside } from '@paulgeorge35/hooks';
function Modal() {
const handleClickOutside = () => {
setIsOpen(false);
};
const { ref, triggerRef } = useClickOutside(handleClickOutside);
return (
<div>
<button ref={triggerRef} type="button">
Open Modal
</button>
<div ref={ref} className="modal">
Modal content that can be closed by clicking outside
</div>
</div>
);
}import { toast } from 'react-toastify';
import { useCopyToClipboard } from '@paulgeorge35/hooks';
function ShareButton() {
const { copy } = useCopyToClipboard({
onSuccess: () => toast.success('Copied to clipboard!'),
onError: (e) => toast.error(e.message)
});
return (
<button onClick={() => copy('Hello, World!')}>
Share Link
</button>
);
}import { useDebounce } from '@paulgeorge35/hooks';
function SearchComponent() {
const [search, setSearch] = useState('');
const { value, status, flush, cancel } = useDebounce<string>(search, {
delay: 500,
updateOnUnmount: true,
onUpdate: (value) => console.log('Updated value:', value)
});
return (
<div>
<input type="text" value={search} onChange={(e) => setSearch(e.target.value)} />
<button onClick={flush}>Flush</button>
<button onClick={cancel}>Cancel</button>
<p>Current value: {search}</p>
<p>Debounced value: {value}</p>
<p>Status: {status}</p>
</div>
);
}import { useFocus } from '@paulgeorge35/hooks';
function InputComponent() {
const [ref, isFocused] = useFocus({
onFocus: () => console.log('Input focused'),
onBlur: () => console.log('Input blurred')
});
return (
<div>
<div ref={ref} tabIndex={0}>
{isFocused ? 'Focused!' : 'Click to focus'}
</div>
<p>Focus status: {isFocused ? 'Focused' : 'Not focused'}</p>
<button onClick={() => ref.current?.focus()}>Focus</button>
<button onClick={() => ref.current?.blur()}>Blur</button>
</div>
);
}import { useLocalStorage } from '@paulgeorge35/hooks';
function ThemeComponent() {
const [theme, setTheme, error] = useLocalStorage('theme', 'light');
}import { useMediaQuery } from '@paulgeorge35/hooks';
function ResponsiveComponent() {
const {matches: isMobile} = useMediaQuery('(max-width: 768px)');
return isMobile ? <MobileView /> : <DesktopView />;
}import { useNumber } from '@paulgeorge35/hooks';
function Counter() {
const volume = useNumber(50, {
min: 0,
max: 100,
step: 5,
loop: true,
float: true,
precision: 1,
enableKeyboardControls: true,
onChange: (value) => updateVolume(value),
onLimit: (at) => showNotification(`Volume at ${at}`)
});
return (
<div>
<label>
Volume: {volume.value}%
<input
type="range"
value={volume.value}
onChange={(e) => volume.setValue(parseFloat(e.target.value))}
min={0}
max={100}
step={5}
/>
</label>
<div>
Delta: {volume.delta > 0 ? '+' : ''}{volume.delta}%
</div>
</div>
);
}import { usePasswordStrength } from '@paulgeorge35/hooks';
function PasswordForm() {
const [password, setPassword] = useState('');
const { strength, criteria, score } = usePasswordStrength(password, {
minLength: 10,
requireSpecialChar: true,
requireNumber: true,
requireMixedCase: true
});
const getStrengthColor = () => {
switch (strength) {
case 'weak': return 'red';
case 'medium': return 'orange';
case 'strong': return 'green';
case 'very-strong': return 'darkgreen';
default: return 'gray';
}
};
return (
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Enter password"
/>
<div style={{ color: getStrengthColor() }}>
Password Strength: {strength} ({score}%)
</div>
<div className="criteria-list">
<div>
Length (min 10): {criteria.minLength ? '✅' : '❌'}
</div>
<div>
Uppercase: {criteria.hasUpperCase ? '✅' : '❌'}
</div>
<div>
Lowercase: {criteria.hasLowerCase ? '✅' : '❌'}
</div>
<div>
Number: {criteria.hasNumber ? '✅' : '❌'}
</div>
<div>
Special Character: {criteria.hasSpecialChar ? '✅' : '❌'}
</div>
</div>
</div>
);
}import { usePrevious } from '@paulgeorge35/hooks';
function Counter() {
const [count, setCount] = useState(0);
const {previous, history, clearHistory} = usePrevious(count);
return (
<div>
<p>Current count: {count}</p>
<p>Previous count: {previous ?? 'No previous value'}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}import { useQueue } from '@paulgeorge35/hooks';
function TaskManager() {
const { queue, enqueue, dequeue, reset } = useQueue<{
id: number;
task: string;
priority: 'high' | 'medium' | 'low';
}>([]);
const addTask = () => {
enqueue({
id: Date.now(),
task: 'New Task',
priority: 'medium'
});
};
return (
<div>
<h2>Task Queue ({queue.length})</h2>
<div className="task-list">
{queue.map((item) => (
<div key={item.id} className={`task-item ${item.priority}`}>
<span>{item.task}</span>
<span className="priority">{item.priority}</span>
</div>
))}
</div>
<div className="controls">
<button onClick={addTask}>Add Task</button>
<button onClick={dequeue} disabled={queue.length === 0}>
Process Next Task
</button>
<button onClick={reset}>Clear All Tasks</button>
</div>
</div>
);
}import { useResizeObserver } from '@paulgeorge35/hooks';
function ResizableComponent() {
const { ref, dimensions, isObserving } = useResizeObserver<HTMLDivElement>({
immediate: true,
useBorderBox: true,
debounceDelay: 250,
});
return (
<div>
<div ref={ref} className="resizable-component">
Resize me!
</div>
<p>Is observing: {isObserving ? 'Yes' : 'No'}</p>
<p>Width: {dimensions.width}px</p>
<p>Height: {dimensions.height}px</p>
<p>Border Box Size: {dimensions.borderBoxSize.map((size) => `${size.inlineSize}x${size.blockSize}`).join(', ')}</p>
<p>Content Box Size: {dimensions.contentBoxSize.map((size) => `${size.inlineSize}x${size.blockSize}`).join(', ')}</p>
<p>Device Pixel Content Box Size: {dimensions.devicePixelContentBoxSize.map((size) => `${size.inlineSize}x${size.blockSize}`).join(', ')}</p>
</div>
);
}import { useStack } from '@paulgeorge35/hooks';
function StackComponent() {
const { stack, push, pop, reset } = useStack<number>({
initialStack: [1, 2, 3],
maxSize: 10,
onPush: (items) => console.log('Pushed:', items),
onPop: (items) => console.log('Popped:', items)
});
return (
<div>
<h2>Stack ({stack.length})</h2>
<div className="stack-list">
{stack.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
<button onClick={() => push(stack.length + 1)}>Push</button>
<button onClick={pop}>Pop</button>
<button onClick={reset}>Reset</button>
</div>
);
}import { useStepper } from '@paulgeorge35/hooks';
function WizardComponent() {
const {
currentStep,
totalSteps,
nextStep,
previousStep,
goToStep,
isFirstStep,
isLastStep
} = useStepper({
initialStep: 0,
totalSteps: 3
});
return (
<div>
<p>Step {currentStep + 1} of {totalSteps}</p>
{currentStep === 0 && <StepOne />}
{currentStep === 1 && <StepTwo />}
{currentStep === 2 && <StepThree />}
<div>
<button disabled={isFirstStep} onClick={previousStep}>
Previous
</button>
<button disabled={isLastStep} onClick={nextStep}>
Next
</button>
</div>
</div>
);
}import { useVisibility } from '@paulgeorge35/hooks';
function Component() {
const [isVisible, isTracking, ref] = useVisibility({
delay: 500,
},(params) => {
if(params.isVisible) {
console.log('visible');
}
});
return (
<div ref={ref}>
<p>Is visible: {isVisible ? 'Yes' : 'No'}</p>
<p>Is tracking: {isTracking ? 'Yes' : 'No'}</p>
</div>
);
}import { useWebSocket } from '@paulgeorge35/hooks';
type ChatMessage = {
text: string;
timestamp: number;
};
function ChatComponent() {
const {
send,
messages,
status,
connect,
disconnect,
clearMessages,
lastError
} = useWebSocket<ChatMessage>({
url: 'wss://chat.example.com',
options: {
autoConnect: true,
reconnect: true,
reconnectInterval: 5000,
maxRetries: 3,
onMessage: (msg) => console.log('New message:', msg),
onError: (error) => console.error('WebSocket error:', error)
}
});
return (
<div>
<p>Connection status: {status}</p>
{lastError && <p>Error: {lastError.message}</p>}
<div>
{messages.map((msg, index) => (
<div key={index}>
{msg.text} - {new Date(msg.timestamp).toLocaleString()}
</div>
))}
</div>
<button onClick={() => send({ text: 'Hello!', timestamp: Date.now() })}>
Send Message
</button>
<button onClick={disconnect}>Disconnect</button>
<button onClick={connect}>Reconnect</button>
<button onClick={clearMessages}>Clear History</button>
</div>
);
}import { useWindowSize } from '@paulgeorge35/hooks';
function Component() {
const { width, height, isLandscape, isPortrait, aspectRatio } = useWindowSize({
debounceDelay: 250,
includeOrientation: true,
});
return (
<div>
<p>Window size: {width} x {height}</p>
<p>Orientation: {isLandscape ? 'Landscape' : isPortrait ? 'Portrait' : 'Unknown'}</p>
<p>Aspect ratio: {aspectRatio ? aspectRatio.toFixed(2) : 'N/A'}</p>
</div>
);
}- Comparing previous and current values to trigger side effects
- Implementing undo functionality
- Tracking value changes in animations
- Form state management
- Returns
undefinedon the first render - Updates only after the component has rendered with a new value
- Preserves value type safety through TypeScript generics
The library is written in TypeScript and includes comprehensive type definitions. All hooks are fully typed and provide excellent IDE support.
react(>16.8.0)react-dom(>16.8.0)
This project is licensed under the MIT License - see the LICENSE file for details.
Paul George - contact@paulgeorge.dev
Project Link: https://github.com/paulgeorge35/hooks