Skip to main content

Custom React Hooks

Meelio provides a collection of custom React hooks for common UI patterns and functionality.

Hook Location

All hooks are located in:
packages/shared/src/hooks/
All hooks are exported through packages/shared/src/hooks/index.ts:
export * from "./use-disclosure";
export * from "./use-interval";
export * from "./use-mobile";
export * from "./use-mounted";
export * from "./use-oscillation";
export * from "./use-previous";
export * from "./use-timer";
export * from "./use-document-title";
export * from "./use-wallpaper-search";
export * from "./use-debounced-value";
export * from "./use-dock-shortcuts";

Available Hooks

useDisclosure

File: use-disclosure.tsManages the open/close state of UI elements like modals, dropdowns, and drawers.

Signature

function useDisclosure(initial?: boolean): {
  isOpen: boolean;
  open: () => void;
  close: () => void;
  toggle: () => void;
}

Parameters

initial
boolean
default:"false"
Initial open state of the disclosure

Returns

isOpen
boolean
Current open state
open
() => void
Function to open the element
close
() => void
Function to close the element
toggle
() => void
Function to toggle the open state

Usage Example

import { useDisclosure } from '@meelio/shared';

function SettingsDialog() {
  const { isOpen, open, close, toggle } = useDisclosure();

  return (
    <>
      <button onClick={open}>Open Settings</button>
      <Dialog isOpen={isOpen} onClose={close}>
        <h2>Settings</h2>
        {/* Dialog content */}
      </Dialog>
    </>
  );
}
This hook is commonly used throughout Meelio for managing modal dialogs, sheets, and expandable sections.
File: use-debounced-value.tsDebounces a value to reduce the frequency of updates, useful for search inputs and expensive operations.

Signature

function useDebouncedValue<T>(
  value: T,
  options: { delay: number }
): T

Parameters

value
T
The value to debounce
options.delay
number
Delay in milliseconds before updating the debounced value

Returns

Returns the debounced value that updates after the specified delay.

Usage Example

import { useDebouncedValue } from '@meelio/shared';

function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearch = useDebouncedValue(searchTerm, { delay: 300 });

  useEffect(() => {
    // Only perform search after user stops typing for 300ms
    if (debouncedSearch) {
      performSearch(debouncedSearch);
    }
  }, [debouncedSearch]);

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}
File: use-interval.tsDeclarative interval hook that handles cleanup automatically.

Signature

function useInterval(
  callback: () => void,
  delay: number | null
): void

Parameters

callback
() => void
Function to execute on each interval
delay
number | null
Delay in milliseconds between executions. Pass null to pause the interval

Usage Example

import { useInterval } from '@meelio/shared';

function CountdownTimer({ duration }: { duration: number }) {
  const [remaining, setRemaining] = useState(duration);
  const [isRunning, setIsRunning] = useState(false);

  useInterval(
    () => {
      setRemaining((prev) => {
        if (prev <= 1) {
          setIsRunning(false);
          return 0;
        }
        return prev - 1;
      });
    },
    isRunning ? 1000 : null
  );

  return (
    <div>
      <p>{remaining} seconds remaining</p>
      <button onClick={() => setIsRunning(!isRunning)}>
        {isRunning ? 'Pause' : 'Start'}
      </button>
    </div>
  );
}
This hook automatically handles cleanup and updates when the callback changes, making it safer than using setInterval directly.
File: use-oscillation.tsCreates smooth oscillating values for animations, particularly for soundscape volume oscillation.

Signature

function useOscillation(
  initialVolume: number,
  isOscillating: boolean
): {
  targetVolume: number;
  lerpValue: (v0: number, v1: number, t: number) => number;
}

Parameters

initialVolume
number
Starting volume value (0-1)
isOscillating
boolean
Whether oscillation is active

Returns

targetVolume
number
Current oscillating volume value
lerpValue
function
Linear interpolation function for smooth transitions
(v0: number, v1: number, t: number) => number

Usage Example

import { useOscillation } from '@meelio/shared';

function OscillatingSound({ baseVolume }: { baseVolume: number }) {
  const [isOscillating, setIsOscillating] = useState(false);
  const { targetVolume, lerpValue } = useOscillation(baseVolume, isOscillating);

  useEffect(() => {
    // Apply the oscillating volume to audio element
    audioRef.current.volume = isOscillating
      ? targetVolume
      : baseVolume;
  }, [targetVolume, isOscillating, baseVolume]);

  return (
    <button onClick={() => setIsOscillating(!isOscillating)}>
      {isOscillating ? 'Stop' : 'Start'} Oscillation
    </button>
  );
}
The hook uses requestAnimationFrame for smooth 60fps animations and includes randomized frequency and phase for natural-sounding volume changes.
File: use-mobile.tsDetects if the current viewport is mobile-sized (< 768px).

Signature

function useIsMobile(): boolean

Returns

Returns true if viewport width is less than 768px.

Usage Example

import { useIsMobile } from '@meelio/shared';

function ResponsiveNav() {
  const isMobile = useIsMobile();

  return (
    <nav>
      {isMobile ? <MobileMenu /> : <DesktopMenu />}
    </nav>
  );
}
The hook listens to window resize events and updates reactively. The mobile breakpoint is set at 768px.
File: use-document-title.tsUpdates the browser tab title to reflect timer state.

Signature

interface DocumentTitleProps {
  remaining: number;  // Seconds remaining
  stage: TimerStage;  // Focus or Break
  running: boolean;   // Is timer running
}

function useDocumentTitle(props: DocumentTitleProps): void

Parameters

remaining
number
Seconds remaining in current timer stage
stage
TimerStage
Current timer stage (Focus or Break)
running
boolean
Whether the timer is currently running

Usage Example

import { useDocumentTitle, TimerStage } from '@meelio/shared';

function Timer() {
  const { stage, isRunning, /* ... */ } = useTimerStore()();
  const remaining = useTimerRemaining();

  useDocumentTitle({
    remaining,
    stage,
    running: isRunning
  });

  return <div>{/* Timer UI */}</div>;
}
The hook formats the title as:
  • Running: 🎯 25:00 - Focus or ☕ 5:00 - Break
  • Paused: Meelio - focus, calm, & productivity
File: use-confetti.tsTriggers confetti animation effect.

Signature

function useConfetti(): () => Promise<void>

Returns

Returns an async function that triggers the confetti animation.

Usage Example

import { useConfetti } from '@meelio/shared';

function TaskItem({ task }: { task: Task }) {
  const confetti = useConfetti();
  const { toggleTask } = useTaskStore();

  const handleToggle = async () => {
    await toggleTask(task.id);
    if (!task.completed) {
      await confetti();
    }
  };

  return (
    <div onClick={handleToggle}>
      {task.title}
    </div>
  );
}
The confetti animation uses the canvas-confetti library, which is dynamically imported for better performance.
File: use-cached-sound-url.tsManages sound file URLs with caching for soundscapes.

Signature

function useCachedSoundUrl(soundUrl: string): string | null

Parameters

soundUrl
string
The sound file URL to cache

Returns

Returns the cached URL or null if not yet loaded.

Usage Example

import { useCachedSoundUrl } from '@meelio/shared';

function SoundPlayer({ sound }: { sound: Sound }) {
  const cachedUrl = useCachedSoundUrl(sound.url);
  const audioRef = useRef<HTMLAudioElement>(null);

  useEffect(() => {
    if (cachedUrl && audioRef.current) {
      audioRef.current.src = cachedUrl;
    }
  }, [cachedUrl]);

  return <audio ref={audioRef} />;
}
File: use-mounted.tsTracks whether a component has mounted.

Signature

function useMounted(): boolean

Returns

Returns true after the component has mounted, false before.

Usage Example

import { useMounted } from '@meelio/shared';

function HydratedComponent() {
  const isMounted = useMounted();

  if (!isMounted) {
    return <Skeleton />;
  }

  return <ActualContent />;
}
Useful for preventing hydration mismatches in SSR scenarios and delaying client-only code.
File: use-previous.tsStores the previous value of a variable.

Signature

function usePrevious<T>(value: T): T | undefined

Parameters

value
T
The value to track

Returns

Returns the previous value from the last render.

Usage Example

import { usePrevious } from '@meelio/shared';

function Counter({ count }: { count: number }) {
  const prevCount = usePrevious(count);

  return (
    <div>
      <p>Current: {count}</p>
      <p>Previous: {prevCount ?? 'none'}</p>
      <p>Changed: {count !== prevCount ? 'Yes' : 'No'}</p>
    </div>
  );
}
File: use-dock-shortcuts.tsManages keyboard shortcuts for dock actions.

Signature

function useDockShortcuts(handlers: ShortcutHandlers): void

interface ShortcutHandlers {
  onTimerToggle?: () => void;
  onTimerReset?: () => void;
  onTasksOpen?: () => void;
  onNotesOpen?: () => void;
  onSoundscapesOpen?: () => void;
  // ... other handlers
}

Usage Example

import { useDockShortcuts } from '@meelio/shared';

function Dock() {
  const { start, pause, reset } = useTimerStore()();
  const { open: openTasks } = useDisclosure();

  useDockShortcuts({
    onTimerToggle: () => isRunning ? pause() : start(),
    onTimerReset: reset,
    onTasksOpen: openTasks,
  });

  return <div>{/* Dock UI */}</div>;
}
File: use-wallpaper-search.tsSearches for wallpapers from Unsplash API.

Signature

function useWallpaperSearch(): {
  searchResults: Wallpaper[];
  isSearching: boolean;
  searchWallpapers: (query: string) => Promise<void>;
}

Usage Example

import { useWallpaperSearch } from '@meelio/shared';

function WallpaperSearch() {
  const { searchResults, isSearching, searchWallpapers } = useWallpaperSearch();
  const [query, setQuery] = useState('');

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button onClick={() => searchWallpapers(query)}>
        Search
      </button>
      {isSearching && <Spinner />}
      <div>
        {searchResults.map(wallpaper => (
          <WallpaperTile key={wallpaper.id} wallpaper={wallpaper} />
        ))}
      </div>
    </div>
  );
}
File: use-timer.tsTimer-specific hook (currently empty placeholder for future timer utilities).
This file is currently empty but reserved for timer-specific hook logic.

Hook Patterns

Cleanup Handling

Many hooks handle cleanup automatically:
useEffect(() => {
  const id = setInterval(() => callback(), delay);
  return () => clearInterval(id); // Automatic cleanup
}, [delay]);

Memoization

Hooks use useCallback to prevent unnecessary re-renders:
const open = React.useCallback(() => setIsOpen(true), []);

TypeScript Generics

Hooks support generic types for type safety:
function useDebouncedValue<T>(value: T, options: { delay: number }): T

Creating Custom Hooks

When creating new hooks for Meelio:
1

Follow naming convention

Use the use prefix for all hooks (e.g., useMyFeature)
2

Add to index.ts

Export the hook from packages/shared/src/hooks/index.ts
3

Include TypeScript types

Provide full type definitions for parameters and return values
4

Handle cleanup

Always clean up side effects in the return function of useEffect
5

Document usage

Include JSDoc comments with usage examples

Next Steps

Component Overview

Learn about component architecture

State Management

Explore Zustand stores

Build docs developers (and LLMs) love