A collection of well-tested, performance-optimized React hooks for common web application patterns. These hooks help manage state, UI interactions, and browser features with clean, reusable abstractions.
# Using npm
npm install use-good-hooks
# Using yarn
yarn add use-good-hooks
# Using pnpm
pnpm add use-good-hooks
- React 19.0.0+
- Lodash 4.17.21+
Debounces value changes to prevent rapid updates. Useful for search inputs, form validation, and other scenarios where you want to delay state updates until after a user has stopped changing the input.
import useDebounce from 'use-good-hooks/use-debounce';
const SearchComponent = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500); // 500ms delay
// API call will only happen 500ms after the user stops typing
useEffect(() => {
if (debouncedSearchTerm) {
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
};
-
value
: The value to debounce -
delay
: (Optional) The delay in milliseconds (default: 300ms)
- The debounced value
Creates a debounced version of a function. This hook ensures that a function is only executed after a specified period of inactivity, preventing it from being called too frequently. It's ideal for handling events like button clicks or API triggers that should not fire on every user action.
import useDebounceFn from 'use-good-hooks/use-debounce-fn';
const SaveButton = () => {
const [status, setStatus] = useState('Idle');
const debouncedSave = useDebounceFn(() => {
setStatus('Saving...');
// Simulate API call
setTimeout(() => setStatus('Saved!'), 1000);
}, 1000); // 1000ms delay
const handleClick = () => {
setStatus('Waiting...');
debouncedSave();
};
return (
<div>
<button onClick={handleClick}>Save Changes</button>
<p>Status: {status}</p>
</div>
);
};
-
fn
: The function to debounce -
delay
: (Optional) The delay in milliseconds (default: 300ms)
- The debounced function.
Limits the rate at which a value can update. Useful for scroll events, window resizing, and other high-frequency events.
import useThrottle from 'use-good-hooks/use-throttle';
const ScrollTracker = () => {
const [scrollY, setScrollY] = useState(0);
const throttledScrollY = useThrottle(scrollY, 200); // 200ms throttle
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>Throttled scroll position: {throttledScrollY}px</div>;
};
-
value
: The value to throttle -
delay
: (Optional) The throttle interval in milliseconds (default: 300ms)
- The throttled value
Creates a throttled version of a function, limiting its execution to at most once per specified interval. It is useful for performance-critical scenarios like handling mouse movements, scrolling, or window resizing events without overwhelming the browser.
import useThrottleFn from 'use-good-hooks/use-throttle-fn';
const MouseTracker = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const throttledMouseMove = useThrottleFn((event) => {
setPosition({ x: event.clientX, y: event.clientY });
}, 300); // Update at most every 300ms
useEffect(() => {
window.addEventListener('mousemove', throttledMouseMove);
return () => window.removeEventListener('mousemove', throttledMouseMove);
}, [throttledMouseMove]);
return (
<div>
Throttled mouse position: X: {position.x}, Y: {position.y}
</div>
);
};
-
fn
: The function to throttle -
delay
: (Optional) The throttle interval in milliseconds (default: 300ms)
- The throttled function.
Captures the previous value of a state or prop. Useful for comparing changes between renders.
import usePrev from 'use-good-hooks/use-prev';
const Counter = ({ count }) => {
const prevCount = usePrev(count);
return (
<div>
<p>Current count: {count}</p>
<p>Previous count: {prevCount ?? 'None'}</p>
<p>Direction: {count > prevCount ? 'Increasing' : count < prevCount ? 'Decreasing' : 'No change'}</p>
</div>
);
};
-
value
: The value to track
- The previous value (undefined on first render)
Tracks the history of a state value, providing undo and redo capabilities. This is perfect for building editors, forms, or any UI where users might want to reverse their actions.
import useHistoryState from 'use-good-hooks/use-history-state';
const TextEditor = () => {
const {
canRedo,
canUndo,
history,
redo,
setState,
state,
undo
} = useHistoryState('', { maxCapacity: 10 });
return (
<div>
<textarea
value={state}
onChange={(e) => setState(e.target.value)}
rows={4}
cols={50}
/>
<div>
<button onClick={undo} disabled={!canUndo}>Undo</button>
<button onClick={redo} disabled={!canRedo}>Redo</button>
</div>
<p>History (last {history.length} changes):</p>
<pre>{JSON.stringify(history, null, 2)}</pre>
</div>
);
};
-
initialState
: The initial state value -
options
: (Optional) Configuration options:-
debounceSettings
: Debounce settings object (default: { leading: true, trailing: false }) -
debounceTime
: Time in milliseconds to debounce the state changes (default: 0) -
immutable
: Boolean indicating if the state should be treated as immutable (default: false) -
maxCapacity
: Maximum number of history entries to keep (default: 10) -
onChange
: Function to call when the state changes -
paused
: Boolean indicating if the history is paused (default: false)
-
- Object with:
-
canRedo
: Boolean indicating if redo is possible -
canUndo
: Boolean indicating if undo is possible -
clear
: Function to clear the history -
future
: Array of future states -
past
: Array of past states -
pause
: Function to pause the history -
paused
: Boolean indicating if the history is paused -
redo
: Function to move to the next state (redo) -
set
: Function to update the state and record history -
setDirect
: Function to update the state without recording history -
state
: The current state value -
undo
: Function to move to the previous state (undo)
-
Detects distinct changes in values with support for deep comparison and custom equality checks. Useful for tracking whether complex objects have actually changed.
import useDistinct from 'use-good-hooks/use-distinct';
const UserProfileForm = ({ user }) => {
const { distinct, value, prevValue } = useDistinct(user, { deep: true });
useEffect(() => {
if (distinct) {
console.log('User data changed from:', prevValue, 'to:', value);
// Perhaps save to backend or update UI
}
}, [distinct, prevValue, value]);
return (
<div>
<h2>Editing profile for: {user.name}</h2>
{distinct && <div className="alert">Unsaved changes!</div>}
{/* Form inputs */}
</div>
);
};
-
inputValue
: The value to check for changes -
options
: (Optional) Configuration options:-
deep
: Boolean to enable deep equality comparison (default: false) -
compare
: Custom comparison function (a, b) => boolean -
debounce
: Debounce time in milliseconds (default: 0)
-
- Object with:
-
distinct
: Boolean indicating if the value changed -
prevValue
: The previous distinct value -
value
: The current value
-
Persists state to localStorage or sessionStorage with automatic serialization/deserialization.
import useStorageState from 'use-good-hooks/use-storage-state';
const ThemePreferences = () => {
const [preferences, setPreferences, { removeKey }] = useStorageState('theme-prefs', {
darkMode: false,
fontSize: 'medium',
compactView: true
});
return (
<div>
<h2>Theme Settings</h2>
<label>
<input
type="checkbox"
checked={preferences.darkMode}
onChange={() => setPreferences({...preferences, darkMode: !preferences.darkMode})}
/>
Dark Mode
</label>
{/* More settings */}
<button onClick={removeKey}>Reset to Defaults</button>
</div>
);
};
-
key
: Storage key name -
initialState
: Default state if no stored value exists -
options
: (Optional) Configuration options:-
storage
: 'local' or 'session' (default: 'local') -
debounce
: Debounce time in milliseconds (default: 500) -
onError
: Error callback function -
omitKeys
: Array of keys to omit from storage or function (value, key) => boolean -
pickKeys
: Array of keys to include in storage or function (value, key) => boolean
-
- Array with:
- State value
- State setter function
- Object with utility functions:
-
removeKey
: Function to clear the storage key
-
Synchronizes state with URL query parameters. Great for shareable UI states, filters, pagination, and search terms.
import useUrlState from 'use-good-hooks/use-url-state';
const ProductFilter = () => {
const [filters, setFilters] = useUrlState({
category: '',
minPrice: 0,
maxPrice: 1000,
sortBy: 'newest'
}, {
url: new URL(window.location.href),
kebabCase: true,
omitValues: ['', 0]
});
return (
<div>
<select
value={filters.category}
onChange={(e) => setFilters({...filters, category: e.target.value})}
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
{/* More filter controls */}
</div>
);
};
-
initialState
: Default state if URL has no parameters -
options
: Configuration options:-
url
: URL object to use (required) -
debounce
: Debounce time in milliseconds (default: 500) -
kebabCase
: Convert camelCase keys to kebab-case in URL (default: true) -
prefix
: Optional prefix for URL parameters -
onError
: Error callback function -
omitKeys
: Array of keys to omit from URL or function (value, key) => boolean -
pickKeys
: Array of keys to include in URL or function (value, key) => boolean -
omitValues
: Array of values to omit from URL or function (value, key) => boolean
-
- Array with:
- State value
- State setter function
Creates and manages global state that can be shared across components with automatic synchronization.
import { createGlobalState, useGlobalState } from 'use-good-hooks/use-global-state';
// Create a global state instance (typically in a separate file)
const counterState = createGlobalState({ count: 0 });
// Component A
const CounterDisplay = () => {
const [counter, setCounter] = useGlobalState(counterState);
return (
<div>
<p>Count: {counter.count}</p>
<button onClick={() => setCounter({ count: counter.count + 1 })}>
Increment
</button>
</div>
);
};
// Component B (in a different part of your app)
const CounterActions = () => {
const [counter, setCounter] = useGlobalState(counterState);
return (
<div>
<button onClick={() => setCounter({ count: 0 })}>
Reset Count
</button>
<button onClick={() => setCounter(prev => ({ count: prev.count + 5 }))}>
Add 5
</button>
</div>
);
};
- First, create a global state store:
// state/counter.ts
import { createGlobalState } from 'use-good-hooks/use-global-state';
export const counterState = createGlobalState({ count: 0 });
- Then use it in any component:
import { useGlobalState } from 'use-good-hooks/use-global-state';
import { counterState } from './state/counter';
const MyComponent = () => {
const [counter, setCounter] = useGlobalState(counterState);
// ...
};
-
initialState
: The initial state value
- Array with:
-
state
: The current state -
setState
: Function to update state -
store
: Object with utility methods:-
getState()
: Function to get current state -
subscribe(callback)
: Subscribe to state changes -
resetState()
: Reset to initial state
-
-
-
globalState
: The global state created withcreateGlobalState
- Array with:
-
state
: The component's local copy of the state -
setState
: Function to update global state (accepts new value or update function)
-
Creates a temporary state that resets after a specified timeout.
import useTemporaryState from 'use-good-hooks/use-temporary-state';
const [state, setState] = useTemporaryState('initial', 1000);
// State will reset to 'initial' after 1 second
-
initialState
: The initial state value -
timeout
: The timeout duration in milliseconds (default: 3000)
- Array with:
-
state
: The current state -
setState
: Function to update state
-
This library is thoroughly tested with Vitest and React Testing Library. To run the tests:
# Using npm
npm test
# Using yarn
yarn test
# Using pnpm
pnpm test
Each hook in this library is designed with performance in mind:
-
useDebounce
anduseThrottle
reduce unnecessary renders using Lodash's optimized implementations -
useDistinct
avoids reference equality problems with optional deep comparison -
useStorageState
batches storage updates to reduce expensive serialization/deserialization -
useUrlState
efficiently handles URL synchronization with debouncing -
useGlobalState
andcreateGlobalState
provide a way to share state across components with automatic synchronization -
useTemporaryState
allows for temporary state that resets after a timeout
# Install dependencies
yarn install
# Start development server
yarn dev
# Run tests
yarn test
# Build the library
yarn build
# Lint and format the code
yarn lint
This project was built with:
MIT © Felipe Rohde
Felipe Rohde
- Twitter: @felipe_rohde
- Github: @feliperohdee
- Email: feliperohdee@gmail.com