React Hooks
React Hooks 是 React 16.8 引入的特性,让你在不编写 class 的情况下使用 state 和其他 React 特性。
Overview
Hooks 允许你在函数组件中:
- 使用 state(
useState) - 执行副作用(
useEffect) - 访问 context(
useContext) - 优化性能(
useMemo,useCallback) - 以及更多...
useState
1. Basic Usage
const [state, setState] = useState(initialState)2. Batching Updates(React 18+) 批量更新
Multiple setState calls are batched into a single render:
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(1);
setCount(2);
setCount(3);
// Only renders once, final count is 3
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}3. Batch Updates's Closure Trap 闭包陷阱
The state value is captured by the closure and won't change during function execution:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log(count) // 0
setCount(count + 1)
console.log(count) // 0
setCount(count + 1)
console.log(count) // 0
// 1. 点击按钮后,所有同步代码打印结果都是 0 ,异步代码计算结果都是 1(闭包捕获了相同的 count = 0 这个值)
// 2. count 更新,组件重新渲染:<p>Count: 1</p>
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}4. Functional Updates 函数式更新
Use Functional Updates when the new state depends on the previous state:
function Counter() {
const [count, setCount] = useState(0);
// const handleClick = () => {
// setCount(count + 1)
// setCount(count + 1)
// setCount(count + 1)
// }
// Use stale value from Closure
// Result: 1
const handleClick = () => {
setCount(prevCount => prevCount + 1)
setCount(prevCount => prevCount + 1)
setCount(prevCount => prevCount + 1)
// Use Previous value from React
// Result: 3
}
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
)
}How it works:
// Direct value - React takes the last value
updateQueue = [
{ type:'value', value: 1 },
{ type:'value', value: 1 },
{ type:'value', value: 1 }
]
// Result: 1
// Functional Update - React executes functions sequentially
updateQueue = [
{ type: 'function', updater: (prevCount => prevCount + 1) },
{ type: 'function', updater: (prevCount => prevCount + 1) },
{ type: 'function', updater: (prevCount => prevCount + 1) }
]
let result = 0
for (let i = 0; i < updateQueue.length; i++) {
let updater = updateQueue[i].updater;
result = updater(result);
}
// Result: 3When to Use Functional Updates(Require Scenarios):
- Multiple updates based on previous state
- Updates in
useEffectwith empty dependencies - Updates in async callbacks(avoid stale closures)
// In useEffect
useEffect(() => {
// 这个函数只执行一次(mount 时)
const timer = setInterval(() => {
// 这个回调函数在第一次 useEffect 执行时创建
// 它捕获的 count 永远是 0
// 即使组件重新渲染 100 次,这个回调函数也不会被重新创建
// setCount(count + 1);
// Always gets latest value
setCount(prevCount => prevCount + 1)
}, 1000);
return () => clearInterval(timer);
}, []); // Empty dependency array, only run once
// In async callbacks
const fetchData = () => {
const result = await fetch('https://api.example.com/data')
// In this case, count might be stale
// 比如网络请求 3 秒后才得到结果
// 在这 3 秒内,用户通过点击事件已经改变了 count 的值,比如加了 10 次
// 那么等网络请求得到结果后,应该是 10 + result
// 但是闭包捕获的 count 值还是初始值 0 ,结果就会是 0 + result
// setCount(count + result)
// Always gets latest value
setCount(prevCount => prevCount + result)
}函数式更新和直接传值更新的区别:只是 React 内部计算最终值的方式不同,并不会让你在同步代码或者当次微任务中看到新值。
5. Immutability 不可变性 - 复杂对象(数组)的状态更新
不可变性的本质:旧状态保持不变,新状态重新创建,两者都是独立的对象,占据不同的内存空间。
const [user, setUser] = useState({ name: 'Ryan', age: 25 });
// ❌ Wrong - Mutable (directly modifies the original object)
const updateAge = () => {
user.age = 26; // Modifies original object
setUser(user); // Same reference, no re-render
};
// ✅ Correct - Immutable (creates a new object)
const updateAge = () => {
setUser({ ...user, age: 26 }); // New object, new reference, triggers re-render
};
// Nested object update
const [user, setUser] = useState({
name: 'Ryan',
address: { city: 'Chengdu', country: 'China' }
});
const updateCity = (newCity) => {
setUser({
...user, // Spread user
address: {
...user.address, // Spread address
city: newCity
}
});
};
// Array
const [items, setItems] = useState([1, 2, 3]);
// Add
setItems([...items, 4]); // Add to end
setItems([0, ...items]); // Add to start
// Delete(Filter)
setItems(items.filter(item => item !== 2)); // Delete value 2
// Modify(Map)
setItems(items.map(item => item === 2 ? 20 : item)); // Modify value 2 to 20
// Sort
setItems([...items].sort()); // Note: copy first then sort核心:filter、map、concat、展开运算符都会返回新数组。
避免:push、pop、sort、splice 会修改原数组。
6. Lazy Initialization 惰性初始化
这个在实际项目中很有用(比如从 localStorage 读取数据)。
function Component() {
// ❌ 问题:每次渲染都会执行这个函数
const [data, setData] = useState(localStorage.getItem('user'));
console.log('组件渲染');
return <div>{data}</div>;
}
function Component() {
// ✅ 正确:传入函数,只在初始化时执行一次
const [data, setData] = useState(() => {
// 计算成本高的操作
console.log('初始化函数执行');
return localStorage.getItem('user');
});
console.log('组件渲染');
return <div>{data}</div>;
}7. 派生状态
派生状态 = 可以从现有数据计算出来的状态
// 反模式:重复存储
function ProductCard() {
const [price, setPrice] = useState(100);
const [tax, setTax] = useState(price * 0.1); // ❌ 派生状态
const [total, setTotal] = useState(price + tax); // ❌ 派生状态
const updatePrice = (newPrice) => {
setPrice(newPrice);
setTax(newPrice * 0.1); // ❌ 需要手动同步
setTotal(newPrice + newPrice * 0.1); // ❌ 需要手动同步
};
return (
<div>
<p>Price: {price}</p>
<p>Tax: {tax}</p>
<p>Total: {total}</p>
</div>
);
}
// 问题:
// 状态冗余(tax 和 total 可以计算出来)
// 需要手动同步(容易忘记,导致状态不一致)
// 代码复杂,难维护
// 正确做法:直接计算
function ProductCard() {
const [price, setPrice] = useState(100);
// ✅ 直接计算,不需要单独的 state
const tax = price * 0.1;
const total = price + tax;
const updatePrice = (newPrice) => {
setPrice(newPrice); // 只需要更新一个 state
// tax 和 total 会自动重新计算 ✅
};
return (
<div>
<p>Price: {price}</p>
<p>Tax: {tax}</p>
<p>Total: {total}</p>
</div>
);
}
// 使用原则
// 能计算出来的,就不要存 state
// 如果计算很慢,用 useMemo 优化useRef
useState vs useRef
| Feature | useState | useRef |
|---|---|---|
| Modify value | Triggers re-render | Does NOT trigger re-render |
| Display on UI | Updates immediately | Updates on next render |
| Use case | UI-related data | Non-UI data that needs to persist |
| Typical usage | Form values, list data, UI state | DOM references, timer IDs, cached values |
Typical Use Cases
1. DOM Reference
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="Auto focus" />;
}2. Store Timer ID
function Timer() {
const [count, setCount] = useState(0);
const timerRef = useRef(null);
const start = () => {
// Clear old timer before creating a new one
if (timerRef.current) {
clearInterval(timerRef.current);
}
timerRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
};
const stop = () => {
clearInterval(timerRef.current);
timerRef.current = null; // Clear reference
};
useEffect(() => {
return () => clearInterval(timerRef.current);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}3. Store Previous Value
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef(0);
useEffect(() => {
prevCountRef.current = count;
}, [count]);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCountRef.current}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}4. Debounce/Throttle
function SearchInput() {
const [keyword, setKeyword] = useState('');
const debounceTimerRef = useRef(null);
const handleChange = (e) => {
const value = e.target.value;
setKeyword(value);
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = setTimeout(() => {
console.log('Search:', value);
}, 500);
};
return <input value={keyword} onChange={handleChange} />;
}Key Points:
useRefdoes NOT trigger re-render when modified(不触发渲染)ref.currentcan be updated directly(可以直接修改 ref.current )- The value persists across renders(值在渲染间保持不变)
- UI only updates when component re-renders for other reasons(需等待其他原因触发渲染才更新 UI)
useEffect
useEffectis a hook that runs side effects in a function component.- It takes a function and an array of dependencies as arguments.
- We usually use it to perform side effects, such as data fetching, timer, manual DOM manipulation, etc(/et cetera/, and so on).
What is SIDE EFFECT?
Side effect is any action that a function performs other than returning a value. For example:
- a function that prints a value to the console is a side effect
- a function that modifies a global variable is a side effect
- a function that makes a network request is a side effect
- a function that subscribes to an event is a side effect
- a function that updates the DOM is a side effect
Key Points Summary:
- Dependency Array Behaviorjsx
// 1. No dependency array useEffect(() => { console.log('Runs on every render'); }) // 2. Empty dependency array useEffect(() => { console.log('Runs only on mount'); }, []) // 3. Dependency array with values useEffect(() => { console.log('Runs on mount and when value changes'); }, [value]) - Cleanup Function
- Purpose: Prevent memory leaks
- When cleanup runs:
- Before the effect re-runs(when dependencies change)
- When the component unmounts
- Common Scenarios Requiring Cleanup:
- Timer
- Event Listener
- WebSocket
- Subscription
- Why Cleanup is Critical: Without cleanup:
- Timer or listener keeps running after component unmounts
- Callback holds references to component variables(Closure), javascript garbage collector can't free memory, which leads to Memory Leaks
useLayoutEffect
useEffect Example
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('useEffect 执行');
}, [count]);
console.log('组件渲染');
return <div>{count}</div>;
}
// Execution order:
// 1. 组件渲染
// 2. React updates DOM
// 3. Browser paints (user sees the update)
// 4. useEffect executes (delayed by React Scheduler, after paint)useLayoutEffect Example
function Component() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
console.log('useLayoutEffect 执行');
}, [count]);
console.log('组件渲染');
return <div>{count}</div>;
}
// Execution order:
// 1. 组件渲染
// 2. React updates DOM
// 3. useLayoutEffect executes (sync, blocks paint)
// 4. Browser paints (user sees the update)Comparison Table
| Feature | useEffect | useLayoutEffect |
|---|---|---|
| Execution timing | After browser paint (delayed) | Before browser paint (sync) |
| Blocks rendering | ❌ No | ✅ Yes |
| Performance | ✅ Better (doesn't block paint) | ⚠️ May affect performance |
| Visual flicker | ⚠️ May flicker | ✅ No flicker |
| Use cases | Data fetching, subscriptions, logs | DOM measurements, sync animations |
| SSR support | ✅ Supported | ⚠️ Warnings (no DOM on server) |
When to Use
Default: useEffect
// ✅ Most cases use useEffect
useEffect(() => {
// Data fetching
fetchData();
// Subscriptions
const sub = subscribe();
return () => sub.unsubscribe();
// Logging
console.log('Component mounted');
}, []);Only when necessary: useLayoutEffect
// ✅ Only use when you need synchronous DOM operations
useLayoutEffect(() => {
// Measure DOM
const rect = elementRef.current.getBoundingClientRect();
// Synchronous animations
element.style.transform = 'translateX(100px)';
// Scroll position
element.scrollTop = 0;
}, []);Decision Guide
Do you need to read DOM size/position and update immediately?
├─ Yes → useLayoutEffect
└─ No → useEffect
Will it cause visual flicker?
├─ Yes → useLayoutEffect
└─ No → useEffect
Other cases → useEffect (default choice)Golden Rules
- Default to useEffect for better performance
- Only use useLayoutEffect when you see flicker
- DOM measurements must use useLayoutEffect
- Data fetching and subscriptions use useEffect
How React Ensures useEffect Runs After Paint
useEffect is NOT traditional "async"
❌ Inaccurate: "useEffect is async"
✅ Accurate: "useEffect is delayed/scheduled by React"React's Implementation (Simplified)
// React Commit Phase
function commitRoot() {
// 1. Update DOM (sync)
updateDOM();
// 2. Execute useLayoutEffect (sync, blocks paint)
flushLayoutEffects();
// 3. Browser can paint here
// 4. Schedule useEffect (delayed, after paint)
Scheduler.scheduleCallback(() => {
flushEffects(); // Execute all useEffect
});
}How React Schedules useEffect
React uses MessageChannel (a macro task) to ensure effects run after browser paint:
Call stack executes
↓
Microtask queue executes (Promise.then)
↓
Browser paints
↓
Macro task executes (MessageChannel) ← useEffect runs hereWhy MessageChannel?
setTimeout(fn, 0)→ Minimum 4ms delay (too slow)Promise.then(fn)→ Microtask, runs before paint (too early)MessageChannel→ Macro task, runs after paint (perfect) ✅
Key Takeaway
useLayoutEffect: Sync execution, blocks paint
useEffect: Scheduled by React Scheduler, runs after paint via MessageChannelReact.memo / useMemo / useCallback
React.memois a HOC (Higher-Order Component) that wraps a component to prevent unnecessary re-renders.It takes a component as an argument.
If the props haven't changed, the component will not re-render.
It caches the result of the component's render.
This is an optimization at the component level.
useMemois a hook that memoizes a value.It takes a function and an array of dependencies as arguments.
If the dependencies haven't changed, the function will not be re-executed.
It caches the value returned by the function.
This is an optimization at the value level.
useCallbackis a hook that memoizes a function.It takes a function and an array of dependencies as arguments.
If the dependencies haven't changed, the function will not be re-created.
It caches the reference to the function.
This is an optimization at the function level.
When to use them?
React.memo: Wrap child components that receive stable propsuseMemo: Cache expensive calculations(e.g. data processing, filtering, sorting, etc.)useCallback: Cache event handlers passed to memoized child components(Usually used withReact.memo).
Closure
Memory Leaks
It's not the closure itself that causes the Memory Leaks, but "resources held by the closure not being cleaned up" that leads to the Memory Leaks.