🪄 Custom Hooks: Building Your Own Magic Spells
Imagine you’re a wizard. React gives you some basic spells like useState (remember things) and useEffect (do things when stuff changes). But what if you could create your own spells that combine these powers? That’s exactly what Custom Hooks are!
🎯 The Big Picture
Think of custom hooks like recipe cards in a kitchen. Instead of remembering every step to make chocolate cake each time, you write it down once. Then anyone can follow your recipe!
graph TD A["🧙 Basic Hooks"] --> B["✨ Custom Hook"] B --> C["📱 Component 1"] B --> D["📱 Component 2"] B --> E["📱 Component 3"]
One recipe. Used everywhere. No copy-pasting!
📜 Hook Rules: The Wizard’s Code
Before creating spells, every wizard must learn The Rules. Break them, and your magic won’t work!
Rule 1: Only Call Hooks at the Top Level
❌ Wrong - Inside a loop:
function Bad() {
for (let i = 0; i < 3; i++) {
useState(i); // BOOM! Broken!
}
}
✅ Right - At the top:
function Good() {
const [count, setCount] = useState(0);
// Now use count in loops
}
Rule 2: Only Call Hooks from React Functions
Hooks work inside:
- React function components
- Custom hooks (starting with
use)
❌ Wrong:
function regularFunction() {
useState(0); // Nope!
}
✅ Right:
function MyComponent() {
useState(0); // Perfect!
}
Why These Rules?
React remembers hooks by the order you call them. It’s like a checklist:
Call 1: useState for name ✓
Call 2: useState for age ✓
Call 3: useEffect for data ✓
If you put hooks in if statements, the order might change. React gets confused!
🔨 Creating Custom Hooks
Here’s the magic formula:
- Name starts with
use(likeuseCounter) - Can use other hooks inside
- Returns whatever you want
Your First Custom Hook
Let’s make a hook that tracks window size:
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener(
'resize', handleResize
);
}, []);
return size;
}
Using it is simple:
function MyApp() {
const { width, height } = useWindowSize();
return <p>Screen: {width} x {height}</p>;
}
That’s it! No copy-pasting. Just use useWindowSize() anywhere!
🧩 Hook Composition: Combining Spells
The real magic happens when hooks use other hooks. It’s like building with LEGO!
graph TD A["useLocalStorage"] --> B["useState"] C["useFetch"] --> D["useState"] C --> E["useEffect"] F["useAuth"] --> A F --> C
Example: A Smart Counter
function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function useCounter(key) {
const [count, setCount] = useLocalStorage(key, 0);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(0);
return { count, increment, decrement, reset };
}
Now useCounter remembers its value even after page refresh!
♻️ Reusable Logic: Write Once, Use Forever
Custom hooks solve the DRY problem (Don’t Repeat Yourself).
Before Custom Hooks 😰
// Component A
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { fetch(url)... }, []);
// Component B - Same code again!
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { fetch(url)... }, []);
After Custom Hooks 😊
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
Now everywhere:
const { data, loading } = useFetch('/api/users');
Clean. Simple. Reusable!
🔍 useDebugValue: Peek Behind the Curtain
When you have many custom hooks, debugging can get tricky. useDebugValue shows helpful labels in React DevTools.
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
// Shows "Online" or "Offline" in DevTools
useDebugValue(isOnline ? 'Online' : 'Offline');
useEffect(() => {
const update = () => setIsOnline(navigator.onLine);
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
};
}, []);
return isOnline;
}
Lazy Formatting (For Heavy Work)
If creating the debug label is slow, pass a function:
useDebugValue(data, d => formatExpensiveData(d));
React only calls this when DevTools is open!
🔗 useSyncExternalStore: Talking to the Outside World
Sometimes you need to read data from outside React—like browser APIs, third-party libraries, or global stores.
useSyncExternalStore is the safe way to do this.
graph LR A["External Store"] -->|subscribe| B["useSyncExternalStore"] B -->|getSnapshot| C["Component"]
Example: Online Status Store
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function getSnapshot() {
return navigator.onLine;
}
function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot);
}
Why Not Just useEffect?
useSyncExternalStore handles tricky timing issues that useEffect can miss, especially with server rendering. It’s bulletproof for external data!
The Three Parts
| Part | What It Does |
|---|---|
subscribe |
Listens for changes |
getSnapshot |
Gets current value |
getServerSnapshot |
Value for server (optional) |
🌟 Putting It All Together
Let’s build a complete hook that does everything:
function useLocalStorageSync(key, initial) {
// Subscribe to storage changes
const subscribe = useCallback((callback) => {
window.addEventListener('storage', callback);
return () => {
window.removeEventListener('storage', callback);
};
}, []);
// Get current value
const getSnapshot = useCallback(() => {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initial;
}, [key, initial]);
// Safe external store sync
const value = useSyncExternalStore(
subscribe,
getSnapshot
);
// Setter function
const setValue = useCallback((newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
window.dispatchEvent(new Event('storage'));
}, [key]);
// Debug label
useDebugValue(value, v => `${key}: ${JSON.stringify(v)}`);
return [value, setValue];
}
This hook:
- ✅ Follows all hook rules
- ✅ Composes multiple hooks
- ✅ Creates reusable logic
- ✅ Uses
useDebugValuefor debugging - ✅ Uses
useSyncExternalStorefor external sync
🎓 Key Takeaways
| Concept | One-Liner |
|---|---|
| Hook Rules | Top-level only, React functions only |
| Custom Hooks | Functions starting with use that use hooks |
| Composition | Hooks can use other hooks |
| Reusable Logic | Write once, use everywhere |
| useDebugValue | Labels for React DevTools |
| useSyncExternalStore | Safe way to read external data |
🚀 You’re Now a Hook Wizard!
You’ve learned to:
- Follow the rules so React trusts you
- Create custom hooks to package your logic
- Compose hooks to build powerful tools
- Share logic across your entire app
- Debug with helpful labels
- Connect safely to external stores
Now go build your own magical hooks! 🧙♂️✨
