---
globs:
  - "packages/hermes-app/src/hooks/**/*.ts"
  - "packages/hermes-app/src/hooks/**/*.tsx"
---

# Hermes App - Custom Hooks Rules

## Hook Naming

- **MUST** start with `use` prefix: `useDownloadActions`, `useTheme`, `useApiKeys`
- File name must match hook name: `useTheme.ts` exports `useTheme`
- Export as named export, not default
- Use descriptive names that indicate purpose

## Hook Structure

### Basic Template
```typescript
interface UseFeatureOptions {
  initialValue?: string;
  onSuccess?: (data: Data) => void;
}

interface UseFeatureReturn {
  data: Data | null;
  isLoading: boolean;
  error: Error | null;
  actions: {
    fetch: () => Promise<void>;
    reset: () => void;
  };
}

/**
 * @example
 * const { data, isLoading, actions } = useFeature({ initialValue: 'test' });
 */
export function useFeature(options: UseFeatureOptions = {}): UseFeatureReturn {
  const [data, setData] = useState<Data | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  return { data, isLoading, error, actions: { fetch, reset } };
}
```

### TypeScript Requirements
- Define explicit return types
- Create interfaces for options and return values
- Avoid `any` type

## Hook Patterns

### Data Fetching (TanStack Query)
```typescript
export function useApiKeys() {
  const queryClient = useQueryClient();

  const { data: apiKeys, isLoading, error } = useQuery({
    queryKey: ["apiKeys"],
    queryFn: fetchApiKeys,
  });

  const createMutation = useMutation({
    mutationFn: createApiKey,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["apiKeys"] });
    },
  });

  return {
    apiKeys: apiKeys ?? [],
    isLoading,
    error,
    createApiKey: createMutation.mutateAsync,
    isCreating: createMutation.isPending,
  };
}
```

### Action Hook
```typescript
export function useDownloadActions() {
  const queryClient = useQueryClient();

  const pauseMutation = useMutation({
    mutationFn: pauseDownload,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["downloads"] });
      toast.success("Download paused");
    },
    onError: (error) => toast.error(`Failed: ${error.message}`),
  });

  const pause = useCallback(
    (id: string) => pauseMutation.mutateAsync(id),
    [pauseMutation]
  );

  return { pause, isPausing: pauseMutation.isPending };
}
```

### Utility Hook
```typescript
export function useDebounce<T>(value: T, delay: number = 500): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(handler);
  }, [value, delay]);

  return debouncedValue;
}
```

### Context Hook
```typescript
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;
}
```

## TanStack Query Integration

### Query Keys
Use hierarchical, descriptive keys with parameters:

```typescript
// ✅ Good
queryKey: ["downloads", "list", { status: "active" }]
queryKey: ["downloads", "detail", downloadId]

// ❌ Bad
queryKey: ["data"]
queryKey: ["downloads"]  // Too generic
```

### Mutations
- Invalidate queries after successful mutations
- Handle optimistic updates when appropriate
- Show toast notifications for user feedback

```typescript
const mutation = useMutation({
  mutationFn: updateItem,
  onMutate: async (newItem) => {
    await queryClient.cancelQueries({ queryKey: ["items"] });
    const prev = queryClient.getQueryData(["items"]);
    queryClient.setQueryData(["items"], (old: Item[]) =>
      old.map((item) => (item.id === newItem.id ? newItem : item))
    );
    return { prev };
  },
  onError: (err, newItem, context) => {
    queryClient.setQueryData(["items"], context?.prev);
    toast.error("Update failed");
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ["items"] });
  },
});
```

## Hook Composition

Compose smaller hooks into larger ones:

```typescript
export function useQueueData() {
  const { downloads, isLoading: isLoadingDownloads } = useDownloads();
  const { stats, isLoading: isLoadingStats } = useStats();
  const { pause, resume, cancel } = useDownloadActions();
  const filters = useFilters();

  const filteredDownloads = useMemo(
    () => applyFilters(downloads, filters.active),
    [downloads, filters.active]
  );

  return {
    downloads: filteredDownloads,
    stats,
    isLoading: isLoadingDownloads || isLoadingStats,
    actions: { pause, resume, cancel },
    filters,
  };
}
```

## Side Effects

### Dependencies
- Always declare all dependencies
- Use ESLint to catch missing dependencies
- Extract stable references with `useCallback`

### Cleanup
- Return cleanup function from effects
- Cancel pending requests
- Clear timers and subscriptions

```typescript
useEffect(() => {
  const controller = new AbortController();

  async function fetchData() {
    try {
      const data = await fetch(url, { signal: controller.signal });
      setData(data);
    } catch (error) {
      if (error.name !== "AbortError") setError(error);
    }
  }

  fetchData();
  return () => controller.abort();
}, [url]);
```

## Performance

### Memoization
- Use `useCallback` for function references passed as props
- Use `useMemo` for expensive computations
- Don't over-optimize - profile first

```typescript
export function useFilters() {
  const [filters, setFilters] = useState<Filter[]>([]);

  const activeFilters = useMemo(
    () => filters.filter((f) => f.active),
    [filters]
  );

  const addFilter = useCallback((filter: Filter) => {
    setFilters((prev) => [...prev, filter]);
  }, []);

  return { filters, activeFilters, addFilter };
}
```

## Error Handling

```typescript
export function useVideoInfo(url: string) {
  const [info, setInfo] = useState<VideoInfo | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  const fetchInfo = useCallback(async () => {
    if (!url) return;

    setIsLoading(true);
    setError(null);

    try {
      const data = await api.getVideoInfo(url);
      setInfo(data);
    } catch (err) {
      const error = err instanceof Error ? err : new Error("Unknown error");
      setError(error);
      toast.error(`Failed: ${error.message}`);
    } finally {
      setIsLoading(false);
    }
  }, [url]);

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

  return { info, error, isLoading, refetch: fetchInfo };
}
```

