Hooks
Learn about the custom React hooks available in Metronic React.
Metronic React includes a collection of custom React hooks to simplify common UI patterns and functionality. These hooks are located in the src/hooks
directory and can be imported as needed in your components.
Hook | Purpose | Path |
---|---|---|
useViewport | Monitor viewport dimensions | src/hooks/use-viewport.ts |
useIsMobile | Detect mobile viewport | src/hooks/use-mobile.tsx |
useScrollPosition | Monitor scroll position | src/hooks/use-scroll-position.ts |
useBodyClass | Add/remove body classes | src/hooks/use-body-class.ts |
useCopyToClipboard | Clipboard functionality | src/hooks/use-copy-to-clipboard.ts |
useMenu | Navigation menu state | src/hooks/use-menu.ts |
useSliderInput | Slider with inputs | src/hooks/use-slider-input.ts |
useMounted | Component mount detection | src/hooks/use-mounted.ts |
Viewport Hooks
useViewport
Monitor the viewport dimensions (height and width) with automatic updates on window resize.
import { useViewport } from '@/hooks/use-viewport';
function ResponsiveComponent() {
const [height, width] = useViewport();
return (
<div className="p-4 bg-card rounded-md shadow-sm">
<h3 className="text-lg font-medium mb-2">Viewport Dimensions</h3>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Width</p>
<p className="text-xl font-semibold">{width}px</p>
</div>
<div className="p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Height</p>
<p className="text-xl font-semibold">{height}px</p>
</div>
</div>
</div>
);
}
useIsMobile
Detect if the current viewport is considered mobile based on a breakpoint (992px by default).
import { useIsMobile } from '@/hooks/use-mobile';
function ResponsiveLayout() {
const isMobile = useIsMobile();
return (
<div className="p-4 bg-card rounded-md shadow-sm">
<h3 className="text-lg font-medium mb-2">Responsive Layout</h3>
{isMobile ? (
<div className="space-y-4">
<div className="p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Mobile Navigation</p>
<div className="flex justify-between mt-2">
<button className="px-3 py-1 bg-primary text-white rounded">Home</button>
<button className="px-3 py-1 bg-primary text-white rounded">Profile</button>
<button className="px-3 py-1 bg-primary text-white rounded">Settings</button>
</div>
</div>
</div>
) : (
<div className="flex space-x-4">
<div className="w-64 p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Sidebar</p>
<ul className="mt-2 space-y-2">
<li className="p-2 hover:bg-background rounded cursor-pointer">Dashboard</li>
<li className="p-2 hover:bg-background rounded cursor-pointer">Analytics</li>
<li className="p-2 hover:bg-background rounded cursor-pointer">Reports</li>
</ul>
</div>
<div className="flex-1 p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Main Content</p>
<p className="mt-2">This is the main content area for desktop view.</p>
</div>
</div>
)}
</div>
);
}
Document and Element Hooks
useScrollPosition
Monitor scroll position with throttling to improve performance.
import { useScrollPosition } from '@/hooks/use-scroll-position';
function ScrollIndicator() {
const scrollY = useScrollPosition();
// Calculate scroll percentage
const scrollPercentage = Math.min(
(scrollY / (document.body.scrollHeight - window.innerHeight)) * 100,
100
);
return (
<div className="fixed top-0 left-0 w-full h-1 z-50">
<div
className="h-full bg-primary transition-all duration-300"
style={{ width: `${scrollPercentage}%` }}
/>
</div>
);
}
// With a specific element reference
function ScrollableContainer() {
const containerRef = useRef<HTMLDivElement>(null);
const scrollY = useScrollPosition({ targetRef: containerRef });
return (
<div
ref={containerRef}
className="h-64 overflow-auto border rounded-md p-4"
>
<div className="h-96 flex items-center justify-center">
<p>Scroll position: {scrollY}px</p>
</div>
</div>
);
}
useBodyClass
Add or remove a CSS class from the document body element, useful for global styling states.
import { useBodyClass } from '@/hooks/use-body-class';
function DarkModeToggle() {
const [isDarkMode, setIsDarkMode] = useState(false);
// Add or remove dark-mode class from body
useBodyClass(isDarkMode ? 'dark-mode' : 'light-mode');
return (
<button
onClick={() => setIsDarkMode(!isDarkMode)}
className="px-4 py-2 bg-primary text-white rounded-md"
>
{isDarkMode ? 'Light Mode' : 'Dark Mode'}
</button>
);
}
// With multiple classes
function LayoutController() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [rtlMode, setRtlMode] = useState(false);
// Add or remove multiple classes
useBodyClass(
`${sidebarOpen ? 'sidebar-open' : 'sidebar-closed'} ${rtlMode ? 'rtl' : 'ltr'}`
);
return (
<div className="space-y-4">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="px-4 py-2 bg-primary text-white rounded-md mr-2"
>
Toggle Sidebar
</button>
<button
onClick={() => setRtlMode(!rtlMode)}
className="px-4 py-2 bg-primary text-white rounded-md"
>
Toggle RTL
</button>
</div>
);
}
UI Utility Hooks
useCopyToClipboard
Provide clipboard copy functionality with success state management.
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
function CopyableText({ text }) {
const { isCopied, copyToClipboard } = useCopyToClipboard({
timeout: 2000, // Reset after 2 seconds
onCopy: () => console.log('Text copied!')
});
return (
<div className="p-4 bg-card rounded-md shadow-sm">
<div className="flex items-center justify-between">
<code className="p-2 bg-muted rounded text-sm">{text}</code>
<button
onClick={() => copyToClipboard(text)}
className={`px-3 py-1 rounded-md ${
isCopied
? 'bg-green-500 text-white'
: 'bg-primary text-white'
}`}
>
{isCopied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
);
}
useMenu
Manage menu state for dropdown or nested menu components.
import { useMenu } from '@/hooks/use-menu';
import { MenuItem } from '@/config/types';
function NavigationMenu() {
const { isActive, hasActiveChild, getBreadcrumb } = useMenu();
// Example menu items
const menuItems: MenuItem[] = [
{
title: 'Dashboard',
path: '/dashboard',
icon: 'HomeIcon'
},
{
title: 'Users',
path: '/users',
icon: 'UsersIcon',
children: [
{ title: 'User List', path: '/users/list' },
{ title: 'User Profile', path: '/users/profile' }
]
},
{
title: 'Settings',
path: '/settings',
icon: 'SettingsIcon'
}
];
// Get current breadcrumb
const breadcrumb = getBreadcrumb(menuItems);
return (
<div className="p-4 bg-card rounded-md shadow-sm">
<h3 className="text-lg font-medium mb-4">Navigation Menu</h3>
<ul className="space-y-2">
{menuItems.map((item) => (
<li key={item.path}>
<div className={`p-2 rounded ${
isItemActive(item) ? 'bg-primary text-white' : 'hover:bg-muted'
}`}>
{item.title}
</div>
{item.children && (
<ul className="ml-4 mt-2 space-y-2">
{item.children.map((child) => (
<li key={child.path}>
<div className={`p-2 rounded ${
isActive(child.path) ? 'bg-primary text-white' : 'hover:bg-muted'
}`}>
{child.title}
</div>
</li>
))}
</ul>
)}
</li>
))}
</ul>
<div className="mt-6 p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Current Breadcrumb:</p>
<div className="flex items-center mt-2">
{breadcrumb.map((item, index) => (
<div key={item.path} className="flex items-center">
{index > 0 && <span className="mx-2">/</span>}
<span className="font-medium">{item.title}</span>
</div>
))}
</div>
</div>
</div>
);
}
useSliderInput
Manage dual input sliders with synchronized text inputs.
import { useSliderInput } from '@/hooks/use-slider-input';
import { Slider } from '@/components/ui/slider';
function PriceRangeFilter() {
const {
sliderValues,
inputValues,
handleSliderChange,
handleInputChange,
validateAndUpdateValue
} = useSliderInput({
minValue: 0,
maxValue: 1000,
initialValue: [100, 500]
});
return (
<div className="p-4 bg-card rounded-md shadow-sm">
<h3 className="text-lg font-medium mb-4">Price Range Filter</h3>
<div className="space-y-6">
<Slider
value={sliderValues}
min={0}
max={1000}
step={10}
onValueChange={handleSliderChange}
className="my-6"
/>
<div className="flex items-center justify-between gap-4">
<div className="flex-1">
<label className="text-sm text-muted-foreground mb-1 block">Min Price</label>
<input
type="number"
value={inputValues[0]}
onChange={(e) => handleInputChange(e, 0)}
onBlur={(e) => validateAndUpdateValue(parseFloat(e.target.value), 0)}
className="w-full p-2 border rounded-md"
/>
</div>
<div className="flex-1">
<label className="text-sm text-muted-foreground mb-1 block">Max Price</label>
<input
type="number"
value={inputValues[1]}
onChange={(e) => handleInputChange(e, 1)}
onBlur={(e) => validateAndUpdateValue(parseFloat(e.target.value), 1)}
className="w-full p-2 border rounded-md"
/>
</div>
</div>
<div className="p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Selected Range:</p>
<p className="text-lg font-medium">${sliderValues[0]} - ${sliderValues[1]}</p>
</div>
</div>
</div>
);
}
useMounted
Check if a component has mounted, useful for avoiding hydration issues with SSR.
import { useMounted } from '@/hooks/use-mounted';
import { useTheme } from 'next-themes';
function ThemeAwareComponent() {
const mounted = useMounted();
const { theme, resolvedTheme } = useTheme();
// Avoid hydration mismatch by not rendering until mounted
if (!mounted) {
return <div className="h-10 w-10 bg-muted rounded animate-pulse" />;
}
return (
<div className="p-4 bg-card rounded-md shadow-sm">
<h3 className="text-lg font-medium mb-2">Theme Information</h3>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Theme Setting</p>
<p className="text-xl font-semibold">{theme}</p>
</div>
<div className="p-3 bg-muted rounded">
<p className="text-sm text-muted-foreground">Resolved Theme</p>
<p className="text-xl font-semibold">{resolvedTheme}</p>
</div>
</div>
</div>
);
}