Skip to content

React Hooks

React Hooks 是 React 16.8 引入的特性,让你在不编写 class 的情况下使用 state 和其他 React 特性。

Overview

Hooks 允许你在函数组件中:

  • 使用 state(useState
  • 执行副作用(useEffect
  • 访问 context(useContext
  • 优化性能(useMemo, useCallback
  • 以及更多...

useState

1. Basic Usage

jsx
const [state, setState] = useState(initialState)

2. Batching Updates(React 18+) 批量更新

Multiple setState calls are batched into a single render:

jsx
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:

jsx
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:

jsx
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:

js
// 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: 3

When to Use Functional Updates(Require Scenarios):

  • Multiple updates based on previous state
  • Updates in useEffect with empty dependencies
  • Updates in async callbacks(avoid stale closures)
jsx
// 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 不可变性 - 复杂对象(数组)的状态更新

不可变性的本质:旧状态保持不变,新状态重新创建,两者都是独立的对象,占据不同的内存空间。

jsx
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 读取数据)。

jsx
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. 派生状态

派生状态 = 可以从现有数据计算出来的状态

jsx
// 反模式:重复存储
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

FeatureuseStateuseRef
Modify valueTriggers re-renderDoes NOT trigger re-render
Display on UIUpdates immediatelyUpdates on next render
Use caseUI-related dataNon-UI data that needs to persist
Typical usageForm values, list data, UI stateDOM references, timer IDs, cached values

Typical Use Cases

1. DOM Reference

jsx
function AutoFocusInput() {
  const inputRef = useRef(null);
  
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  
  return <input ref={inputRef} placeholder="Auto focus" />;
}

2. Store Timer ID

jsx
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

jsx
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

jsx
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:

  • useRef does NOT trigger re-render when modified(不触发渲染)
  • ref.current can be updated directly(可以直接修改 ref.current )
  • The value persists across renders(值在渲染间保持不变)
  • UI only updates when component re-renders for other reasons(需等待其他原因触发渲染才更新 UI)

useEffect

  • useEffect is 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:

  1. Dependency Array Behavior
    jsx
    // 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])
  2. Cleanup Function
    1. Purpose: Prevent memory leaks
    2. When cleanup runs:
      1. Before the effect re-runs(when dependencies change)
      2. When the component unmounts
    3. Common Scenarios Requiring Cleanup:
      1. Timer
      2. Event Listener
      3. WebSocket
      4. Subscription
    4. Why Cleanup is Critical: Without cleanup:
      1. Timer or listener keeps running after component unmounts
      2. Callback holds references to component variables(Closure), javascript garbage collector can't free memory, which leads to Memory Leaks

useLayoutEffect

useEffect Example

jsx
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

jsx
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

FeatureuseEffectuseLayoutEffect
Execution timingAfter 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 casesData fetching, subscriptions, logsDOM measurements, sync animations
SSR support✅ Supported⚠️ Warnings (no DOM on server)

When to Use

Default: useEffect

jsx
// ✅ Most cases use useEffect
useEffect(() => {
  // Data fetching
  fetchData();
  
  // Subscriptions
  const sub = subscribe();
  return () => sub.unsubscribe();
  
  // Logging
  console.log('Component mounted');
}, []);

Only when necessary: useLayoutEffect

jsx
// ✅ 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

  1. Default to useEffect for better performance
  2. Only use useLayoutEffect when you see flicker
  3. DOM measurements must use useLayoutEffect
  4. 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)

javascript
// 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 here

Why 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 MessageChannel

React.memo / useMemo / useCallback

  • React.memo is 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.

  • useMemo is 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.

  • useCallback is 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 props
  • useMemo: Cache expensive calculations(e.g. data processing, filtering, sorting, etc.)
  • useCallback: Cache event handlers passed to memoized child components(Usually used with React.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.

Released under the MIT License.