Claude-skill-registry bellog-hooks
Provides custom React hooks patterns and best practices specific to Bellog. Triggers when creating custom hooks or implementing interactive features.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/bellog-hooks" ~/.claude/skills/majiayu000-claude-skill-registry-bellog-hooks && rm -rf "$T"
manifest:
skills/data/bellog-hooks/SKILL.mdsource content
Bellog Hook Patterns
This skill defines the patterns and best practices for creating custom React hooks in the Bellog blog project.
Hook Location
All custom hooks:
/src/hooks/
Naming convention:
use[Feature].ts (camelCase)
Core Hook Patterns
Bellog uses three main patterns:
- Scroll-based hooks - Track scroll position and direction
- Observer-based hooks - Use IntersectionObserver for viewport detection
- Content processing hooks - Parse and transform data
Pattern 1: Scroll-Based Hooks
Example:
useScrollSpy.ts
Structure
import { useState, useEffect, useRef, useCallback } from 'react'; export function useScrollSpy() { // 1. State with useRef for position tracking const [activeId, setActiveId] = useState<string>(""); const positionsRef = useRef<Map<string, number>>(new Map()); // 2. Handler with useCallback for optimization const handleScroll = useCallback(() => { const scrollPosition = window.scrollY + OFFSET; // Find active section logic let active = ""; positionsRef.current.forEach((position, id) => { if (scrollPosition >= position) { active = id; } }); setActiveId(active); }, []); // 3. Effect with cleanup useEffect(() => { // Passive listener for better performance window.addEventListener('scroll', handleScroll, { passive: true }); // Initial call handleScroll(); // Cleanup return () => { window.removeEventListener('scroll', handleScroll); }; }, [handleScroll]); return { activeId, setPositions: (positions) => { positionsRef.current = positions; }}; }
Key Features
- useRef for positions - Avoids re-renders on position updates
- useCallback - Prevents handler recreation
- Passive listener - Better scroll performance
- Cleanup - Remove listeners to prevent memory leaks
Constants Import
import { HEADER_OFFSET, SCROLL_SPY_OFFSET } from '@/constants/ui';
Use these constants instead of magic numbers.
Pattern 2: Observer-Based Hooks
Example:
useTocObserver.ts
Structure
import { useEffect, useState, useRef } from 'react'; export function useTocObserver() { const [activeId, setActiveId] = useState<string>(""); const observerRef = useRef<IntersectionObserver | null>(null); useEffect(() => { // 1. Create observer observerRef.current = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setActiveId(entry.target.id); } }); }, { rootMargin: '-80px 0px -80% 0px', // Adjust for header threshold: 0 } ); // 2. Observe elements const headings = document.querySelectorAll('h2, h3'); headings.forEach(heading => { observerRef.current?.observe(heading); }); // 3. Cleanup: unobserve and disconnect return () => { headings.forEach(heading => { observerRef.current?.unobserve(heading); }); observerRef.current?.disconnect(); }; }, []); // Dependencies return activeId; }
Key Features
- IntersectionObserver - Efficient viewport detection
- useRef for observer - Stable reference across renders
- Proper cleanup - unobserve + disconnect
- rootMargin - Account for fixed headers
Pattern 3: Content Processing Hooks
Example:
useHeadings.ts
Structure
import { useEffect, useState } from 'react'; interface Heading { id: string; text: string; level: number; } export function useHeadings() { const [headings, setHeadings] = useState<Heading[]>([]); useEffect(() => { // 1. Extract headings from DOM const elements = document.querySelectorAll('h2, h3'); // 2. Process into structured data const processedHeadings = Array.from(elements).map(el => ({ id: el.id, text: el.textContent || '', level: parseInt(el.tagName[1]) })); // 3. Update state setHeadings(processedHeadings); // 4. Handle empty state if (processedHeadings.length === 0) { console.warn('No headings found'); } }, []); // Re-run only on mount return headings; }
Key Features
- DOM querying - Extract content from rendered HTML
- Data transformation - Convert to usable structure
- Empty state handling - Graceful degradation
- Type safety - Explicit return type
Custom Hook Template
Use this template for new hooks:
import { useState, useEffect, useRef, useCallback } from 'react'; /** * Hook description: What it does and when to use it * * @example * const value = useCustomHook(); */ export function useCustomHook() { // 1. State declarations const [state, setState] = useState<Type>(initialValue); // 2. Refs (for values that don't cause re-renders) const refValue = useRef<Type>(initialValue); // 3. Callbacks (for stable function references) const handleEvent = useCallback(() => { // Event handling logic }, [/* dependencies */]); // 4. Effects (side effects, subscriptions) useEffect(() => { // Setup // ... // Cleanup return () => { // Cleanup logic }; }, [/* dependencies */]); // 5. Return value (keep API minimal) return state; // or return { state, setState, handleEvent }; }
Hook Best Practices
1. Naming
// ✅ Correct useScrollSpy useTocObserver useScrollPosition useMediaQuery // ❌ Wrong scrollSpy ObserverHook scrollPositionHook
2. Return Values
// ✅ Single value when simple return activeId; // ✅ Object when multiple values return { activeId, setActiveId, isScrolling }; // ❌ Too many values return { value1, value2, value3, value4, value5 }; // Too complex
3. Dependencies
// ✅ Correct dependencies useEffect(() => { doSomething(value); }, [value]); // value is used // ❌ Missing dependencies useEffect(() => { doSomething(value); }, []); // value not in deps! // ✅ ESLint exhaustive-deps will catch this
4. Cleanup
// ✅ Always cleanup event listeners useEffect(() => { window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [handleScroll]); // ✅ Always cleanup observers useEffect(() => { const observer = new IntersectionObserver(...); // observe elements return () => observer.disconnect(); }, []); // ✅ Always cleanup timers useEffect(() => { const timer = setTimeout(...); return () => clearTimeout(timer); }, []);
5. Performance
// ✅ Use passive listeners for scroll/touch { passive: true } // ✅ Use useCallback for stable references const handler = useCallback(() => {...}, [deps]); // ✅ Use useRef to avoid re-renders const ref = useRef(value); // ✅ Debounce/throttle expensive operations const debouncedHandler = useMemo( () => debounce(handler, 100), [handler] );
Common Hook Patterns
Scroll Position Hook
export function useScrollPosition() { const [scrollY, setScrollY] = useState(0); useEffect(() => { const handleScroll = () => { setScrollY(window.scrollY); }; window.addEventListener('scroll', handleScroll, { passive: true }); handleScroll(); // Initial value return () => window.removeEventListener('scroll', handleScroll); }, []); return scrollY; }
Scroll Direction Hook
export function useScrollDirection() { const [scrollDirection, setScrollDirection] = useState<'up' | 'down'>('up'); const lastScrollY = useRef(0); useEffect(() => { const handleScroll = () => { const currentScrollY = window.scrollY; if (currentScrollY > lastScrollY.current) { setScrollDirection('down'); } else if (currentScrollY < lastScrollY.current) { setScrollDirection('up'); } lastScrollY.current = currentScrollY; }; window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); }, []); return scrollDirection; }
Media Query Hook
export function useMediaQuery(query: string) { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); setMatches(media.matches); const listener = (e: MediaQueryListEvent) => { setMatches(e.matches); }; media.addEventListener('change', listener); return () => media.removeEventListener('change', listener); }, [query]); return matches; } // Usage const isMobile = useMediaQuery('(max-width: 768px)');
Debounce Hook
export function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(handler); }, [value, delay]); return debouncedValue; } // Usage const debouncedSearch = useDebounce(searchTerm, 300);
Previous Value Hook
export function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>(); useEffect(() => { ref.current = value; }, [value]); return ref.current; }
Integration with Components
Usage Pattern
"use client"; import { useScrollSpy } from '@/hooks/useScrollSpy'; import { HEADER_OFFSET } from '@/constants/ui'; export function TableOfContents({ headings }: Props) { // 1. Use the hook const { activeId, setPositions } = useScrollSpy(); // 2. Update positions when headings change useEffect(() => { const positions = new Map(); headings.forEach(heading => { const element = document.getElementById(heading.id); if (element) { positions.set(heading.id, element.offsetTop - HEADER_OFFSET); } }); setPositions(positions); }, [headings, setPositions]); // 3. Use the returned value return ( <nav> {headings.map(heading => ( <a key={heading.id} className={activeId === heading.id ? 'active' : ''} > {heading.text} </a> ))} </nav> ); }
TypeScript Patterns
Generic Hooks
export function useLocalStorage<T>( key: string, initialValue: T ): [T, (value: T) => void] { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } }); const setValue = (value: T) => { setStoredValue(value); window.localStorage.setItem(key, JSON.stringify(value)); }; return [storedValue, setValue]; }
Return Type Inference
// ✅ Let TypeScript infer when possible export function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = () => setValue(v => !v); return [value, toggle] as const; // as const for tuple } // Result: [boolean, () => void]
Hook Testing Checklist
- Hook is pure (same inputs → same outputs)
- All dependencies in useEffect arrays
- Cleanup functions defined where needed
- Event listeners use { passive: true } for performance
- useCallback used for stable function references
- useRef used for values that don't need re-renders
- Type safety: explicit return type or inferred correctly
- JSDoc comments for complex hooks
- Constants imported from @/constants/ui
- File named use[Feature].ts in /src/hooks/
Common Mistakes
Mistake 1: Missing Cleanup
// ❌ Wrong useEffect(() => { window.addEventListener('scroll', handleScroll); }, []); // Missing cleanup! // ✅ Correct useEffect(() => { window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []);
Mistake 2: Incorrect Dependencies
// ❌ Wrong useEffect(() => { fetchData(id); }, []); // id should be in deps! // ✅ Correct useEffect(() => { fetchData(id); }, [id]);
Mistake 3: Using State Instead of Ref
// ❌ Wrong (causes unnecessary re-renders) const [lastScrollY, setLastScrollY] = useState(0); // ✅ Correct (no re-renders) const lastScrollY = useRef(0);
Quick Reference
// Scroll-based pattern const [value, setValue] = useState(initial); const handleScroll = useCallback(() => {...}, []); useEffect(() => { window.addEventListener('scroll', handleScroll, { passive: true }); return () => window.removeEventListener('scroll', handleScroll); }, [handleScroll]); // Observer-based pattern const observerRef = useRef<IntersectionObserver | null>(null); useEffect(() => { observerRef.current = new IntersectionObserver(...); // observe elements return () => observerRef.current?.disconnect(); }, []); // Processing pattern const [data, setData] = useState<Type[]>([]); useEffect(() => { const processed = processData(); setData(processed); }, [dependency]);
Remember: Hooks are for reusable logic. If it's only used once, consider keeping it in the component.