npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

Readme

@upendra.manike/react-utils

React utilities for common problems: hooks, state management, performance optimization, forms, and more.

npm version License: MIT

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
): T

useDebounce

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): T

Parameters:

  • value - The value to debounce
  • delay - 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): T

Parameters:

  • value - The value to throttle
  • delay - 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 | undefined

Returns: 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
): void

Note: 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 values
  • errors - Validation errors
  • touched - Field touched state
  • isSubmitting - Submission state
  • setValue - Update field value
  • setFieldTouched - Mark field as touched
  • handleSubmit - Form submission handler
  • reset - 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 execute
  • immediate - Execute immediately on mount (default: true)

Returns:

  • data - Result data or null
  • loading - Loading state
  • error - Error object or null
  • execute - Manually trigger execution
  • reset - 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 render
  • options.itemHeight - Fixed height or function returning height per index
  • options.overscan - Number of items to render outside viewport (default: 5)
  • options.containerHeight - Container viewport height

Returns:

  • virtualItems - Array of visible virtual items with positioning
  • totalHeight - Total height of all items
  • scrollToIndex - Scroll to specific item index
  • scrollOffset - Current scroll offset
  • setScrollOffset - 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 or null
  • hasError - Boolean indicating if error exists
  • resetError - Clear error state
  • captureError - 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 width
  • height - 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

  1. Use useStableCallback for callbacks passed to memoized components
  2. Use useDebounce for search inputs and expensive operations
  3. Use useVirtualList for lists with 100+ items
  4. Use useDeepCompareEffect sparingly - prefer data restructuring
  5. Use useSafeState for async operations that might outlive component

🤝 Contributing

Contributions welcome! Please feel free to submit a Pull Request.


📄 License

MIT © Upendra Manike


🔗 Related Packages


💡 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

🔗 Explore All JSLib Libraries