Intro – Welcome to Hook Land 🎣**
If you’ve been writing React for a hot minute, you’ve probably met Hooks. If you’re brand new, you’ve at least heard the whispers:
“Hooks are the future.” “Hooks changed everything.” “Hooks cured my cat’s depression.”
Okay, maybe not the last one.
Hooks are one of React’s most important features, and they’re not just a neat API — they’re a different way of thinking about your components. They help you do things like:
- Keep your state and logic tidy
- Avoid prop drilling nightmares
- Reuse code without copy-pasting the same logic everywhere
In this article, we’re not going to dissect every single hook (because you’re smart and there are other articles in this series for that). Instead, we’re going to zoom out and look at the bigger picture:
- What hooks actually are (and aren’t)
- The official rules that keep React happy
- Some “unofficial” rules that’ll keep you happy
By the end, you’ll have the mental models and guardrails to use hooks like a pro — whether you’ve written zero lines of React or thousands.
Table of Contents
- What Hooks Are, Really 🪝
-
Implicit (a.k.a. “Unwritten”) Rules of Hooks 🤫
- Ruleish #1 — Call hooks in the same order every render
- Rule-ish #2 — Custom hooks follow the same rules as built-in hooks
- Rule-ish #3 — Don’t skip effect dependencies
- Rule-ish #4 —
useRef
won’t trigger re-renders - Rule-ish #5 —
useMemo
anduseCallback
aren’t permanent vaults - Rule-ish #6 — State setters replace, not merge
2. What Hooks Are, Really 🪝
Here’s the shortest possible definition:
Hooks are special functions that let you tap into React’s features directly from your component code.
That’s it. No secret handshake required.
When you use a hook, you’re essentially telling React:
- “Hey, I need some state here.” →
useState
- “Hey, I want to do something after this component renders.” →
useEffect
- “Hey, I need to remember this value without re-rendering.” →
useRef
It’s not just state and effects — hooks also handle context, reducers, memoization, and even your own custom reusable logic.
A few quick facts about hooks:
- They’re just functions, but with superpowers granted by React
- They only work inside React’s world (components and other hooks)
- They start with
use
— not because React is picky about naming, but because the linter is (more on that in the rules section)
And here’s the really cool part: you can compose hooks. Meaning:
- Built-in hooks can be combined to create custom hooks that package logic for reuse
- Custom hooks can call other custom hooks, like a little hook inception
Once you get used to hooks, you’ll find yourself naturally splitting your logic into smaller, reusable pieces without even trying.
If you're new to React Hooks or could use a quick refresher, this short video (just a few minutes) provides a high-level overview to help set the stage. It's not a replacement for the article—just a helpful primer that might make the deep dive a bit easier to digest (part of our 1 hour React crash course).
📝 Quick Hook Syntax Primer
Before we dive into the rules, let’s make sure we’re all on the same page about how hooks look in code.
Here are the three hooks you’ll see the most:
useState
— for state
const [count, setCount] = useState(0);
// usage:
setCount(count + 1);
-
count
is the current value -
setCount
is the function to update it
useEffect
— for side effects
useEffect(() => {
console.log("I run after every render");
}, [count]); // runs again if `count` changes
- The first argument is a function (your effect)
- The second argument is a dependency array (when to re-run the effect)
useRef
— for values that don’t cause re-renders
const inputRef = useRef(null);
<input ref={inputRef} />;
-
inputRef.current
holds the value - Updating it does not cause the component to re-render
Those three cover state, effects, and refs. But remember — React has more built-in hooks (like useContext
, useReducer
, useMemo
, useCallback
), and you can write your own custom hooks too.
With that foundation in place, let’s look at the rules that keep React’s hook system working correctly.
3. The Rules of Hooks 📜
Hooks are powerful, but they’re also… let’s say “delicate.” To keep React from losing track of your component’s brain, you need to follow two official rules:
Rule #1 — Only call hooks at the top level
Hooks should live in the main body of your component or custom hook. Not inside if
statements, not inside loops, not inside random functions.
Why? React keeps track of your hooks in the order they’re called. If you start moving them around conditionally, React’s “mental map” gets scrambled and chaos ensues. Think of it like a boarding pass — if your seat number keeps changing mid-flight, things are going to get messy.
Rule #2 — Only call hooks from React functions
That means:
- Function components
- Custom hooks (which are just functions that call hooks)
Hooks won’t work if you call them from regular JavaScript functions, event handlers, or outside of React entirely. React needs to know when your component is rendering so it can hook (pun intended) the right data to it.
These two rules are so important that React has an ESLint plugin (eslint-plugin-react-hooks
) that will literally yell at you if you break them. And trust me, you want it to yell — because breaking these rules leads to bugs that make you question all your life choices.
4. Implicit (a.k.a. “Unwritten”) Rules of Hooks 🤫
React officially gives you two rules for hooks, but the truth is… there are more. They’re just not printed in bold in the docs. These are the street-smart rules — the ones you learn after you’ve broken things a few times and wondered why.
Follow them, and you’ll avoid 90% of the “Why is this doing THAT?” moments.
Rule-ish #1 — Call hooks in the same order every render
Technically this is just Rule #1 in disguise, but it’s worth stating outright:
Every time your component renders, hooks must be called in the exact same sequence as before.
If you swap the order or skip one in certain renders, React’s internal hook “filing system” will get confused, and suddenly your useState
might return the wrong value.
It’s like a vending machine that gives you whatever’s in slot #3 — if you change the order of the snacks, you’re going to get trail mix when you wanted chocolate.
Rule-ish #2 — Custom hooks follow the same rules as built-in hooks
A custom hook is just a function that calls other hooks. If you break the rules inside your custom hook, you’re still breaking the rules.
This means: no conditional useEffect
calls inside your useMyAwesomeHook
unless you like chasing ghost bugs.
Rule-ish #3 — Don’t skip effect dependencies
I know, I know — the temptation is real:
useEffect(() => {
fetchData();
}, []); // "just run once"
But if fetchData
depends on props or state that can change, skipping dependencies will give you stale values and mysterious bugs later.
React’s ESLint rules will warn you for a reason — and that reason is so Future You doesn’t cry in the debugger at 2 AM.
Rule-ish #4 — useRef
won’t trigger re-renders
This one trips up a lot of beginners. When you change myRef.current
, React doesn’t re-render your component.
That’s actually the point — refs are for storing mutable values that don’t trigger updates (like DOM elements, timers, or cached values). If you want the UI to update, you need state.
Rule-ish #5 — useMemo
and useCallback
aren’t permanent vaults
A common misunderstanding is thinking they “lock in” values forever. In reality, React is free to throw away your memoized value and recompute it — especially in concurrent rendering.
They’re performance hints, not contracts. Use them when recomputation is expensive, not just because “it’s the pro thing to do.”
Rule-ish #6 — State setters replace, not merge
Coming from other frameworks (or old-school React classes), you might expect setState
to merge objects. Nope — useState
overwrites the value entirely.
If you want merging behavior, you have to do it yourself:
setUser(prev => ({ ...prev, name: 'Alex' }));
Bottom line:
The official rules keep React from breaking. The implicit rules keep you from breaking React. They’re not enforced by React itself, but ignore them at your own risk.
Let's now focus our attention on Patterns That Apply Across Hooks — because once you know the rules, the next step is learning how to bend them into clean, reusable, chef’s kiss logic.
5. Patterns That Apply Across Hooks 🧩
Hooks aren’t just little utilities you sprinkle in — they’re building blocks. And once you start thinking of them as Lego bricks instead of magic spells, you can build some pretty neat structures.
Here are a few patterns you’ll see (and use) over and over.
Pattern #1 — Custom Hooks for Reusability
Custom hooks are where hooks really shine.
Say you’ve got the same data-fetching logic in multiple components: loading states, error handling, retries, the works. Instead of copy-pasting that mess everywhere, you can pull it into a neat little package:
function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(res => res.json())
.then(data => { if (!ignore) setData(data); })
.catch(err => { if (!ignore) setError(err); });
return () => { ignore = true; };
}, [url]);
return { data, error };
}
Now any component can just do:
const { data, error } = useFetch('/api/tacos');
…and boom — instant taco data.
Pattern #2 — Co-Locate Related State & Effects
If two pieces of state are always updated together, or if an effect depends on a specific piece of state, keep them near each other in the code.
It’s not just about code looking tidy — it’s about making the connection obvious for whoever reads the code next (including Future You).
Bad:
function Search() {
const [query, setQuery] = useState('');
// ...50 lines of unrelated code here...
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/search?q=${query}`)
.then(res => res.json())
.then(setResults);
}, [query]);
}
Better:
function Search() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
fetch(`/search?q=${query}`)
.then(res => res.json())
.then(setResults);
}, [query]);
}
Now it’s clear at a glance: results
come from query
, and the effect ties them together.
Pattern #3 — Split Big Hooks Into Smaller Hooks
If your custom hook feels like it’s juggling multiple unrelated jobs (data fetching, form handling, websocket connections…), it’s probably time to split it.
Why? Smaller hooks are easier to read, test, and reuse.
Too much in one hook:
function useDashboardData() {
// Fetch user
// Fetch notifications
// Connect websocket
// Handle theme
}
Split into smaller hooks:
function useUser() { /* fetch user */ }
function useNotifications() { /* fetch notifications */ }
function useLiveUpdates() { /* websocket */ }
function useDashboardData() {
const user = useUser();
const notifications = useNotifications();
useLiveUpdates();
return { user, notifications };
}
Now each piece is reusable in other components.
Pattern #4 — Hook Composition (Hooks Calling Hooks)
Hooks can (and should!) call other hooks. This is how you compose complex behavior from smaller building blocks.
For example:
function useAuth() {
const [user, setUser] = useState(null);
useEffect(() => {
// pretend API call
setUser({ name: 'Ada Lovelace' });
}, []);
return user;
}
function useUserProfile() {
const user = useAuth();
const profile = useFetch(`/profile/${user?.name}`);
return profile;
}
Here, useUserProfile
uses useAuth
and useFetch
inside — this is perfectly fine and makes your hooks modular.
Pattern #5 — Event Hooks
Sometimes your hook only exists to provide event handlers. For example:
function useKeyPress(targetKey, handler) {
useEffect(() => {
function onKeyDown(e) {
if (e.key === targetKey) handler(e);
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [targetKey, handler]);
}
This hook doesn’t store state — it just wires up (and tears down) an event in a clean, reusable way.
The golden rule with patterns:
If you notice you’re repeating the same hooks and logic in multiple places, stop and ask yourself,
“Could this be a custom hook?”
Nine times out of ten, the answer is yes.
6. Common Pitfalls & How to Avoid Them 🚧
Hooks are awesome… until they’re not.
And when they’re not, it’s usually because we’ve accidentally wandered into one of these classic traps.
Let’s save you the trouble (and therapy bills) by calling them out now.
Pitfall #1 — The Infinite useEffect
Loop 🔁
You add an effect that updates state, but that state is also in the effect’s dependency array.
Result? Boom — render → effect → setState → render → effect → forever.
Example:
useEffect(() => {
setCount(count + 1); // uh-oh
}, [count]);
Fix:
Only update state inside an effect if you really mean to, and understand the dependency implications. Sometimes you need conditional logic inside the effect, or you need to rethink where that state update happens.
Pitfall #2 — Missing Dependencies in Effects
You think: “If I leave this dependency out, it’ll only run once.”
React thinks: “Cool, now you’re using stale data and you’ll never know why it’s wrong.”
Example:
useEffect(() => {
doSomething(user.id);
}, []); // missing user
Fix:
List all dependencies. If the effect is running too often, restructure your logic — don’t just ignore the warning.
Pitfall #3 — Stale Closures 🕰
You grab a variable inside an effect or callback, but it never updates because you didn’t include it as a dependency.
Example:
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // always logs the value from the first render
}, 1000);
return () => clearInterval(interval);
}, []); // count is missing here
Fix:
Understand that functions in hooks “capture” variables at the time they were created. To get fresh values, include them in dependencies or use refs.
Pitfall #4 — Over-Memoizing Everything 🏋️
useMemo
and useCallback
are like protein shakes — great in the right doses, but overuse just makes things heavy.
If you memoize everything “just in case,” your code will be harder to read, and performance can actually get worse because memoization has a cost.
Fix:
Only memoize when:
- The computation is expensive
- Or you’re passing a function/prop to a memoized child component that would otherwise re-render unnecessarily
Pitfall #5 — Breaking Hook Order in Custom Hooks
Sometimes people think, “I’ll just put a hook inside this if
inside my custom hook — what could go wrong?”
What goes wrong: your hook order changes, React’s internal tracking explodes, and you spend an afternoon on Stack Overflow.
Fix:
Never call hooks conditionally — not in components, not in custom hooks, not anywhere.
Pitfall #6 — Forgetting That State Setters Are Asynchronous
If you call setCount(count + 1)
twice in a row, you might expect it to go up by 2. Surprise — it doesn’t.
Example:
setCount(count + 1);
setCount(count + 1); // still +1
Fix:
Use the functional form if you need to update based on the previous state:
setCount(c => c + 1);
setCount(c => c + 1); // now +2
TL;DR: Most hook bugs boil down to either
- Messing with the rules/order, or
- Not thinking about how dependencies and closures work.
Once you keep those in check, hooks become a lot less mysterious — and a lot more fun.
Alright — let’s dive into Hooks and Performance, mixing practical advice with a little myth-busting so it’s not just “here’s a list of optimizations” but also when they’re actually worth doing.
7. Hooks and Performance ⚡
Here’s a hard truth: you probably don’t need to optimize as much as you think.
React is already fast — most performance issues come from doing too much work unnecessarily, not from React itself being slow.
That said, hooks give us some tools to keep components snappy — as long as we don’t misuse them.
Performance Tool #1 — React.memo
+ useCallback
= Fewer Re-Renders
Let’s say you have a parent component that renders a child. If the parent re-renders, the child re-renders too — unless you wrap that child in React.memo
.
But there’s a catch:
If the child receives a function prop, that function changes identity on every render unless you wrap it in useCallback
.
Example:
const Child = React.memo(({ onClick }) => {
console.log('Child render');
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => setCount(c => c + 1), []);
return <Child onClick={handleClick} />;
}
Result: the child only re-renders when it needs to.
Performance Tool #2 — useMemo
for Expensive Calculations
useMemo
can prevent heavy computations from running unnecessarily.
Example:
const sortedData = useMemo(() => {
return data.sort((a, b) => a.value - b.value);
}, [data]);
Without useMemo
, that sort runs on every render — even if data
hasn’t changed.
With useMemo
, it only runs when data
changes.
⚠️ Caution: Don’t wrap every calculation in useMemo
. Memoization itself has a cost — use it for expensive work, not simple math.
Performance Tool #3 — Splitting Components & Hooks
If your component does a lot of work (state, effects, rendering), split it into smaller components or custom hooks.
- Smaller components re-render independently
- Smaller hooks keep logic isolated and easier to test
This can be a bigger win than any memoization trick.
Performance Tool #4 — Avoid “Unnecessary” State
Sometimes you’re storing things in state that don’t need to be there, causing extra renders.
For example, instead of:
const [filteredList, setFilteredList] = useState([]);
useEffect(() => setFilteredList(list.filter(...)), [list]);
…just compute it on the fly:
const filteredList = useMemo(() => list.filter(...), [list]);
This keeps state lean and avoids an entire render cycle.
Performance Myth — “Always Optimize Early”
Premature optimization in React is a time sink. Profile first, then optimize.
Use the React DevTools Profiler to actually see what’s slow — you might be surprised to find that your “performance fix” is making things worse.
Bottom line:
Hooks give you fine-grained control over when and how your components re-render, but with great power comes… the temptation to over-optimize. Use these tools deliberately, and you’ll keep your app fast and your code clean.
8. How Hooks Work Under the Hood 🛠️
If you’ve ever wondered “How does React know which useState
is which?” — welcome to the rabbit hole. 🐇
It’s simpler (and weirder) than you might think.
A Hook is… just a function call in a list
When React renders your component, it doesn’t actually care about the names of your variables.
It cares about the order in which you call hooks.
Behind the scenes, React keeps an internal array (or linked list) of hook states for each component.
- On the first render, it runs your function top to bottom, calling hooks in order and storing their initial values.
- On future renders, it runs your function top to bottom again, matching up each hook call by its position in the render order.
That’s why “Only call hooks at the top level” is a thing — if you skip a hook call (e.g., inside an if
), React’s internal indexing gets out of sync and chaos ensues.
The Hook Slot Machine 🎰
Think of each hook call as pulling a lever on a slot machine:
- First pull →
useState("Hello")
gets slot #1 in the hook list. - Second pull →
useEffect(...)
gets slot #2. - Third pull →
useState(42)
gets slot #3.
Next render, React expects you to pull those levers in the same order. If you suddenly skip lever #2, lever #3’s state gets assigned to #2 — and boom, weird bugs.
But what about custom hooks?
A custom hook is just a function that calls other hooks — React doesn’t treat it specially.
The same ordering rule applies inside that function, so you still can’t conditionally call hooks inside your custom hook.
Where is this stored?
React stores hook state in its internal Fiber tree — a data structure that represents your UI at a given moment.
- Each component has a Fiber node.
- Each Fiber has a linked list of hook objects.
- Each hook object stores its own state, effect callbacks, refs, and other bookkeeping info.
When React re-renders, it walks the Fiber tree, runs components, and reuses the hook objects in the same order.
What about effects?
When you call useEffect
, React doesn’t run your effect immediately.
It stores your effect function in the hook object, then schedules it to run after the paint.
useLayoutEffect
is almost the same — except it runs synchronously after the DOM updates but before the browser paints.
This scheduling is why effects don’t block rendering and why cleanup functions run before the next effect or before the component unmounts.
Concurrent Rendering twist
With React’s concurrent features, a render can be started, paused, thrown away, or replayed — and hooks still work.
That’s because the hook state lives with the Fiber tree, not in your component function itself.
React always matches up the correct hook state to the correct render attempt, even if it bails halfway.
TL;DR:
- Hooks are matched by call order, not names.
- State lives in React’s internal hook list for each component.
- Break the call order → break everything.
- Effects are stored, then scheduled to run later.
- Concurrent rendering makes this trickier — but React’s Fiber system handles it for you.
Let's now discuss about Testing Hooks — where we’ll see how to poke and prod these magical state-machines without losing our sanity.
9. Testing Hooks 🧪
Testing hooks isn’t mystical — you just need the right tools.
If your hook lives inside a component, you can test it indirectly by testing that component’s behavior with React Testing Library.
But if you’ve written a custom hook and want to test it in isolation, you can use @testing-library/react
(previously react-hooks-testing-library
):
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Key tips:
- Always wrap state updates in
act()
so React can flush effects and updates before assertions. - Mock API calls or timers if your hook depends on them.
- For effects, test the outcome, not the effect itself.
Rule of thumb: If a hook is so tied to UI that you can’t test it without a component, test it through that component instead.
10. Conclusion 🎯
Hooks aren’t just a “React thing you have to learn” — they’re the way to think about state, side effects, and shared logic in modern React.
You’ve now seen:
- Why the rules of hooks aren’t arbitrary
- How React keeps hook state in sync under the hood
- Common pitfalls and how to dodge them
- How to test hooks without losing your cool
Whether you’re using them to manage a simple counter or orchestrate a complex data-fetching strategy, hooks give you predictable state management and reusable logic — without the boilerplate.
And now that you know how they tick, you’ll write cleaner, more intentional code instead of just “trying things until it works.”
Next up in the React Deep Dive series:
🚀 Understanding React’s Component Lifecycle — we’ll demystify what happens from the moment a component is born, through its glory days of re-renders, all the way to its peaceful unmount.
Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome
📢 Play Games & Earn Money for FREE!
Want to make money without spending a single penny?
✅ FREE Sign - Up Bonus : Get $10 instantly when you sign up !
🎯 No investments need - just play and start earning today !
Sign up :
listwr.com/o75kZJ