1小时速通React之Hooks

本文系统梳理 React 中 8 个最常用的核心 Hooks:useStateuseReduceruseContextuseEffectuseRefuseImperativeHandleuseMemouseCallback。从底层运行机制到高阶实战用法,配合完整可运行的代码块,助你彻底掌握函数式组件的开发精髓。


前言

React 16.8 正式引入 Hooks,使得函数组件拥有了类组件的全部能力(状态、生命周期、上下文等),同时摒弃了类组件中 this 指向混乱、逻辑复用困难(HOC / Render Props)等问题。Hooks 的本质是将组件状态逻辑与视图逻辑进行解耦 ,其底层依赖 React 内部的 Fiber 架构 ,通过链表(memoizedState)来按顺序存储各个 Hook 的状态值。

核心铁律(必须遵守)

  1. 只在 React 函数组件或自定义 Hook 中调用 Hooks。
  2. 只在顶层调用 Hooks(不在循环、条件或嵌套函数中),以保证 Hooks 的调用顺序在每次渲染中一致。

1. useState ------ 响应式状态的基石

原理

useState 是 React 内部状态管理的最小单元。在 Fiber 节点上,状态以单向链表的形式存储。每次渲染时,React 根据 Hook 的调用顺序依次从链表上读取对应的状态值。setState 会触发组件重新渲染,并生成新的 Fiber 树。

作用

为函数组件添加内部状态,状态改变时 UI 自动更新。

语法

jsx

scss 复制代码
const [state, setState] = useState(initialState);
  • 支持函数式更新:setState(prev => prev + 1) 可避免因闭包导致的状态过期问题。

示例

jsx

ini 复制代码
import { useState } from 'react';

function UserForm() {
  const [username, setUsername] = useState('');
  const [age, setAge] = useState(18);

  const handleSubmit = () => {
    alert(`用户名:${username},年龄:${age}`);
  };

  return (
    <div>
      <input 
        value={username} 
        onChange={(e) => setUsername(e.target.value)} 
        placeholder="请输入用户名" 
      />
      <input 
        type="number" 
        value={age} 
        onChange={(e) => setAge(Number(e.target.value))} 
      />
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

2. useReducer ------ 复杂状态逻辑的治理专家

原理

useState 共享底层状态存储机制,但将状态更新逻辑集中到 reducer 纯函数中。通过 dispatch 派发 actionreducer 根据 action.type 计算出新状态。这种模式源自 Redux,让状态变化可预测、可追踪

作用

  • 当状态更新逻辑包含多个子值或复杂转换(如购物车、表单校验)时,比 useState 更具可读性。
  • 适合状态更新依赖先前状态,且操作类型多样(增、删、改、重置)的场景。

语法

jsx

scss 复制代码
const [state, dispatch] = useReducer(reducer, initialArg, init);

示例:购物车数量管理

jsx

javascript 复制代码
import { useReducer } from 'react';

// 1. 定义 reducer
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return { ...state, count: state.count + 1 };
    case 'SUBTRACT':
      return { ...state, count: Math.max(0, state.count - 1) };
    case 'RESET':
      return { ...state, count: 0 };
    default:
      return state;
  }
}

function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, { count: 0 });

  return (
    <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
      <button onClick={() => dispatch({ type: 'SUBTRACT' })}>-</button>
      <span>数量:{state.count}</span>
      <button onClick={() => dispatch({ type: 'ADD' })}>+</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
    </div>
  );
}

3. useContext ------ 跨层级数据传递的直通车

原理

useContext 基于 React 的 Context API 。当上层组件通过 <Provider value={...}> 提供数据时,React 会在 Fiber 节点上维护一个 context 链。下层调用 useContext 会向上查找最近的 Provider,并订阅其值。当 Provider 的 value 变化时,所有订阅该 Context 的组件会触发更新。

作用

彻底解决 Props Drilling(属性钻取)问题,让数据在组件树中无障碍传递,常用于主题、语言环境、用户鉴权信息等全局数据。

语法

jsx

ini 复制代码
const MyContext = React.createContext(defaultValue);
const value = useContext(MyContext);

示例:全局主题切换

jsx

javascript 复制代码
import { createContext, useContext, useState } from 'react';

// 1. 创建 Context
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });

// 2. 提供者组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const toggleTheme = () => {
    setTheme((t) => (t === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. 深层子组件消费
function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  const isDark = theme === 'dark';

  return (
    <button
      onClick={toggleTheme}
      style={{
        background: isDark ? '#333' : '#fff',
        color: isDark ? '#fff' : '#000',
        padding: '8px 16px',
        border: '1px solid #ccc',
        cursor: 'pointer',
      }}
    >
      当前主题:{theme},点击切换
    </button>
  );
}

function Toolbar() {
  return <ThemedButton />; // 中间组件无需透传任何 props
}

// 4. 使用
export default function App() {
  return (
    <ThemeProvider>
      <Toolbar />
    </ThemeProvider>
  );
}

4. useEffect ------ 副作用与生命周期的统一管理

原理

useEffect 的调度基于 Fiber 的 Effect List 。在浏览器完成布局与绘制 之后,React 会异步执行 Effect 函数(默认)。通过依赖数组,React 使用 Object.is 比较前后依赖项,决定是否跳过执行。清理函数会在组件卸载或下一次 Effect 执行之前调用,用于清除定时器、取消订阅等。

作用

  • 替代类组件生命周期:componentDidMount(空依赖)、componentDidUpdate(指定依赖)、componentWillUnmount(清理函数)。
  • 执行数据获取、手动 DOM 操作、订阅事件等副作用。

语法

jsx

scss 复制代码
useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理逻辑(可选)
  };
}, [dependencies]);

示例:获取用户数据并设置定时器

jsx

javascript 复制代码
import { useState, useEffect } from 'react';

function UserProfile() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isMounted = true; // 防止组件卸载后依然 setState

    // 模拟异步请求
    const fetchData = async () => {
      try {
        const response = await new Promise((resolve) =>
          setTimeout(() => resolve({ name: '王五', age: 30 }), 2000)
        );
        if (isMounted) {
          setUser(response);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) setLoading(false);
      }
    };

    fetchData();

    // 清理函数:组件卸载时取消挂载标记
    return () => {
      isMounted = false;
    };
  }, []); // 仅在挂载时执行一次

  if (loading) return <p>加载中...</p>;
  return (
    <div>
      <h2>用户信息</h2>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
    </div>
  );
}

5. useRef ------ 存储可变数据与 DOM 引用

原理

useRef 返回一个 { current: initialValue } 对象。该对象在组件的整个生命周期内内存地址恒定 (类似类组件的实例属性)。修改 current 不会触发组件重新渲染,这使得它非常适合存储"不影响视图"的可变数据。

作用

  • 获取 DOM 元素的引用(如聚焦、测量尺寸)。
  • 存储上一次渲染的值、定时器 ID、WebSocket 实例等跨渲染周期数据。

语法

jsx

ini 复制代码
const refContainer = useRef(initialValue);
// 使用:refContainer.current

示例一:自动聚焦输入框

jsx

javascript 复制代码
import { useRef, useEffect } from 'react';

function AutoFocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} placeholder="页面加载后自动聚焦" />;
}

示例二:保存计时器 ID 并清除

jsx

javascript 复制代码
import { useRef, useState } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);
  const timerIdRef = useRef(null);

  const start = () => {
    if (timerIdRef.current) return; // 防止重复启动
    timerIdRef.current = setInterval(() => {
      setSeconds((prev) => prev + 1);
    }, 1000);
  };

  const stop = () => {
    clearInterval(timerIdRef.current);
    timerIdRef.current = null;
  };

  return (
    <div>
      <p>计时:{seconds} 秒</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

6. useImperativeHandle ------ 封装子组件的命令式接口

原理

useImperativeHandle 必须与 forwardRef 配合。父组件传递的 ref 对象会被子组件接收,useImperativeHandle 允许子组件自定义 挂载到 ref.current 上的内容(通常是一个方法对象)。这通过劫持 ref 的映射过程实现,防止父组件直接操作子组件的 DOM 或内部状态,增强封装性。

作用

  • 暴露特定方法给父组件(如 focusresetvalidate)。
  • 控制父组件对子组件的访问权限,避免破坏子组件的内部逻辑。

语法

jsx

scss 复制代码
useImperativeHandle(ref, () => ({
  // 返回暴露的方法
}), [dependencies]);

示例:父组件调用子组件表单重置与聚焦

jsx

javascript 复制代码
import { forwardRef, useImperativeHandle, useRef, useState } from 'react';

// 子组件
const CustomInput = forwardRef((props, ref) => {
  const [value, setValue] = useState('');
  const inputRef = useRef(null);

  // 暴露方法给父组件
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    reset: () => {
      setValue('');
      inputRef.current.focus();
    },
    getValue: () => value,
  }));

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="子组件输入框"
    />
  );
});

// 父组件
function ParentForm() {
  const childRef = useRef(null);

  const handleReset = () => {
    childRef.current.reset();
  };

  const handleGetValue = () => {
    alert(childRef.current.getValue());
  };

  return (
    <div>
      <CustomInput ref={childRef} />
      <button onClick={handleReset}>重置并聚焦</button>
      <button onClick={handleGetValue}>获取子组件值</button>
    </div>
  );
}

7. useMemo ------ 记忆化计算结果,提升渲染性能

原理

useMemo 利用闭包 存储上一次的返回值。在渲染阶段,React 会对比依赖项数组,若依赖未变,直接返回缓存的旧值,跳过函数执行。其缓存挂载在 Fiber 节点的 memoizedState 上,属于渲染期间的计算优化。

作用

  • 缓存高开销的计算(如大数据过滤、复杂数学运算)。
  • 保持引用类型的稳定性(如对象、数组),避免因每次渲染重新创建而导致子组件(React.memo)无效重绘。

语法

jsx

scss 复制代码
const memoizedValue = useMemo(() => compute(a, b), [a, b]);

示例:大数据过滤与输入框丝滑交互

jsx

javascript 复制代码
import { useState, useMemo } from 'react';

function FilterList() {
  const [keyword, setKeyword] = useState('');
  const [count, setCount] = useState(0);

  // 模拟一个庞大的静态数据列表
  const allItems = useMemo(() => {
    const items = [];
    for (let i = 0; i < 20000; i++) {
      items.push(`商品-${i}`);
    }
    return items;
  }, []); // 只在首次渲染生成一次

  // 根据关键词过滤(仅当 keyword 或 allItems 变化时重新计算)
  const filteredItems = useMemo(() => {
    console.log('执行过滤计算...');
    if (!keyword) return allItems;
    return allItems.filter((item) => item.includes(keyword));
  }, [keyword, allItems]);

  return (
    <div>
      <input
        value={keyword}
        onChange={(e) => setKeyword(e.target.value)}
        placeholder="输入关键词过滤(体验丝滑输入)"
      />
      <button onClick={() => setCount((c) => c + 1)}>重渲染计数器:{count}</button>
      <ul style={{ height: '300px', overflow: 'auto', border: '1px solid #ccc' }}>
        {filteredItems.slice(0, 100).map((item) => (
          <li key={item}>{item}</li>
        ))}
        {filteredItems.length > 100 && <li>...仅显示前100条</li>}
      </ul>
    </div>
  );
}

点击计数器改变 count 时,组件重渲染,但 filteredItems 因依赖未变直接命中缓存,列表不会重新过滤,输入框交互依然丝滑。


8. useCallback ------ 记忆化函数引用,优化子组件渲染

原理

useCallback 本质是 useMemo(() => fn, deps) 的语法糖,返回的是一个记忆化的函数。当依赖项不变时,函数的引用地址保持不变。

作用

  • 当父组件重渲染时,传递给 React.memo 子组件的回调函数引用不变,子组件得以跳过渲染。
  • 作为 useEffect 的依赖项时,避免因函数重新创建而导致 Effect 频繁执行。

语法

jsx

ini 复制代码
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

示例:配合 React.memo 避免子组件无效重绘

jsx

javascript 复制代码
import { useState, useCallback, memo } from 'react';

// 子组件用 memo 包裹,进行浅层 props 比较
const Child = memo(({ onAdd, label }) => {
  console.log(`Child ${label} 渲染了`);
  return <button onClick={onAdd}>增加 {label}</button>;
});

function Parent() {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);
  const [text, setText] = useState('');

  // 使用 useCallback 缓存,依赖为空,函数永远不变
  const handleAddA = useCallback(() => {
    setCountA((prev) => prev + 1);
  }, []);

  // 此处故意不使用 useCallback,每次渲染都会创建新函数
  const handleAddB = () => {
    setCountB((prev) => prev + 1);
  };

  return (
    <div>
      <input
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="在此输入,观察子组件渲染情况"
      />
      <p>Count A: {countA}</p>
      <p>Count B: {countB}</p>
      <Child onAdd={handleAddA} label="A (优化)" />
      <Child onAdd={handleAddB} label="B (未优化)" />
    </div>
  );
}

当在输入框中打字时,父组件重渲染。Child AhandleAddA 引用不变,不会重新渲染;Child B 因每次接收新的 handleAddB 函数,导致 props 变化,触发不必要的重渲染。


扩展补充:React.memo(非 Hook,但密切配合)

React.memo 是一个高阶组件(HOC),用于缓存组件。它对 props 进行浅层比较,若 props 未变则跳过本次渲染。常与 useCallbackuseMemo 组成性能优化的"铁三角"。

jsx

javascript 复制代码
const MyComponent = memo(function MyComponent(props) {
  // 仅当 props 改变时才重新渲染
});

完整总结对照表

Hook 名称 核心职责 典型应用场景
useState 管理简单局部响应式状态 表单输入、开关、计数器
useReducer 管理含有复杂逻辑的局部状态 购物车、具有多种操作类型的对象状态
useContext 跨组件树共享数据,避免 Props 透传 主题、语言、用户认证信息
useEffect 执行副作用与生命周期管理 数据请求、DOM 操作、订阅与清理
useRef 存储可变数据(不触发渲染)及 DOM 引用 获取 DOM 节点、保存定时器 ID、记录上一次值
useImperativeHandle 自定义暴露给父组件的实例方法 封装表单组件、暴露 focus / reset 方法
useMemo 记忆化计算结果,缓存对象/数组引用 大数据计算、稳定子组件 props
useCallback 记忆化函数引用,配合 memo 优化 传递给子组件的回调函数

进阶提示(并非全部,但值得了解)

除了上述 8 个核心 Hook,React 还提供了:

  • useLayoutEffect :与 useEffect 类似,但在 DOM 更新后、浏览器绘制前同步执行,适合处理 DOM 尺寸测量等需要避免闪烁的操作。
  • useDebugValue:在 React DevTools 中为自定义 Hook 显示标签,方便调试。
  • useId :生成稳定且唯一的 ID,用于 SSR 场景下的无障碍属性(如 aria-describedby)。
  • useTransition / useDeferredValue:用于并发模式下的 UI 更新降级与渲染优化。
相关推荐
JAVA面经实录9171 小时前
操作系统(面试全覆盖)
java·计算机网络·面试
黄敬峰1 小时前
从 DFS 遍历到抖音推荐算法:前端工程师的硬核复习笔记
前端
zach1 小时前
网页中的虚拟滚动技术是不是源自IOS中的tableview的机制
前端
柯克七七1 小时前
公司前端项目打包体积从 2MB 降到 400KB,我改了这四个配置
前端
英勇无比的消炎药1 小时前
我才发现这些架构的“拆”与“合”哲学
前端
shen_1 小时前
AI Coding:前端UI规范笔记
前端
石山代码2 小时前
JavaScript 进阶核心知识点
开发语言·javascript·ecmascript
llz_1122 小时前
web-第五次课后作业
前端·后端·http
牛油果子哥q2 小时前
AVL平衡树与红黑树深度精讲对比,平衡因子、四大旋转原理、着色规则、平衡策略、性能差异与面试手撕全解
数据结构·c++·面试