@upendra.manike/react-utils
v0.1.4
Published
React hooks library solving common React problems: debounce input, throttle scroll, form validation, async data fetching, virtual scrolling, error boundaries, memory leaks, stale closures, infinite loops, performance optimization. Production-ready React h
Maintainers
Keywords
Readme
@upendra.manike/react-utils
React utilities for common problems: hooks, state management, performance optimization, forms, and more.
A comprehensive collection of React hooks and utilities that solve common React development problems, from performance optimization to state management, forms, and error handling.
✨ Features
- 🚀 Performance Hooks - Optimize renders with debounce, throttle, and stable callbacks
- 📊 State Management - Safe state updates, deep comparison effects, previous value tracking
- 📝 Form Utilities - Complete form state management with validation
- 🔄 Async Handling - Simplified async operations with loading/error states
- 📜 Virtual Lists - Efficient rendering of large lists
- 🛡️ Error Handling - Error boundary hooks for graceful error management
- 🪟 UI Utilities - Window size tracking and responsive helpers
- 📦 TypeScript - Full TypeScript support with type definitions
- 🎯 Tree-shakeable - Import only what you need
📦 Install
npm install @upendra.manike/react-utils
# or
yarn add @upendra.manike/react-utils
# or
pnpm add @upendra.manike/react-utils📚 API Reference
Performance Hooks
useStableCallback
Returns a stable callback reference that doesn't change between renders, preventing unnecessary re-renders of child components.
Problem Solved: Inline functions in props causing child components to re-render unnecessarily.
import { useStableCallback } from '@upendra.manike/react-utils';
function ParentComponent() {
const [count, setCount] = useState(0);
// This callback reference stays stable across renders
const handleClick = useStableCallback(() => {
setCount(c => c + 1);
});
// Child won't re-render unless count changes
return <ExpensiveChild onClick={handleClick} count={count} />;
}API:
function useStableCallback<T extends (...args: any[]) => any>(
callback: T
): TuseDebounce
Debounces a value, updating only after the specified delay has passed since the last change.
Problem Solved: Debouncing or throttling input within React causing stale state or excessive renders.
import { useDebounce } from '@upendra.manike/react-utils';
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
// API call only happens 300ms after user stops typing
fetchResults(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}API:
function useDebounce<T>(value: T, delay: number): TParameters:
value- The value to debouncedelay- Delay in milliseconds
Returns: Debounced value
useThrottle
Throttles a value, updating at most once per delay period.
Problem Solved: Throttling causing stale state issues.
import { useThrottle } from '@upendra.manike/react-utils';
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const throttledScroll = useThrottle(scrollY, 100);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// throttledScroll updates max once per 100ms
return <div>Scroll: {throttledScroll}px</div>;
}API:
function useThrottle<T>(value: T, delay: number): TParameters:
value- The value to throttledelay- Throttle delay in milliseconds
Returns: Throttled value
State Management Hooks
usePrevious
Returns the previous value of a prop or state, useful for comparisons and detecting changes.
Problem Solved: setState isn't applied immediately — subsequent code sees stale value.
import { usePrevious } from '@upendra.manike/react-utils';
function Counter({ value }) {
const prevValue = usePrevious(value);
const [direction, setDirection] = useState<'up' | 'down' | null>(null);
useEffect(() => {
if (prevValue !== undefined) {
setDirection(value > prevValue ? 'up' : 'down');
}
}, [value, prevValue]);
return (
<div>
<span>{value}</span>
{direction && <span> ({direction})</span>}
</div>
);
}API:
function usePrevious<T>(value: T): T | undefinedReturns: Previous value or undefined on first render
useSafeState
Safe state that prevents updates after component unmount, avoiding memory leaks and warnings.
Problem Solved: State updates and race conditions when component unmounts during async operations.
import { useSafeState } from '@upendra.manike/react-utils';
function AsyncDataComponent() {
const [data, setData] = useSafeState(null);
const [loading, setLoading] = useSafeState(false);
useEffect(() => {
setLoading(true);
fetchData()
.then(result => {
setData(result); // Safe even if component unmounted
setLoading(false);
})
.catch(() => {
setLoading(false); // No warnings if unmounted
});
}, []);
if (loading) return <div>Loading...</div>;
return <div>{JSON.stringify(data)}</div>;
}API:
function useSafeState<T>(
initialState: T | (() => T)
): [T, (value: T | ((prev: T) => T)) => void]Returns: [state, setState] tuple (same API as useState)
useDeepCompareEffect
useEffect with deep comparison of dependencies, preventing unnecessary effect runs and infinite loops.
Problem Solved:
- State updates not triggering re-render when using nested objects/arrays (reference unchanged)
- Infinite render loops due to incorrect useEffect dependencies
import { useDeepCompareEffect } from '@upendra.manike/react-utils';
function ConfigComponent({ config }) {
// This only runs when config deeply changes, not on reference change
useDeepCompareEffect(() => {
console.log('Config changed:', config);
// Expensive operation based on config
}, [config]);
return <div>Config: {JSON.stringify(config)}</div>;
}
// Usage:
<ConfigComponent config={{ a: 1, b: 2 }} />
<ConfigComponent config={{ a: 1, b: 2 }} /> // Effect won't run (deeply equal)
<ConfigComponent config={{ a: 1, b: 3 }} /> // Effect will run (deeply different)API:
function useDeepCompareEffect(
effect: EffectCallback,
deps: DependencyList
): voidNote: Use sparingly - deep comparison has performance cost. Prefer restructuring data when possible.
Form Utilities
useForm
Complete form state management with validation, error handling, and submission.
Problem Solved: React forms validation logic becoming messy (especially with TypeScript).
import { useForm } from '@upendra.manike/react-utils';
interface FormValues {
email: string;
password: string;
age: number;
}
function LoginForm() {
const { values, errors, touched, isSubmitting, setValue, setFieldTouched, handleSubmit, reset } = useForm<FormValues>({
initialValues: {
email: '',
password: '',
age: 0,
},
validate: (vals) => {
const errs: Record<string, string> = {};
if (!vals.email) {
errs.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(vals.email)) {
errs.email = 'Email is invalid';
}
if (!vals.password) {
errs.password = 'Password is required';
} else if (vals.password.length < 8) {
errs.password = 'Password must be at least 8 characters';
}
if (vals.age < 18) {
errs.age = 'Must be 18 or older';
}
return errs;
},
onSubmit: async (vals) => {
await loginUser(vals);
console.log('Logged in:', vals.email);
},
});
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={values.email}
onChange={(e) => setValue('email', e.target.value)}
onBlur={() => setFieldTouched('email')}
placeholder="Email"
/>
{touched.email && errors.email && (
<span className="error">{errors.email}</span>
)}
</div>
<div>
<input
type="password"
value={values.password}
onChange={(e) => setValue('password', e.target.value)}
onBlur={() => setFieldTouched('password')}
placeholder="Password"
/>
{touched.password && errors.password && (
<span className="error">{errors.password}</span>
)}
</div>
<div>
<input
type="number"
value={values.age}
onChange={(e) => setValue('age', parseInt(e.target.value) || 0)}
onBlur={() => setFieldTouched('age')}
placeholder="Age"
/>
{touched.age && errors.age && (
<span className="error">{errors.age}</span>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
<button type="button" onClick={reset}>
Reset
</button>
</form>
);
}API:
interface UseFormOptions<T> {
initialValues: T;
validate?: (values: T) => FormErrors;
onSubmit: (values: T) => void | Promise<void>;
}
interface FormErrors {
[key: string]: string | undefined;
}
function useForm<T extends Record<string, any>>(
options: UseFormOptions<T>
): {
values: T;
errors: FormErrors;
touched: Record<string, boolean>;
isSubmitting: boolean;
setValue: (name: keyof T, value: any) => void;
setFieldTouched: (name: keyof T) => void;
handleSubmit: (e?: React.FormEvent) => Promise<void>;
reset: () => void;
}Returns:
values- Current form valueserrors- Validation errorstouched- Field touched stateisSubmitting- Submission statesetValue- Update field valuesetFieldTouched- Mark field as touchedhandleSubmit- Form submission handlerreset- Reset form to initial values
Async & Data Hooks
useAsync
Handles async operations with loading and error states, preventing race conditions.
Problem Solved:
- Large data fetches in React cause waterfall API calls instead of parallel/batched
- React state race conditions: two async calls setting state confusingly
import { useAsync } from '@upendra.manike/react-utils';
function UserProfile({ userId }) {
const { data, loading, error, execute, reset } = useAsync(
() => fetchUser(userId),
true // Execute immediately
);
if (loading) return <div>Loading user...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return null;
return (
<div>
<h1>{data.name}</h1>
<p>{data.email}</p>
<button onClick={execute}>Refresh</button>
<button onClick={reset}>Clear</button>
</div>
);
}
// Manual execution
function ManualFetch() {
const { data, loading, error, execute } = useAsync(
() => fetchData(),
false // Don't execute immediately
);
return (
<div>
<button onClick={execute} disabled={loading}>
{loading ? 'Loading...' : 'Fetch Data'}
</button>
{error && <div>Error: {error.message}</div>}
{data && <div>{JSON.stringify(data)}</div>}
</div>
);
}API:
interface UseAsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate?: boolean
): UseAsyncState<T> & {
execute: () => Promise<void>;
reset: () => void;
}Parameters:
asyncFunction- Async function to executeimmediate- Execute immediately on mount (default:true)
Returns:
data- Result data ornullloading- Loading stateerror- Error object ornullexecute- Manually trigger executionreset- Reset state
Performance & Rendering Hooks
useVirtualList
Efficiently renders large lists by only rendering visible items (virtual scrolling).
Problem Solved: Large lists cause UI performance issues (no virtualization/windowing).
import { useVirtualList } from '@upendra.manike/react-utils';
function LargeList({ items }) {
const containerRef = useRef<HTMLDivElement>(null);
const { virtualItems, totalHeight, scrollToIndex } = useVirtualList(items, {
itemHeight: 50, // Fixed height
containerHeight: 400,
overscan: 5, // Render 5 extra items above/below viewport
});
return (
<div
ref={containerRef}
style={{ height: 400, overflow: 'auto' }}
onScroll={(e) => {
// Handle scroll for dynamic offset
}}
>
<div style={{ height: totalHeight, position: 'relative' }}>
{virtualItems.map((virtualItem) => {
const item = items[virtualItem.index];
return (
<div
key={virtualItem.index}
style={{
position: 'absolute',
top: virtualItem.start,
height: virtualItem.size,
width: '100%',
}}
>
{item.name}
</div>
);
})}
</div>
</div>
);
}
// Dynamic heights
function DynamicHeightList({ items }) {
const { virtualItems, totalHeight } = useVirtualList(items, {
itemHeight: (index) => items[index].height || 50, // Dynamic height
containerHeight: 400,
});
// ... similar implementation
}API:
interface UseVirtualListOptions {
itemHeight: number | ((index: number) => number);
overscan?: number;
containerHeight: number;
}
interface VirtualItem {
index: number;
start: number;
end: number;
size: number;
}
function useVirtualList<T>(
items: T[],
options: UseVirtualListOptions
): {
virtualItems: VirtualItem[];
totalHeight: number;
scrollToIndex: (index: number) => void;
scrollOffset: number;
setScrollOffset: (offset: number) => void;
}Parameters:
items- Array of items to renderoptions.itemHeight- Fixed height or function returning height per indexoptions.overscan- Number of items to render outside viewport (default: 5)options.containerHeight- Container viewport height
Returns:
virtualItems- Array of visible virtual items with positioningtotalHeight- Total height of all itemsscrollToIndex- Scroll to specific item indexscrollOffset- Current scroll offsetsetScrollOffset- Set scroll offset
Error Handling Hooks
useErrorBoundary
Hook for error boundary functionality, capturing errors and providing recovery.
Problem Solved: React error boundaries missing or misused, resulting in UI crash rather than graceful fallback.
import { useErrorBoundary } from '@upendra.manike/react-utils';
function App() {
const { error, hasError, resetError, captureError } = useErrorBoundary();
if (hasError) {
return (
<div>
<h2>Something went wrong</h2>
<p>{error?.message}</p>
<button onClick={resetError}>Try Again</button>
</div>
);
}
return <MainContent onError={captureError} />;
}
function MainContent({ onError }) {
const handleAsyncOperation = async () => {
try {
await riskyOperation();
} catch (err) {
onError(err instanceof Error ? err : new Error(String(err)));
}
};
return <button onClick={handleAsyncOperation}>Do Something</button>;
}API:
interface ErrorBoundaryState {
error: Error | null;
hasError: boolean;
}
function useErrorBoundary(): ErrorBoundaryState & {
resetError: () => void;
captureError: (error: Error) => void;
}Returns:
error- Current error ornullhasError- Boolean indicating if error existsresetError- Clear error statecaptureError- Manually capture an error
Note: Also automatically captures unhandled errors and promise rejections.
UI Utilities
useWindowSize
Tracks window/viewport size with automatic updates on resize.
Problem Solved: Window/viewport size differences across browsers causing inconsistent UI in React.
import { useWindowSize } from '@upendra.manike/react-utils';
function ResponsiveComponent() {
const { width, height } = useWindowSize();
const isMobile = width < 768;
const isTablet = width >= 768 && width < 1024;
const isDesktop = width >= 1024;
return (
<div>
<p>Window Size: {width} x {height}</p>
{isMobile && <MobileLayout />}
{isTablet && <TabletLayout />}
{isDesktop && <DesktopLayout />}
</div>
);
}API:
function useWindowSize(): {
width: number;
height: number;
}Returns:
width- Window inner widthheight- Window inner height
Note: Returns { width: 0, height: 0 } during SSR.
🎯 Common Use Cases
Preventing Unnecessary Re-renders
import { useStableCallback } from '@upendra.manike/react-utils';
// ❌ Bad: Creates new function on every render
function Parent() {
return <Child onClick={() => console.log('click')} />;
}
// ✅ Good: Stable reference
function Parent() {
const handleClick = useStableCallback(() => console.log('click'));
return <Child onClick={handleClick} />;
}Debounced Search
import { useDebounce, useAsync } from '@upendra.manike/react-utils';
function SearchComponent() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data, loading } = useAsync(
() => searchAPI(debouncedQuery),
false
);
useEffect(() => {
if (debouncedQuery) {
// Trigger search
}
}, [debouncedQuery]);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{loading && <div>Searching...</div>}
{data && <Results data={data} />}
</div>
);
}Safe Async Operations
import { useSafeState, useAsync } from '@upendra.manike/react-utils';
function DataComponent() {
const [data, setData] = useSafeState(null);
const { loading, error, execute } = useAsync(
async () => {
const result = await fetchData();
setData(result); // Safe even if component unmounts
return result;
},
true
);
if (loading) return <Loader />;
if (error) return <Error message={error.message} />;
return <DataDisplay data={data} />;
}🔧 TypeScript Support
Full TypeScript support with comprehensive type definitions:
import type {
FormErrors,
UseFormOptions,
UseAsyncState,
VirtualItem,
UseVirtualListOptions
} from '@upendra.manike/react-utils';📖 Examples
Complete Form Example
import { useForm } from '@upendra.manike/react-utils';
interface SignupForm {
name: string;
email: string;
password: string;
confirmPassword: string;
}
function SignupForm() {
const { values, errors, touched, isSubmitting, setValue, setFieldTouched, handleSubmit } = useForm<SignupForm>({
initialValues: {
name: '',
email: '',
password: '',
confirmPassword: '',
},
validate: (vals) => {
const errs: Record<string, string> = {};
if (!vals.name.trim()) errs.name = 'Name is required';
if (!vals.email) errs.email = 'Email is required';
if (vals.password !== vals.confirmPassword) {
errs.confirmPassword = 'Passwords do not match';
}
return errs;
},
onSubmit: async (vals) => {
await signup(vals);
// Handle success
},
});
return (
<form onSubmit={handleSubmit}>
{/* Form fields */}
</form>
);
}Virtual List Example
import { useVirtualList } from '@upendra.manike/react-utils';
function ProductList({ products }) {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = useState(0);
const { virtualItems, totalHeight } = useVirtualList(products, {
itemHeight: 100,
containerHeight: 600,
overscan: 3,
});
return (
<div
ref={containerRef}
style={{ height: 600, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
{virtualItems.map(({ index, start, size }) => (
<div
key={index}
style={{
position: 'absolute',
top: start,
height: size,
width: '100%',
}}
>
<ProductCard product={products[index]} />
</div>
))}
</div>
</div>
);
}🚀 Performance Tips
- Use
useStableCallbackfor callbacks passed to memoized components - Use
useDebouncefor search inputs and expensive operations - Use
useVirtualListfor lists with 100+ items - Use
useDeepCompareEffectsparingly - prefer data restructuring - Use
useSafeStatefor async operations that might outlive component
🤝 Contributing
Contributions welcome! Please feel free to submit a Pull Request.
📄 License
MIT © Upendra Manike
🔗 Related Packages
@upendra.manike/tiny-utils- General JavaScript utilities@upendra.manike/async-utils- Async/promise utilities@upendra.manike/string-utils- String manipulation utilities
💡 Problems Solved
This package addresses common React problems including:
- ✅ Inline functions causing unnecessary re-renders
- ✅ Debouncing/throttling state issues
- ✅ State updates not triggering re-renders
- ✅ Infinite render loops
- ✅ Large list performance issues
- ✅ Async data fetching complexity
- ✅ Form validation messiness
- ✅ Error boundary misuse
- ✅ Window size inconsistencies
- ✅ Race conditions in async operations
- ✅ Memory leaks from unmounted components
Made with ❤️ by Upendra Manike
