0. Introduction
React Hooks are a way to manage state and lifecycle without classes (since 16.8).
- Simpler than class syntax
- Easier to reuse logic (custom hooks)
- Easier to test
👉 Goals of this article:
-
Beginners: Use
useState
/useEffect
correctly - Intermediate: Understand render/commit, stale closures, and re-renders caused by function props to make sound design decisions
1. Visualize Render vs. Commit
- Render: React calls your component function to produce virtual DOM.
- Commit: The diff is applied to the real DOM.
👉 The component function is invoked by React itself—internally as part of the lifecycle, not by your code directly.
2. Core Hooks
2-1. useState
const [user, setUser] = useState({ name: "Taro", age: 20 });
// ❌ setUser replaces the whole state
setUser({ age: 21 });
// => { age: 21 } (name is gone)
// ✅ Spread to keep the rest
setUser(prev => ({ ...prev, age: 21 }));
useState — Key Points
- Holds state inside a component
-
setState
replaces the value (no partial merge) - Updates are async; prefer the functional updater when you need the latest value
- Use it for data that should be reflected in the UI
2-2. useEffect
useEffect(() => {
const id = setInterval(() => console.log("tick"), 1000);
return () => clearInterval(id);
}, []);
useEffect — Key Points
- Runs side effects after render
- Control when it runs with the dependency array
- Return a cleanup function to release resources
- Great for async calls, subscriptions, DOM operations, etc.
2-3. useContext
const ThemeContext = createContext("light");
const theme = useContext(ThemeContext);
useContext — Key Points
- Share values with descendants without prop drilling
- When the value changes, all consumers re-render
- Handy for global settings (theme, auth info, etc.)
- For large-scale state, consider Redux or Zustand
3. Effect Dependencies, Stale Closures, and Solving Them with useRef
What is a stale closure?
Function components create a new function scope on every render.
As a result, callbacks that captured variables at render time may keep reading old values—that’s a stale closure.
❌ Bug caused by a stale closure
function Timer() {
const [count, setCount] = useState(0);
function tick() {
// ❌ 'count' may be stale here
setCount(count + 1);
}
useEffect(() => {
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // 'tick' not in deps
}
👉 tick
keeps referring to the initial count
, so it never increments beyond 0.
✅ Avoid it with a functional update
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // ✅ always uses the latest value
}, 1000);
return () => clearInterval(id);
}, []);
✅ Avoid it with useRef
function Timer() {
const [count, setCount] = useState(0);
const countRef = useRef(0);
useEffect(() => {
const id = setInterval(() => {
countRef.current += 1;
setCount(countRef.current);
}, 1000);
return () => clearInterval(id);
}, []);
}
useRef — Key Points
- Holds a mutable value in
.current
- Updating it does not trigger a re-render
- Also used for DOM element refs
- Prevents stale closures by always reading the latest value
- Great for values not needed in the UI or temporary values
4. useCallback and Re-renders Caused by “Functions Are Objects”
What happens when passing a function to a child?
- The parent re-renders.
- The parent’s function definition runs again, creating a new function object.
- The child sees a new prop reference.
- The child re-renders.
👉 If the child is heavy, unnecessary re-renders make the UI feel sluggish.
In JS, functions are objects
function a() {}
function b() {}
console.log(a === b); // false
→ A newly created function is a different object each time.
✅ Optimize with useCallback + React.memo
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} />;
}
useCallback — Key Points
- Memoizes a function so its reference stays stable
- Prevents unnecessary child re-renders when passing callbacks
- Stabilizes functions used inside
useEffect
dependency arrays - Works best together with
React.memo
5. useEffect × useReducer: Handling Function Dependencies & Multi-step State
Function dependency example
function Child({ onData }) {
useEffect(() => {
const id = setInterval(() => {
onData(new Date().toLocaleTimeString());
}, 1000);
return () => clearInterval(id);
}, [onData]); // include the function dependency
}
👉 In the parent, use useCallback
to stabilize onData
.
Multi-step transitions with useReducer
const initialState = { status: "idle", data: null, error: null };
function reducer(state, action) {
switch (action.type) {
case "FETCH_START": return { status: "loading", data: null, error: null };
case "FETCH_SUCCESS": return { status: "success", data: action.payload, error: null };
case "FETCH_ERROR": return { status: "error", data: null, error: action.error };
default: return state;
}
}
useReducer — Key Points
- Great for complex, multi-step state transitions
- Name actions to clarify logic
- Often dispatch from inside
useEffect
- Ideal for forms, data fetching, step flows
- Frequently combined with function dependencies
6. Custom Hooks
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return width;
}
Custom Hooks — Key Points
- Combine multiple hooks to build reusable logic
- Must be named starting with
use
- Extract UI-agnostic logic for reuse
- Perfect for common tasks (window size, fetching, auth checks)
7. Hooks Summary Table
Hook | Primary Use | Caveats / Pitfalls |
---|---|---|
useState | Simple state management |
setState replaces the value. Spread objects yourself when updating. |
useEffect | Side effects (fetching, subscriptions, DOM) | Wrong deps cause stale closures or infinite loops. |
useContext | Avoid prop drilling | Value changes re-render all consumers. |
useRef | DOM refs / non-UI mutable values | Updates don’t re-render. Useful against stale closures. |
useCallback | Memoize functions / stable props | Functions are objects; new ones re-render children. Use with React.memo . |
useReducer | Complex state (multi-step transitions) | More setup (actions), but clearer and more readable state. |
Custom Hooks | Share & reuse logic | Must start with use . Best for UI-agnostic, reusable behavior. |
📌 How to choose
- Small features →
useState
+useEffect
- Global shared values →
useContext
- Perf issues from prop functions →
useCallback
+React.memo
- Complex flows →
useReducer
- Reusable logic → Custom hooks
Top comments (0)