A lightweight, tree-shakable, and type-safe collection of essential React hooks for modern applications. Each hook is designed to be independent, performant, and production-ready, covering common real-world scenarios with minimal overhead.
🧩 Peer Dependency Notice This package depends on nhb-toolbox, a modular utility library that provides foundational classes and utilities used internally by certain hooks.
Specifically:
- useClock and useTimer rely on the Chronos class from nhb-toolbox for accurate, timezone-aware date and time manipulation.
- Make sure to install both
nhb-hooksandnhb-toolboxpackages to use all available features. Both packages are fully tree-shakable, so only the hooks you use will be bundled if you use bundler tools likevite,turbopack,rollup,webpacketc.
npm:
npm i nhb-hooks nhb-toolboxpnpm:
pnpm add nhb-hooks nhb-toolboxyarn:
yarn add nhb-hooks nhb-toolbox✅ Tree-shakable – Only bundles the hooks you actually import. ✅ First-class TypeScript support – Written in TypeScript for strict type safety and IntelliSense. ✅ Zero runtime bloat – Minimal footprint with no unnecessary dependencies. ✅ Chronos integration – Hooks like useClock and useTimer leverage the powerful Chronos class for time, date, and timezone operations.
🧠 So far, only one utility class (Chronos) from nhb-toolbox is used. This keeps the package extremely light while allowing seamless future integration of more utilities from nhb-toolbox.
- useMediaQuery
- useBreakPoint
- useClickOutside
- useCopyText
- useDebouncedValue
- useClock
- useTimer
- useToggle
- useValidImage
- useWindowResize
- useTitle
- useMount
- useStorage
Evaluates a media query string or a screen width range and returns whether it matches. Detect if a media query matches the current viewport. Perfect for responsive UI logic.
import { useMediaQuery } from 'nhb-hooks';function useMediaQuery(queryOrOptions: string | MediaQueryOptions): boolean;// Checking for Mobile Screen Size (maxWidth)
const isMobile = useMediaQuery({ maxWidth: 767 });
// Checking for Tablet Screen Size (minWidth and maxWidth)
const isTablet = useMediaQuery({ minWidth: 768, maxWidth: 1024 });
// Checking for Desktop Screen Size (minWidth)
const isDesktop = useMediaQuery({ minWidth: 1025 });
// Using a Custom Media Query String
const isLandscape = useMediaQuery('(orientation: landscape)');
const mobile = useMediaQuery('(max-width: 767px)');
const tablet = useMediaQuery('(min-width: 768px) and (max-width: 1279px)');
const desktop = useMediaQuery('(min-width: 1280px)');// Show mobile-only component
const isMobile = useMediaQuery({ maxWidth: 767 });
return (
{isMobile && <MobileMenu />}
)
// Adjust layout based on screen size
const isLargeScreen = useMediaQuery({ minWidth: 1200 });
return (
<Grid columns={isLargeScreen ? 4 : 2} />
)- Automatic Updates: Recalculates whenever the viewport size changes
- Performance: Uses
matchMediaunder the hood for efficient detection - Options Format: Prefer using the object format (
{ minWidth, maxWidth }) over strings for better type safety - SSR Incompatible: Hooks are not meant for SSR. Use it in client components
- Multiple Conditions: Combine conditions with
andin strings or by passing bothminWidthandmaxWidthin options object.
Best Practice:
// Recommended
const isTablet = useMediaQuery({ minWidth: 768, maxWidth: 1024 });
// Less recommended (prone to typos)
const isTablet = useMediaQuery('(min-width: 768px) and (max-width: 1024px)');/** Interface for `useMediaQuery` hook's options */
interface MediaQueryOptions {
/** Minimum screen width in pixels (inclusive) */
minWidth?: number;
/** Maximum screen width in pixels (inclusive) */
maxWidth?: number;
}Simplified responsive breakpoints detection. Detects responsive breakpoints based on screen width.
import { useBreakPoint } from 'nhb-hooks';function useBreakPoint(): {
mobile: boolean;
tablet: boolean;
desktop: boolean;
};const { mobile, tablet, desktop } = useBreakPoint();
// mobile: true if width ≤ 767px
// tablet: true if 768px ≤ width ≤ 1279px
// desktop: true if width ≥ 1280pxconst { mobile, tablet, desktop } = useBreakPoint();
return (
<>
{mobile && <MobileNav />}
{tablet && <TabletLayout />}
{desktop && <DesktopSidebar />}
</>
);- Predefined Breakpoints: Uses common device breakpoints (mobile < 768px, tablet 768-1279px, desktop ≥1280px)
- Derived Hook: Built on top of useMediaQuery
- Consistent Values: Only one breakpoint will be true at any time
- No Customization: Breakpoints are fixed (use
useMediaQuerydirectly for custom breakpoints)
When to Use:
- Quick responsive layouts with standard breakpoints
- When you need mobile/tablet/desktop detection
Detects clicks outside of specified element(s). Great for closing dropdowns/modals when clicking outside.
import { useClickOutside } from 'nhb-hooks';// Single element version
function useClickOutside<T extends Element | null>(
handler: () => void,
): React.RefObject<T>;
// Multiple elements version
function useClickOutside<T extends Element | null>(
refs: RefType<T>[],
handler: () => void,
): void;// Single element
const ref = useClickOutside(() => {
console.log('Clicked outside the element');
});
return <div ref={ref}>Click outside me</div>;
// Multiple elements
const ref1 = useRef(null);
const ref2 = useRef(null);
useClickOutside([ref1, ref2], () => {
console.log('Clicked outside both elements');
});
return (
<>
<div ref={ref1}>Box 1</div>
<div ref={ref2}>Box 2</div>
</>
);function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useClickOutside(() => setIsOpen(false));
return (
<div ref={ref}>
<button onClick={() => setIsOpen(true)}>Menu</button>
{isOpen && <div className="dropdown">...</div>}
</div>
);
}- Multiple Elements: Supports both single element and multiple element detection
- Event Types: Handles both mouse and touch events
- Cleanup: Automatically removes event listeners
- Ref Handling: Returns a ref for single element version
Important:
- Elements must be in the DOM when the click occurs
- Doesn't work with elements that stop event propagation
- For modals, ensure proper z-index so elements aren't covered
Performance Tip:
// Memoize handler if it creates new functions
const handler = useCallback(() => setIsOpen(false), []);
const ref = useClickOutside(handler);Copy text to clipboard with lifecycle callbacks and timeout-controlled state reset.
import { useCopyText } from 'nhb-hooks';function useCopyText(options?: CopyOptions): {
copiedText: string | undefined;
copyToClipboard: (
text: string,
msg?: string,
errorMsg?: string,
) => Promise<void>;
};// Basic usage
const { copiedText, copyToClipboard } = useCopyText();
return (
<button onClick={() => copyToClipboard('Hello, world!')}>
{copiedText ? 'Copied!' : 'Copy Text'}
</button>
);// With success and error handling
const { copiedText, copyToClipboard } = useCopyText({
onSuccess: (msg) => toast.success(msg),
onError: (msg) => toast.error(msg),
resetTimeOut: 1500,
});
return (
<button onClick={() => copyToClipboard('secret-token', 'Token copied!')}>
{copiedText ? '✔ Copied' : 'Copy Token'}
</button>
);onSuccess: Callback called when text is successfully copied. Receives a success message string.onError: Callback called if copy operation fails. Receives an error message string.resetTimeOut: Time in milliseconds to retaincopiedTextbefore it resets toundefined. Defaults to2500.
copiedTextState: Useful for showing transient UI feedback like button label change ("Copied!" state).- Fallback-Safe: Works in environments without
navigator.clipboardby falling back todocument.execCommand('copy'). - Resets Automatically: Automatically clears
copiedTextafter timeout (resets toundefined).
/** Options for useCopyText hook. */
interface CopyOptions {
/** Called when text is successfully copied. Receives a message. */
onSuccess?: (msg: string) => void;
/** Called when copy operation fails. Receives an error message. */
onError?: (msg: string) => void;
/** How long to retain the copied text in state before resetting. */
resetTimeOut?: number;
}Returns a debounced version of the input value. Optimize inputs and expensive calculations.
import { useDebouncedValue } from 'nhb-hooks';function useDebouncedValue<T>(value: T, delay?: number): [T, () => void];const [search, setSearch] = useState('');
const [debouncedSearch, cancel] = useDebouncedValue(search, 500);
// debouncedSearch updates 500ms after search stops changing
// cancel() aborts pending updatefunction Search() {
const [query, setQuery] = useState('');
const [debouncedQuery] = useDebouncedValue(query, 500);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}- Cancellation: Includes a cancel function to abort pending updates
- Leading Edge: Doesn't fire immediately (for leading edge debounce, consider
useThrottle) - Cleanup: Automatically clears pending timeouts
- Value Stability: Returns the same value until delay passes
Common Use Cases:
- Search input debouncing
- Expensive calculations
- Auto-save forms
Warning:
// Try to avoid this - creates new function each render. For tiny project using like this is okay but for large scale project use it with `RTK Query` or `React (Tanstack) Query`
useEffect(() => {
fetchResults(debouncedQuery);
}, [debouncedQuery]);Live-updating clock based on Chronos from nhb-toolbox. Supports formatting, timezones, animation frame ticking, and pause/resume. Lightweight and reactive by default.
import { useClock } from 'nhb-hooks';function useClock(options?: UseClockOptions): UseClockResult;// Default usage — updates every second
const { time } = useClock();
console.log(time.toISOString());// With formatting
const { formatted } = useClock({ format: 'HH:mm:ss' });
console.log(formatted); // → "14:45:32"// With custom timezone
const { time } = useClock({ timeZone: 'BDT' });
console.log(time.format()); // → local time in BDT// Frame-based updates (using requestAnimationFrame)
const { time } = useClock({ interval: 'frame' });// Start paused, then resume manually
const clock = useClock({ autoStart: false });
clock.resume(); // Starts tickingfunction ClockWidget() {
const { formatted } = useClock({ format: 'hh:mm:ss A', timeZone: '+06:00' });
return <p className="text-lg font-mono">{formatted}</p>;
}- Dependency: Uses
Chronosfromnhb-toolbox. - Timezone: Supports
TimeZonenames orUTCOffsetvalues (e.g."BDT"or"+06:00"). - Precision: Set
intervalfor custom update rate (default:1000ms). Use'frame'for smooth updates. - Control: Fully pauseable/resumable using
.pause()/.resume(). - Tree-shaking: Only includes
Chronosand itstimeZonePluginplugin is automatically applied internally.
interface UseClockOptions {
timeZone?: TimeZone | UTCOffSet;
format?: StrictFormat;
interval?: number | 'frame';
autoStart?: boolean;
}
interface UseClockResult {
time: Chronos;
formatted: string | undefined;
pause: () => void;
resume: () => void;
isPaused: boolean;
}| Property | Type | Default | Description |
|---|---|---|---|
timeZone |
TimeZone | UTCOffSet |
System TZ | Time zone override, e.g. 'BDT' or '+06:00' etc. |
format |
StrictFormat |
'HH:mm:ss' |
Format string used by format() method of Chronos instance |
interval |
number | 'frame' |
1000 |
Update interval in milliseconds or 'frame' for requestAnimationFrame |
autoStart |
boolean |
true |
Whether the clock starts immediately or remains paused |
| Property | Type | Description |
|---|---|---|
time |
Chronos |
The current Chronos instance, auto-updated |
formatted |
string |
Formatted time string using the given format, or HH:mm:ss if none |
pause |
() => void |
Function to pause the ticking clock |
resume |
() => void |
Function to resume the clock if paused |
isPaused |
boolean |
Indicates whether the clock is currently paused |
Creates a countdown timer. Requires Chronos from nhb-toolbox (automatically tree-shaken if not used). Install it separately. Create countdown timers with minimal setup. Also provides a duration formatter utility: formatTimer.
import { useTimer, formatTimer} from 'nhb-hooks';// Duration-based timer
function useTimer(initialDuration: number, unit: TimerUnit): TimeDuration;
// Target time-based timer
function useTimer(time: ChronosInput): TimeDuration;// Countdown from 5 minutes
const timeLeft = useTimer(5, 'minute');
// { days: 0, hours: 0, minutes: 4, seconds: 59, ... }
// Countdown to specific date
const timeLeft = useTimer('2023-12-31');// Product sale countdown
function SaleBanner() {
const { days, hours, minutes, seconds } = useTimer('2023-12-31');
return (
<div>
Sale ends in: {days}d {hours}h {minutes}m {seconds}s
</div>
);
}
// Use the formatTimer utility
function SaleBanner() {
const timeLeft = useTimer('2023-12-31');
return (
<div>
Sale ends in: {formatTimer(timeLeft)}
</div>
);
}// Session timeout warning
function SessionTimeout() {
const timeLeft = useTimer(15, 'minute');
return (
<div>
Session expires in: {timeLeft.minutes}m {timeLeft.seconds}s
</div>
);
}
// Use the formatTimer utility
function SessionTimeout() {
const timeLeft = useTimer(15, 'minute');
return (
<div>
Session expires in: {formatTimer(timeLeft)}
</div>
);
}- Dependency: Requires Chronos from
nhb-toolbox - Precision: Updates every second (
1000ms) - Formats: Accepts both duration and target date
- Output: Returns a
TimeDurationobject with dynamicyears,months,days,hours,minutes,secondsand staticmillisecondsproperties - Duration Formatter:
nhb-hooksalso provides utility to format the returnedTimeDurationobject:formatTimer
Important:
- Install required package:
npm i nhb-toolbox - Tree-shakable -
Chronosis bundled only if the hook is used
Accepted Formats:
useTimer(5, 'minute'); // Countdown from 5 minutes
useTimer(5, 'day'); // Countdown from 5 days
useTimer('2025-12-31'); // Countdown to NYE
useTimer(new Date(2025, 11, 31)); // Date object
useTimer(new Chronos(2025, 11, 31)); // Chronos objectinterface TimeDuration {
years: number;
months: number;
days: number;
hours: number;
minutes: number;
seconds: number;
milliseconds: number;
}
type ChronosInput = number | string | Date | Chronos;
type TimerUnit = 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second' | 'millisecond';Formats a TimeDuration object (returned by useTimer hook) into a human-readable string.
- The
formatTimerutility transforms a duration object into a readable time string such as"2 hours · 15 minutes"or"2h 15m".- It is especially useful when displaying timer or countdown values in the UI with minimal code.
- Exported as separate utility to reduce final bundle size by making it optional.
| Name | Type | Description |
|---|---|---|
duration |
TimeDuration |
Duration object returned by useTimer. |
options |
TimerFormatOptions (optional) |
Control display style, separator, and formatting behavior. |
| Option | Type | Default | Description |
|---|---|---|---|
maxUnits |
1–6 |
6 |
Limits the number of displayed time units. |
separator |
string |
' · ' |
String used to separate time units. |
style |
'full' | 'short' |
'full' |
Display style. "full" → "2 hours", "short" → "2h". |
showZero |
boolean |
false |
Whether to include units with 0 value. |
import { formatTimer, useTimer } from 'nhb-hooks';
const duration = useTimer('2025-12-31');
console.log(formatTimer(duration));
// something like → "1 day · 2 hours · 15 minutes · 30 seconds"
console.log(formatTimer(duration, { style: 'short', maxUnits: 2 }));
// something like → "1d · 2h"
console.log(formatTimer(duration, { showZero: true }));
// something like → "0 years · 0 months · 1 day · 2 hours · 15 minutes · 30 seconds"formatTimerreturns a human-readable formatted duration string from duration object returned byuseTimerhook.- When
showZeroisfalse(default), only units with non-zero values are included in the output. - If all values are zero and
showZeroisfalse, the result will be"0 seconds"or"0s"depending onstyle. - The
maxUnitsparameter limits the number of time units displayed, starting from the largest unit (years). It always applies after zero filtering. - The method automatically handles pluralization in
"full"style (e.g.,"1 second"vs"2 seconds"). - The order of units is always consistent:
years → months → days → hours → minutes → seconds. - It removes
'milliseconds'property as it is static (updates after1000mswhich is equivalent toseconds). - Short style abbreviations:
years(y),months(mo),days(d),hours(h),minutes(m),seconds(s).
durationmethod fromChronos— Used internally byuseTimerto compute durations.durationString— SimilarChronosmethod which returns formatted duration string.
Clean state toggling between two values.
import { useToggle } from 'nhb-hooks';function useToggle<T>(values: [T, T]): [T, () => void];const [isOn, toggle] = useToggle([false, true]);
toggle(); // switches between false and true
const [fruit, switchFruit] = useToggle(['apple', 'orange']);
switchFruit(); // switches between 'apple' and 'orange'
const [theme, toggleTheme] = useToggle(['light', 'dark']);
toggleTheme(); // Switches between `dark` and `light` theme- Simple API: Just provide two values to toggle between
- Type Safe: Maintains your value types
- Stable Toggle: Function identity remains consistent
- No Limits: Works with any comparable values
- Note: Values must be distinct (don't use [true, true])
Creative Uses:
const [mode, toggle] = useToggle(['light', 'dark']); // Theme
const [tab, switchTab] = useToggle(['overview', 'details']); // Tabs
const [view, toggleView] = useToggle(['list', 'grid']); // LayoutGraceful image loading with fallbacks. Validates image URLs and provides fallback for broken images.
import { useValidImage } from 'nhb-hooks';function useValidImage<T extends string | string[]>(
input: T | undefined,
options?: ValidImageOptions,
): ValidImage<T>;// Single image
const avatar = useValidImage('user/avatar.jpg', {
imgHostLink: 'https://cdn.example.com/',
placeholder: '/default-avatar.png',
});
// Multiple images
const gallery = useValidImage(['img1.jpg', 'img2.jpg']);// Single image with CDN prefix and no trailing slash
const avatar = useValidImage('user123.jpg', {
imgHostLink: 'https://cdn.example.com',
placeholder: '/default-avatar.png',
trailingSlash: false,
});
return <img src={avatar} alt="Profile" />;
// Image gallery
const galleryImages = useValidImage(
['photo1.jpg', 'photo2.jpg', 'photo3.jpg'],
{ imgHostLink: 'https://images.example.com' },
);
return galleryImages.map((img, i) => (
<img key={i} src={img} alt={`Photo ${i}`} />
));imgHostLink: Base path to prepend to image URL(s) if the image is hosted somewhere else. By default the hook assumes that the link has a trailing/. Customize it intrailingSlashoption.trailingSlash: Whether theimgHostLinkhas a trailing slash/. Default istrue. Full image URL will be built on this flag.placeholder: Fallback image URL. It can be local/public image or hosted image (needs full url for hosted placeholder image).
- Fallback: Automatically uses placeholder for broken images
- CDN Support: Easily prepend base URLs
- Async Loading: Checks images in parallel
- Type Safe: Maintains input type (string or string[])
Important Options:
{
imgHostLink: 'https://cdn.example.com', // Base URL
trailingSlash: false, // Handle URL formatting
placeholder: '/fallback.jpg' // Custom fallback
}Performance:
- Only checks images once per URL
- Doesn't revalidate unless input changes
/** Type for `useValidImage` hook's return type. */
type ValidImage<T> = T extends string ? string : string[];
/** Options for `useValidImage` hook. */
interface ValidImageOptions {
/** Base path to prepend to image URL(s) if the image is hosted somewhere else. By default the hook assumes that the link has a trailing `/`. Customize it in `trailingSlash` option. */
imgHostLink?: string;
/** Whether the `imgHostLink` has a trailing slash `/`. Default is `true`. Full image URL will be built on this flag. */
trailingSlash?: boolean;
/** Fallback image URL. It can be local/public image or hosted image (needs full url for hosted placeholder image). */
placeholder?: string;
}Triggers a callback whenever the window is resized..
import { useWindowResize } from 'nhb-hooks';function useWindowResize(callback: () => void): void;useWindowResize(() => {
console.log('Window resized');
});useWindowResize(() => {
// Recalculate layout on resize
updateChartDimensions();
});- Simple API: Just pass your resize handler
- Cleanup: Automatically removes listeners
- Throttling: Doesn't include built-in throttling (add your own if needed)
Performance Tip:
// Throttle heavy operations
useWindowResize(() => {
throttleAction(() => updateLayout(), 100);
});
// Or use with useDebouncedValue
const [width, setWidth] = useState(window.innerWidth);
useWindowResize(debounceAction(() => setWidth(window.innerWidth), 200);Sets the document.title dynamically at runtime, using your app’s site title configuration. Supports prepend/append positions, custom separators, and global title context via a provider.
import { useTitle, useTitleMeta, TitleProvider } from 'nhb-hooks';Wrap your root component (or layout) with TitleProvider to configure the global site title and defaults:
import { TitleProvider } from 'nhb-hooks';
<TitleProvider
config={{
siteTitle: 'Bangu Site Inc.',
defaultPosition: 'after', // or 'before'
defaultSeparator: ' - ',
}}
>
<App />
</TitleProvider>function useTitle(title: string, options?: TitleOptions): void| Option | Type | Description | Default |
|---|---|---|---|
separator |
string |
Character(s) between page and site title | " - " |
position |
"before" | "after" |
Where to place the page title: before or after the site title | "before" |
favicon |
string | undefined |
Optional favicon to temporarily set with the title |
// Basic usage (uses default site title and config)
useTitle('Dashboard'); // → "Dashboard - Bangu Site Inc."
// Change position
useTitle('Login', { position: 'after' }); // → "Bangu Site Inc. - Login"
// Custom separator
useTitle('Docs', { separator: ' | ' }); // → "Docs | Bangu Site Inc."
// Custom everything
useTitle('Account', { position: 'after', separator: ' • ' }); // → "Bangu Site Inc. • Account"function Page() {
useTitle('Settings');
return <h1>Settings Page</h1>;
}function Page() {
useTitle('About', { position: 'after', separator: ' · ' });
return <h1>About Us</h1>;
}On unmount, useTitle will restore the previous document title, making it safe for conditional rendering and nested layouts.
- Client-only: This hook must run in a browser environment.
- Memoization: You don’t need to memoize
options; shallow comparison is already handled. - TitleProvider config options: If not used, fallback title will be
titleonly.
You can extract the current title metadata using:
import { useTitleMeta } from 'nhb-hooks';
const { siteTitle, pageTitle, fullTitle, ... } = useTitleMeta();Extract and observe current title state from the global TitleProvider context.
Use useTitleMeta when you want to read the current title state (e.g., for displaying breadcrumbs, page headers, or meta tags).
function useTitleMeta(): TitleMeta| Key | Type | Description |
|---|---|---|
pageTitle |
string |
The current page-specific title |
siteTitle |
string |
The global app/site name |
fullTitle |
string |
The computed document.title value |
defaultPosition |
"before" | "after" |
Global default for title positioning |
defaultSeparator |
string |
Global default separator |
import { useTitleMeta } from 'nhb-hooks';
function Breadcrumb() {
const { pageTitle, fullTitle } = useTitleMeta();
return <nav aria-label="breadcrumb">{pageTitle}</nav>;
}/** Configuration values for the provider context */
interface TitleConfig {
siteTitle?: string;
defaultPosition?: 'before' | 'after';
defaultSeparator?: string;
}
/** Props for the TitleProvider component */
interface TitleProviderProps {
children: React.ReactNode;
config?: Partial<TitleConfig>;
}
/** Per-call override options */
interface TitleOptions {
separator?: string;
position?: 'before' | 'after';
favicon?: string;
}
/** Metadata from `TitleProvider` and `useTitle` */
interface TitleMeta {
siteTitle?: string;
pageTitle?: string;
fullTitle?: string;
defaultPosition?: 'before' | 'after';
defaultSeparator?: string;
}| Scenario | Recommendation |
|---|---|
| Default branding | Use TitleProvider once in your root layout for consistent app-wide titles |
| Specific page titles | Use useTitle for client-side updates; use <title> tag for SSR |
| Read-only access | Use useTitleMeta() in components like breadcrumbs or metadata injection |
| SSR | useTitle doesn't run on server — inject <title> tag manually for SEO |
ℹ️
useTitleonly affects the document title after hydration.
For proper SEO and server-rendered HTML, include a static<title>in your SSR framework's head management.
A tiny, client-only React hook to prevent Next.js hydration mismatch errors.
Next.js hydration mismatch errors occur when the server-rendered HTML doesn't match the client render.
useMount solves this by rendering children only after the component mounts on the client.
import { useMount } from 'nhb-hooks';function useMount<T extends ReactNode>(children: T, onMount?: () => void): T | null'use client';
import FloatingButton from '@/components/ui/floating-button';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { useMount } from 'nhb-hooks';
import { useCallback } from 'react';
export default function ThemeToggler() {
const { theme, setTheme } = useTheme();
const toggleTheme = useCallback(() => {
if (theme) {
setTheme(theme === 'dark' ? 'light' : 'dark');
}
}, [theme]);
return useMount(
<FloatingButton onClick={toggleTheme} icon={theme === 'dark' ? Sun : Moon} />
);
}const ClientOnlyContent = () => {
return useMount(<div>This will only render on the client!</div>, () => console.log('Mounted on client!'));
};- Returns
nullon the server or before mounting to avoid mismatch. - Perfect for Floating Buttons, theme toggles, or other client-only UI elements.
- Lightweight, zero dependencies, fully typed for TypeScript.
- Works seamlessly with
Next.jsApp Router.
- Hydration-safe: Ensures
childrenrender only on the client. - Tiny & composable: Works with any component or UI element. Keeps SSR clean while safely rendering client-only logic.
- Optional callback: Executes client-only logic after mount, useful for initialization.
- Type-safe: Generic
<T extends ReactNode>supports all React content. - No layout shift: Simple, lightweight, no extra markup is added.
- Versatile: Can be used for buttons, modals, theme togglers, animations, etc.
Persist state in localStorage or sessionStorage with reactive updates and type safety. Safely handles SSR environments like Next.js.
import { useStorage } from 'nhb-hooks';function useStorage<T>(options: StorageOptions<T>): WebStorage<T>;// Basic usage - store theme preference
const themeStorage = useStorage<string>({
key: 'app-theme',
type: 'local',
});
return (
<button onClick={() => themeStorage.set('dark')}>
Current theme: {themeStorage.value ?? 'none'}
</button>
);// Store complex objects with custom serialization
type User = {
name: string;
age: number;
dob: Date;
};
const userStore = useStorage<User>({
key: 'app-user',
serialize: (u) => JSON.stringify(u),
deserialize: (s) => {
const parsed = JSON.parse(s);
return { ...parsed, dob: new Date(parsed.dob) };
},
});
// Session storage example
const sessionData = useStorage<number[]>({
key: 'cart-items',
type: 'session',
});// Complete storage management
const settings = useStorage<Settings>({
key: 'app-settings',
type: 'local',
});
// Update settings
settings.set({ theme: 'dark', language: 'en' });
// Remove just these settings
settings.remove();
// Clear all local storage
settings.clear();key: Unique key to identify the stored value (required)type: Storage type -'local'(default) or'session'serialize: Custom function to convert value to string (default:JSON.stringify)deserialize: Custom function to parse stored string back to type (default:JSON.parse)
- SSR Safe: Delays storage access until client-side hydration is complete
- Reactive: Component re-renders when stored value changes
- Type Safe: Full TypeScript support with generic type parameter
- Error Handling: Gracefully handles storage errors (quota exceeded, etc.)
- Synchronized: Multiple components using same key stay in sync
Important Behaviors:
- Returns
nullforvalueif key doesn't exist or on error set()overwrites existing valueremove()deletes only the specified keyclear()removes all items from the selected storage type
/** Options for `useStorage` hook. */
export type StorageOptions<T> = {
/** Key to store the value with in local/session storage. */
key: string;
/** Storage type to use (`localStorage`/`sessionStorage`). Defaults to `'local'`. */
type?: 'local' | 'session';
/**
* Optional serializer function to convert the value of type `T` to a string.
* Defaults to `JSON.stringify`.
*/
serialize?: (value: T) => string;
/**
* Optional deserializer function to convert the stored value back to type `T`.
* Defaults to `JSON.parse`.
*/
deserialize?: (value: string) => T;
};
/** Return type of `useStorage` hook. */
export type WebStorage<T> = {
/** Current value from storage, or `null` if not set or on error. */
value: T | null;
/** Function to set value in specified storage. */
set: (value: T) => void;
/** Function to remove the item from specified storage. */
remove: () => void;
/** Function to clear all items from the selected storage type. */
clear: () => void;
};MIT © Nazmul Hassan. See LICENSE for details.