Skip to content

Custom Hooks Reference

Complete reference for all custom React hooks in the Constellation project.

Hook Organization

All custom hooks are located in src/hooks/ with the naming convention use-*.tsx or use-*.ts.


Authentication Hooks

useIsAdmin

Check if current user is an admin.

import { useIsAdmin } from '@/src/hooks/use-is-admin';

export function AdminOnlyComponent() {
  const isAdmin = useIsAdmin();

  if (!isAdmin) return null;

  return <div>Admin content</div>;
}

Returns: boolean

Usage: Protect admin-only features and routes

useRequireAdmin

Redirect to login if not admin.

import { useRequireAdmin } from '@/src/hooks/use-require-admin';

export function AdminPanel() {
  useRequireAdmin(); // Redirects if not admin

  return <div>Admin panel</div>;
}

Returns: void

Side Effect: Redirects user if not authorized


UI/UX Hooks

useMobile

Detect if viewport is mobile size.

import { useMobile } from '@/src/hooks/use-mobile';

export function ResponsiveComponent() {
  const isMobile = useMobile();

  if (isMobile) {
    return <MobileLayout />;
  }

  return <DesktopLayout />;
}

Returns: boolean

Breakpoint: < 768px (Tailwind md breakpoint)

useIsTouchDevice

Detect if device supports touch.

import { useIsTouchDevice } from '@/src/hooks/use-is-touch-device';

export function InteractiveElement() {
  const isTouchDevice = useIsTouchDevice();

  return (
    <button className={isTouchDevice ? 'large' : 'normal'}>
      {isTouchDevice ? 'Tap' : 'Click'} me
    </button>
  );
}

Returns: boolean

Use Case: Adjust UI for touch interfaces

useMounted

Check if component is mounted (SSR safe).

import { useMounted } from '@/src/hooks/use-mounted';

export function ClientOnlyContent() {
  const mounted = useMounted();

  if (!mounted) return null; // Don't render on server

  return <div>Client-only content</div>;
}

Returns: boolean

Use Case: Prevent hydration mismatch


Utility Hooks

useDebounce

Debounce a value.

import { useDebounce } from '@/src/hooks/use-debounce';

export function SearchUsers({ onSearch }: Props) {
  const [search, setSearch] = useState('');
  const debouncedSearch = useDebounce(search, 500); // 500ms delay

  useEffect(() => {
    onSearch(debouncedSearch);
  }, [debouncedSearch]);

  return (
    <input
      value={search}
      onChange={(e) => setSearch(e.target.value)}
      placeholder="Search..."
    />
  );
}

Parameters: - value - Value to debounce - delay - Debounce delay in ms (default: 500)

Returns: T - Debounced value

Use Case: Search inputs, API calls on input

useCopyToClipboard

Copy text to clipboard.

import { useCopyToClipboard } from '@/src/hooks/use-copy-to-clipboard';

export function ShareLink({ url }: Props) {
  const { copy, copied } = useCopyToClipboard();

  return (
    <button onClick={() => copy(url)}>
      {copied ? 'Copied!' : 'Copy Link'}
    </button>
  );
}

Methods: - copy(text: string) - Copy text to clipboard

State: - copied: boolean - Whether copy was successful

Use Case: Copy links, share buttons, code snippets


Workspace Hooks

useWorkspace

Get current workspace data.

import { useWorkspace } from '@/src/hooks/use-workspace';

export function WorkspaceEditor() {
  const { workspace, loading, error } = useWorkspace();

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      <h1>{workspace.name}</h1>
      <Editor content={workspace.content} />
    </div>
  );
}

Returns:

{
  workspace: Workspace | null;
  loading: boolean;
  error: Error | null;
}

Use Case: Fetch and display workspace data


Chat Hooks

useCustomChat

Manage chat state and messages.

import { useCustomChat } from '@/src/hooks/use-custom-chat';

export function ChatComponent() {
  const { messages, sendMessage, loading } = useCustomChat();

  const handleSend = async (text: string) => {
    await sendMessage(text);
  };

  return (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.content}</div>
      ))}
      <ChatInput onSend={handleSend} />
    </div>
  );
}

Returns:

{
  messages: ChatMessage[];
  sendMessage: (text: string) => Promise<void>;
  loading: boolean;
  error: Error | null;
}

Use Case: Chatbot integration, real-time messaging


Collaboration Hooks

useYjs

Access Yjs collaborative data structures.

import { useYjs } from '@/src/hooks/use-yjs';

export function CollaborativeEditor() {
  const { yText, yMap, connected } = useYjs('document');

  if (!connected) return <div>Connecting...</div>;

  return (
    <Editor
      text={yText.toString()}
      onChange={(newText) => {
        yText.delete(0, yText.length);
        yText.insert(0, newText);
      }}
    />
  );
}

Returns:

{
  yText: Y.Text;      // Collaborative text
  yMap: Y.Map;        // Collaborative map
  connected: boolean;
  awareness: Awareness; // User awareness
}

Use Case: Real-time collaborative editing


Admin Hooks

useAdminPing

Check admin status with periodic refresh.

import { useAdminPing } from '@/src/hooks/use-admin-ping';

export function AdminStatus() {
  const { isAdmin, refreshing } = useAdminPing();

  return (
    <div>
      Admin: {isAdmin ? 'Yes' : 'No'}
      {refreshing && ' (checking...)'}
    </div>
  );
}

Returns:

{
  isAdmin: boolean;
  refreshing: boolean;
  refresh: () => Promise<void>;
}

Use Case: Periodic admin status checks


Language Hooks

useLanguageChange

Handle language switching.

import { useLanguageChange } from '@/src/hooks/use-language-change';

export function LanguageSwitcher() {
  const { currentLanguage, setLanguage } = useLanguageChange();

  return (
    <select value={currentLanguage} onChange={(e) => setLanguage(e.target.value)}>
      <option value="en">English</option>
      <option value="fr">FranΓ§ais</option>
    </select>
  );
}

Returns:

{
  currentLanguage: string;
  setLanguage: (lang: string) => void;
}

Use Case: Language selection UI


Node Lookup Hook

useNodeByLabel

Find workspace nodes by label.

import { useNodeByLabel } from '@/src/hooks/use-node-by-label';

export function NodeSearch({ label }: Props) {
  const { node, loading, error } = useNodeByLabel(label);

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return <NodeDetails node={node} />;
}

Parameters: - label: string - Node label to search for

Returns:

{
  node: Node | null;
  loading: boolean;
  error: Error | null;
}

Use Case: Graph node lookup and navigation


Creating Custom Hooks

Hook Template

// src/hooks/use-my-hook.ts
import { useEffect, useState, useCallback } from 'react';

interface UseMyHookReturn {
  data: string | null;
  loading: boolean;
  error: Error | null;
  refetch: () => Promise<void>;
}

/**
 * Custom hook description
 * @param param - Parameter description
 * @returns Hook state and methods
 * @example
 * const { data, loading } = useMyHook('value');
 */
export function useMyHook(param: string): UseMyHookReturn {
  const [data, setData] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const result = await someAsyncOperation(param);
      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Unknown error'));
    } finally {
      setLoading(false);
    }
  }, [param]);

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return { data, loading, error, refetch: fetchData };
}

Hook Test Template

// src/hooks/use-my-hook.test.ts
import { describe, it, expect } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useMyHook } from './use-my-hook';

describe('useMyHook', () => {
  it('should initialize with loading state', () => {
    const { result } = renderHook(() => useMyHook('test'));
    expect(result.current.loading).toBe(true);
  });

  it('should load data successfully', async () => {
    const { result } = renderHook(() => useMyHook('test'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toBe('expected data');
  });

  it('should handle errors', async () => {
    const { result } = renderHook(() => useMyHook('invalid'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.error).toBeDefined();
  });
});

Hook Best Practices

βœ… Do's

  • βœ… Return state, setters, and methods in an object
  • βœ… Handle loading and error states
  • βœ… Use meaningful names starting with use
  • βœ… Document with JSDoc
  • βœ… Write tests for hooks
  • βœ… Clean up side effects with return function
  • βœ… Memoize callbacks with useCallback
  • βœ… Use useMemo for expensive computations

❌ Don'ts

  • ❌ Don't call hooks conditionally
  • ❌ Don't use hooks outside React components
  • ❌ Don't forget dependencies in useEffect
  • ❌ Don't make hooks too complex
  • ❌ Don't forget loading/error states
  • ❌ Don't forget cleanup functions
  • ❌ Don't use unnecessary state
  • ❌ Don't forget to export hooks

Hook Usage Index

Hook Purpose Import
useIsAdmin Check admin status @/src/hooks/use-is-admin
useRequireAdmin Require admin or redirect @/src/hooks/use-require-admin
useMobile Detect mobile viewport @/src/hooks/use-mobile
useIsTouchDevice Detect touch support @/src/hooks/use-is-touch-device
useMounted Check if mounted (SSR) @/src/hooks/use-mounted
useDebounce Debounce value @/src/hooks/use-debounce
useCopyToClipboard Copy to clipboard @/src/hooks/use-copy-to-clipboard
useWorkspace Get workspace data @/src/hooks/use-workspace
useCustomChat Chat state management @/src/hooks/use-custom-chat
useYjs Collaborative editing @/src/hooks/use-yjs
useAdminPing Admin status check @/src/hooks/use-admin-ping
useLanguageChange Language switching @/src/hooks/use-language-change
useNodeByLabel Graph node lookup @/src/hooks/use-node-by-label


Last Updated: January 2026