Skip to main content

State Management Hooks

useToggle

A simple hook for managing boolean state with toggle functionality.
import { useToggle } from '@zayne-labs/toolkit-react';

function Component() {
  const [isOpen, toggleIsOpen] = useToggle(false);
  
  return (
    <div>
      <button onClick={() => toggleIsOpen()}>
        Toggle (current: {isOpen ? 'open' : 'closed'})
      </button>
      <button onClick={() => toggleIsOpen(true)}>Open</button>
      <button onClick={() => toggleIsOpen(false)}>Close</button>
    </div>
  );
}
Type Signature:
type InitialState = boolean | (() => boolean);

function useToggle(initialValue?: InitialState): [
  value: boolean,
  toggleValue: <TValue>(newValue?: TValue) => void
];

useDisclosure

Manages open/close state for modals, drawers, and other disclosure widgets with optional scroll locking.
import { useDisclosure } from '@zayne-labs/toolkit-react';

function Modal() {
  const { isOpen, onOpen, onClose, onToggle } = useDisclosure({
    initialState: false,
    hasScrollControl: true // Locks scroll when open
  });
  
  return (
    <>
      <button onClick={onOpen}>Open Modal</button>
      {isOpen && (
        <div className="modal">
          <button onClick={onClose}>Close</button>
          <button onClick={() => onToggle()}>Toggle</button>
        </div>
      )}
    </>
  );
}
Type Signature:
type DisclosureOptions = {
  hasScrollControl?: boolean;
  initialState?: boolean | (() => boolean);
};

function useDisclosure(options?: DisclosureOptions): {
  isOpen: boolean;
  onOpen: () => void;
  onClose: () => void;
  onToggle: (value?: boolean) => void;
};

useControllableState

Creates state that can be either controlled or uncontrolled, following the React controlled component pattern.
import { useControllableState } from '@zayne-labs/toolkit-react';

function Input({ value: controlledValue, onChange, defaultValue }) {
  const [value, setValue] = useControllableState({
    prop: controlledValue,
    defaultProp: defaultValue,
    onChange: onChange
  });
  
  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

// Uncontrolled usage
<Input defaultValue="hello" />

// Controlled usage
<Input value={externalValue} onChange={setExternalValue} />
Type Signature:
type UseControllableStateOptions<TProp> = {
  defaultProp?: TProp | (() => TProp);
  isControlled?: boolean;
  onChange?: (prop: TProp) => void;
  prop?: TProp;
};

function useControllableState<TProp>(
  options: UseControllableStateOptions<TProp>
): [state: TProp, setState: StateSetter<TProp>];

useStorageState

Syncs state with localStorage or sessionStorage with automatic serialization and cross-tab synchronization.
import { useStorageState } from '@zayne-labs/toolkit-react';

function ThemeToggle() {
  const [theme, { setState: setTheme }] = useStorageState(
    'theme',
    'light',
    {
      storageArea: localStorage,
      syncStateAcrossTabs: true
    }
  );
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current theme: {theme}
    </button>
  );
}
const [user, userActions] = useStorageState('user', { name: 'John' });

// Update state
userActions.setState({ name: 'Jane' });

// Remove from storage
userActions.removeState();
Type Signature:
function useStorageState<TValue, TSlice = TValue>(
  key: string,
  defaultValue?: TValue,
  options?: {
    equalityFn?: (a: TValue, b: TValue) => boolean;
    logger?: (error: Error) => void;
    parser?: (value: string) => TValue;
    partialize?: (value: TValue) => Partial<TValue>;
    select?: SelectorFn<TValue, TSlice>;
    serializer?: (value: TValue) => string;
    storageArea?: Storage;
    syncStateAcrossTabs?: boolean;
  }
): [state: TSlice, actions: StorageStoreApi<TValue>];

Performance Hooks

useDebouncedFn / useDebouncedState

Debounce function calls or state updates.
import { useDebouncedFn } from '@zayne-labs/toolkit-react';

function SearchInput() {
  const [query, setQuery] = useState('');
  
  const debouncedSearch = useDebouncedFn(
    (searchTerm: string) => {
      console.log('Searching for:', searchTerm);
    },
    500
  );
  
  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value);
        debouncedSearch(e.target.value);
      }}
    />
  );
}
Type Signatures:
function useDebouncedFn<TParams>(
  callBackFn: CallbackFn<TParams>,
  delay: number | undefined
): DebouncedFunction;

function useDebouncedState<TValue>(
  defaultValue: TValue,
  delay: number | undefined
): readonly [value: TValue, setDebouncedValue: Function, setValue: Function];

useThrottle Variants

Throttle function calls using different strategies.
import { useThrottleByTimeout } from '@zayne-labs/toolkit-react';

function ScrollComponent() {
  const handleScroll = useThrottleByTimeout(
    (event: Event) => {
      console.log('Scroll position:', window.scrollY);
    },
    200
  );
  
  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);
  
  return <div>Scroll the page</div>;
}
Type Signatures:
function useThrottleByTimeout<TParams>(
  callbackFn: CallbackFn<TParams>,
  delay: number
): ThrottledFunction;

function useThrottleByTimer<TParams>(
  callbackFn: CallbackFn<TParams>,
  delay: number
): ThrottledFunction;

function useThrottleByFrame<TParams>(
  callbackFn: CallbackFn<TParams>
): ThrottledFunction;

useCallbackRef

Returns a stable function reference that always points to the latest callback version.
import { useCallbackRef } from '@zayne-labs/toolkit-react';

function Component({ onSubmit }) {
  const stableOnSubmit = useCallbackRef(onSubmit);
  
  useEffect(() => {
    // stableOnSubmit reference never changes
    // but always calls the latest onSubmit
    const timer = setTimeout(() => stableOnSubmit(), 1000);
    return () => clearTimeout(timer);
  }, [stableOnSubmit]); // Safe to use in dependency array
  
  return <form onSubmit={stableOnSubmit}>...</form>;
}
Type Signature:
function useCallbackRef<TCallback extends AnyFunction | null | undefined | void>(
  callbackFn: TCallback
): TCallback;

Effect Hooks

useMountEffect

Runs an effect only when the component mounts.
import { useMountEffect } from '@zayne-labs/toolkit-react';

function Component() {
  useMountEffect(() => {
    console.log('Component mounted');
    fetchInitialData();
  });
  
  return <div>Content</div>;
}

useUnmountEffect

Runs cleanup code when component unmounts.
import { useUnmountEffect } from '@zayne-labs/toolkit-react';

function Component() {
  useUnmountEffect(() => {
    console.log('Component unmounting');
    cleanup();
  });
  
  return <div>Content</div>;
}

useEffectOnce

Runs an effect exactly once, even in React Strict Mode.
import { useEffectOnce } from '@zayne-labs/toolkit-react';

function Component() {
  useEffectOnce(() => {
    console.log('This runs exactly once');
    initializeAnalytics();
    
    return () => {
      console.log('Cleanup runs exactly once');
    };
  });
  
  return <div>Content</div>;
}
useEffectOnce is safe to use in React Strict Mode and will only execute once despite double-rendering.

Browser API Hooks

useClickOutside

Detects clicks outside of specified elements.
import { useClickOutside } from '@zayne-labs/toolkit-react';

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef(null);
  
  useClickOutside({
    ref: dropdownRef,
    onClick: () => setIsOpen(false),
    enabled: isOpen
  });
  
  return (
    <div ref={dropdownRef}>
      <button onClick={() => setIsOpen(true)}>Open</button>
      {isOpen && <div className="dropdown-content">Content</div>}
    </div>
  );
}
const ref = useRef(null);

useClickOutside({
  ref: ref,
  onClick: () => console.log('Clicked outside')
});
Type Signature:
type UseClickOutsideOptions<TElement extends HTMLElement> = {
  enabled?: boolean;
  onClick: (event: MouseEvent | TouchEvent) => void;
  ref?: Array<React.RefObject<TElement>> | React.RefObject<TElement>;
};

function useClickOutside<TElement extends HTMLElement>(
  options: UseClickOutsideOptions<TElement>
): { ref: React.RefObject<TElement> };

useCopyToClipboard

Copies content to the clipboard with automatic timeout.
import { useCopyToClipboard } from '@zayne-labs/toolkit-react';

function CopyButton() {
  const { handleCopy, hasCopied, value } = useCopyToClipboard({
    timeout: 2000,
    onSuccess: () => console.log('Copied!'),
    onError: (err) => console.error('Failed:', err)
  });
  
  return (
    <button onClick={() => handleCopy('Text to copy')}>
      {hasCopied ? 'Copied!' : 'Copy'}
    </button>
  );
}
Type Signature:
type AllowedClipboardItems = string | Blob;

function useCopyToClipboard(options?: {
  mimeType?: string;
  onCopied?: () => void;
  onError?: (error: Error) => void;
  onSuccess?: () => void;
  timeout?: number;
}): {
  handleCopy: (valueToCopy: AllowedClipboardItems) => void;
  hasCopied: boolean;
  setValue: (value: AllowedClipboardItems) => void;
  value: AllowedClipboardItems;
};

useIsHydrated

Detects if React has hydrated on the client side.
import { useIsHydrated } from '@zayne-labs/toolkit-react';

function Component() {
  const isHydrated = useIsHydrated();
  
  return (
    <button disabled={!isHydrated} onClick={handleClientSideAction}>
      {isHydrated ? 'Click me' : 'Loading...'}
    </button>
  );
}
This hook is useful for SSR applications to prevent hydration mismatches and enable client-only features.
Type Signature:
function useIsHydrated(): boolean;

useSearchParams

Manages URL search parameters with React state.
import { useSearchParams } from '@zayne-labs/toolkit-react';

function SearchPage() {
  const [searchParams, setSearchParams, triggerPopstate] = useSearchParams({
    defaultValues: { q: '', filter: 'all' },
    action: 'push' // or 'replace'
  });
  
  const query = searchParams.get('q');
  
  return (
    <input
      value={query}
      onChange={(e) => setSearchParams({ q: e.target.value })}
    />
  );
}
const [params, setParams] = useSearchParams();

// Get value
const query = params.get('q');

// Set params
setParams({ q: 'search term', page: '1' });

Advanced Hooks

createCustomContext

Creates type-safe React context with built-in error handling.
import { createCustomContext } from '@zayne-labs/toolkit-react';

type ThemeContextValue = {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
};

const [ThemeProvider, useTheme] = createCustomContext<ThemeContextValue>({
  name: 'Theme',
  hookName: 'useTheme',
  providerName: 'ThemeProvider',
  strict: true,
  errorMessage: 'useTheme must be used within ThemeProvider'
});

function App() {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeProvider.Provider value={{ theme, toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light') }}>
      <Child />
    </ThemeProvider.Provider>
  );
}

function Child() {
  const { theme, toggleTheme } = useTheme(); // Type-safe!
  return <button onClick={toggleTheme}>{theme}</button>;
}
Type Signature:
type CustomContextOptions<TContextValue, TStrict extends boolean> = {
  defaultValue?: TContextValue | null;
  errorMessage?: string;
  extendValue?: (contextValue: NoInfer<TContextValue> | null) => TContextValue | null;
  hookName?: string;
  name?: string;
  providerName?: string;
  strict?: TStrict;
};

function createCustomContext<TContextValue = null, TStrict extends boolean = true>(
  options?: CustomContextOptions<TContextValue, TStrict>
): [
  Provider: React.Context<TContextValue>,
  useCustomContext: UseCustomContext<TContextValue, TStrict>
];

useConstant

Lazy initialization that runs only once.
import { useConstant } from '@zayne-labs/toolkit-react';

function Component() {
  const expensiveObject = useConstant(() => {
    // This runs only once, even in strict mode
    return createExpensiveObject();
  });
  
  return <div>{expensiveObject.value}</div>;
}

Build docs developers (and LLMs) love