← Back to list

bellog-hooks
by whddltjdwhd
Castle Bell's custom blog
⭐ 2🍴 0📅 Jan 18, 2026
SKILL.md
name: bellog-hooks description: Provides custom React hooks patterns and best practices specific to Bellog. Triggers when creating custom hooks or implementing interactive features.
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.
Score
Total Score
55/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
✓最近の活動
1ヶ月以内に更新
+10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
✓タグ
1つ以上のタグが設定されている
+5
Reviews
💬
Reviews coming soon


