Metronic React
Getting Started

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.

HookPurposePath
useViewportMonitor viewport dimensionssrc/hooks/use-viewport.ts
useIsMobileDetect mobile viewportsrc/hooks/use-mobile.tsx
useScrollPositionMonitor scroll positionsrc/hooks/use-scroll-position.ts
useBodyClassAdd/remove body classessrc/hooks/use-body-class.ts
useCopyToClipboardClipboard functionalitysrc/hooks/use-copy-to-clipboard.ts
useMenuNavigation menu statesrc/hooks/use-menu.ts
useSliderInputSlider with inputssrc/hooks/use-slider-input.ts
useMountedComponent mount detectionsrc/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>
  );
}