【React Hook全家桶】大致过一遍React Hooks

React Hook 全家桶核心逻辑(最底层原理)

所有 Hook 都存在组件的链表 (Hook Chain) 中,React 根据调用顺序来读取状态。

1.useState和useEffect:

1.1 useState 和 useEffect 的执行时机

关键时间线:

javascript 复制代码
function Component() {
  console.log('1. 组件函数执行');
  
  const [count, setCount] = useState(0);
  console.log('2. useState 返回值');
  
  useEffect(() => {
    console.log('4. useEffect 执行(DOM 已更新)');
    return () => console.log('5. 清理函数(下次 effect 前或卸载时)');
  }, [count]);
  
  console.log('3. return JSX 前');
  return <div>{count}</div>;
}

// 输出顺序:1 → 2 → 3 → 渲染 DOM → 4

浏览器永远是:

  1. 先执行完所有同步 JS 代码
  2. 再执行一次浏览器渲染(解析HTML+CSS+合成渲染树+布局 + 绘制)
  3. 最后才执行异步队列(微任务 → 宏任务)

并且:

  • useEffect 属于异步任务
  • DOM 渲染在同步代码之后
时间线:
javascript 复制代码
同步 JS 执行阶段(阻塞渲染)
  1. 组件执行
  2. useState
  3. 注册 useEffect(不执行)
  4. return JSX
同步代码执行完毕 

浏览器渲染阶段(JS 暂停)
  5. React 更新真实 DOM
  6. 浏览器布局、绘制
DOM 渲染完毕 

异步任务阶段
  7. 执行 useEffect 回调

实战案例:聊天室滚动到底部

javascript 复制代码
function ChatRoom({ messages }) {
  const bottomRef = useRef(null);
  
  useEffect(() => {
    // √此时 DOM 已渲染,可以安全操作
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]); // 新消息到达时滚动
  
  return (
    <div>
      {messages.map(msg => <div key={msg.id}>{msg.text}</div>)}
      <div ref={bottomRef} />
    </div>
  );
}

常见错误:

javascript 复制代码
// 错误:在渲染阶段操作 DOM
function Bad() {
  const ref = useRef(null);
  ref.current?.focus(); // 此时 DOM 可能还没渲染!
  return <input ref={ref} />;
}

// 正确:在 useEffect 中操作
function Good() {
  const ref = useRef(null);
  useEffect(() => {
    ref.current?.focus(); // DOM 已就绪
  }, []);
  return <input ref={ref} />;
}

2. useRef:稳定引用与缓存

  • 核心 :返回一个永远不变 的对象 { current: 值 }
  • 特性 :修改 .current 不会触发组件重渲染。
  • 本质 :它是类组件 this 的替代方案,专门用于存值 而不是渲染
  • 两大场景
    1. 获取 DOM 节点ref={inputRef}
    2. 存储可变值 :定时器 ID、上一次的 props/state(通过 .current 存取以避开闭包陷阱)。

3. useRef vs useState:什么时候用谁?

核心区别:

特性 useState useRef
改变时重新渲染 是的 不是
保存跨渲染的值 是的 是的
适用场景 UI 状态 DOM 引用、定时器 ID、上一次的值

实战案例 1:保存定时器 ID

javascript 复制代码
function Stopwatch() {
  const [time, setTime] = useState(0);
  const timerRef = useRef(null); // 不需要触发渲染
  
  const start = () => {
    timerRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };
  
  const stop = () => {
    clearInterval(timerRef.current); // 访问最新的 timer ID
  };
  
  useEffect(() => {
    return () => clearInterval(timerRef.current); // 清理
  }, []);
  
  return (
    <div>
      <p>{time}s</p>
      <button onClick={start}>开始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

为什么不用 useState 存 timer ID?

javascript 复制代码
// ❌ 错误示范
const [timerId, setTimerId] = useState(null);

const start = () => {
  setTimerId(setInterval(...)); // 触发重新渲染,浪费性能
};

只有这 3 种东西能触发组件重渲染

  1. useState 里的 setXxx
  2. useReducer 的 dispatch
  3. 父组件重新渲染

👉 useEffect 绝对不能触发重渲染! 它只是个副作用回调,本身不触发渲染。

  • setState 调用 → 一定重渲染(组件函数跑一遍)
  • 值真的变了 ( React 用 Object.is 对比:值变化)→ 才更 DOM→ 才重新执行useEffect
  • 值没变 → 组件重跑,但 DOM 不动(React 优化),也不重新执行useEffect

实战案例 2:保存上一次的值

javascript 复制代码
function usePrevious(value) {
  const ref = useRef();
  
  useEffect(() => {
    ref.current = value; // 渲染后更新
  });
  
  return ref.current; // 返回上一次的值
}

// 使用
function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  
  return (
    <div>
      <p>当前: {count}</p>
      <p>上一次: {prevCount}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

4. useMemo & useCallback:缓存优化(语法糖关系)

两者底层逻辑一致,都是基于**Object.is** 做浅比较缓存。

  • useMemo :缓存计算结果(值)
    • 作用:避免复杂逻辑在每次渲染时重复执行(如大数据过滤)。
    • 公式:const value = useMemo(() => calc(bigList), [bigList])
  • useCallback :缓存函数引用(函数本身)
    • 作用:保证函数地址不变,防止子组件(被 memo 包装)不必要的重渲染。
    • 本质:它就是 useMemo(() => () => {}, []) 的语法糖。
    • 公式:const fn = useCallback(() => {}, [])
javascript 复制代码
// 等价于
const handleClick = useMemo(() => () => doSomething(a, b), [a, b]);

什么时候用?什么时候不用?

应该用的场景:

场景 1:昂贵的计算

javascript 复制代码
function ProductList({ products, filters }) {
  // ✅ 过滤和排序很耗时,缓存结果
  const filteredProducts = useMemo(() => {
    console.log('重新计算...');
    return products
      .filter(p => p.category === filters.category)
      .sort((a, b) => b.price - a.price);
  }, [products, filters]);
  
  return (
    <ul>
      {filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

场景 2:避免子组件不必要的重新渲染

javascript 复制代码
const ExpensiveChild = React.memo(({ data, onUpdate }) => {
  console.log('子组件渲染');
  return <div>{data.name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [user, setUser] = useState({ name: 'Alice' });
  
  // ✅ 缓存函数,避免每次渲染创建新函数
  const handleUpdate = useCallback(() => {
    setUser({ ...user, name: 'Bob' });
  }, [user]);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* count 变化时,ExpensiveChild 不会重新渲染 */}
      <ExpensiveChild data={user} onUpdate={handleUpdate} />
    </div>
  );
}
  • 父组件渲染 → 默认所有子组件都会跟着渲染
  • React.memo 让子组件变成 "浅比较 props",只有 props 变了才渲染
  • 但函数是引用类型,每次父组件重渲染都会创建新函数 → 导致 memo 失效
  • useCallback 就是用来 "缓存函数",让函数引用不变 → memo 才能生效
  • 最终:count 变化时,子组件不渲染!
不应该用的场景:
javascript 复制代码
// ❌ 过度优化:简单计算不需要 useMemo
const doubled = useMemo(() => count * 2, [count]); // 浪费
const doubled = count * 2; // 直接算更快

// ❌ 依赖项太多,缓存失效频繁
const result = useMemo(() => a + b + c + d + e, [a, b, c, d, e]); // 没意义

// ❌ 子组件没用 React.memo,缓存函数无效
function Child({ onClick }) { // 没有 React.memo
  return <button onClick={onClick}>点击</button>;
}

function Parent() {
  const handleClick = useCallback(() => {}, []); // 白费力气
  return <Child onClick={handleClick} />;
}

5. memo:组件层面的门禁

  • 作用 :包裹函数组件,默认阻止重渲染。
  • 逻辑 :只有传给子组件的 props 发生变化(Object.is 对比),子组件才会重新渲染。
  • 配合 :必须和 useCallback 配合使用,否则函数引用变化依然会触发子组件重渲染。

6. 自定义 Hook:封装可复用逻辑

实战案例 1:useDebounce(防抖值)
javascript 复制代码
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [value, delay]);
  
  return debouncedValue;
}

// 使用
function SearchBox() {
  const [input, setInput] = useState('');
  const debouncedInput = useDebounce(input, 500);
  
  useEffect(() => {
    if (debouncedInput) {
      fetch(`/api/search?q=${debouncedInput}`)
        .then(res => res.json())
        .then(setResults);
    }
  }, [debouncedInput]); // 只在防抖后的值变化时请求
  
  return <input value={input} onChange={e => setInput(e.target.value)} />;
}
  1. 用户输入 → input 立刻更新
  2. useDebounce 监测到 input 变化
  3. 开启 500ms 定时器
  4. 500ms 内再次输入 → 清除旧定时器,重新计时
  5. 停手 500ms → debouncedInput 才更新
  6. debouncedInput 变化 → 发送搜索请求

**只发一次请求,完美避免频繁请求!**useDebounce = 让值延迟更新,专门解决输入搜索、频繁请求的防抖问题,是 React 最实用的自定义 Hook 之一。

实战案例 2:useLocalStorage(持久化状态)
javascript 复制代码
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });
  
  const setStoredValue = useCallback((newValue) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  }, [key]);
  
  return [value, setStoredValue];
}

// 使用,主题变化:
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      当前主题: {theme}
    </button>
  );
}

逐行拆解:

javascript 复制代码
function useLocalStorage(key, initialValue) {
  // 1. 初始化:优先从 localStorage 取,取不到就用默认值
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key); // 从本地存储拿数据
      return item ? JSON.parse(item) : initialValue; // 有就解析,没有就用默认值
    } catch {
      return initialValue; // 解析失败也用默认值
    }
  });

  // 2. 缓存一个更新函数:改 state 同时自动存 localStorage
  const setStoredValue = useCallback((newValue) => {
    setValue(newValue); // 先更新内部 state
    localStorage.setItem(key, JSON.stringify(newValue)); // 再同步存到本地
  }, [key]); // 依赖 key,key 不变函数就不变

  // 3. 返回:用法和 useState 一毛一样!
  return [value, setStoredValue];
}

完整执行流程:

我用主题切换的例子走一遍:

第一次进入页面(初始化):

  1. 执行 useLocalStorage('theme', 'light')
  2. 进入 useState 初始化函数
  3. 去 localStorage 找 theme 这个 key
    • 第一次:找不到
    • 所以 value = 'light'
  4. 准备好 setStoredValue 函数
  5. 返回 ['light', 函数]

点击按钮切换主题:

  1. 执行 setTheme('dark')
  2. 调用 setStoredValue('dark')
  3. 内部执行:
    • setValue('dark') → 状态更新
    • localStorage.setItem('theme', 'dark')存到本地
  4. 页面重渲染 → 显示 dark

刷新页面!(最关键):

  1. 再次执行 useLocalStorage('theme', 'light')
  2. 去 localStorage 找 theme
    • 找到了!值是 'dark'
  3. JSON.parse 解析后返回
  4. value = 'dark'
  5. 返回 ['dark', ...]
  6. 页面直接显示 dark,不会变回 light!

这就是持久化状态。

原生 localStorage useLocalStorage Hook
是什么 浏览器原生存储 API React 自定义 Hook
是否响应式 不触发渲染 自动触发渲染
是否自动同步 手动存、手动取 改状态自动存
JSON 处理 自己写 自动处理
异常处理 容易崩 自带 try/catch
适合谁 原生 JS、简单场景 React 项目、状态持久化
实战案例 3:useIntersectionObserver核心hook:
javascript 复制代码
function useIntersectionObserver(ref, options) {
  const [isVisible, setIsVisible] = useState(false);
  
  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      setIsVisible(entry.isIntersecting);
    }, options);
    
    if (ref.current) {
      observer.observe(ref.current);
    }
    
    return () => observer.disconnect();
  }, [ref, options]);
  
  return isVisible;
}
懒加载:监听图片是否可见
javascript 复制代码
function LazyImage({ src }) {
  const imgRef = useRef(null);
  const isVisible = useIntersectionObserver(imgRef, { threshold: 0.1 });
  
  return (
    <div ref={imgRef}>
      {isVisible ? <img src={src} /> : <div>加载中...</div>}
    </div>
  );
}
无限滚动组件:

思路:页面底部放一个看不见的小 div → 它一出现就加载下一页

javascript 复制代码
function InfiniteList() {
  const [list, setList] = useState([]);
  const [page, setPage] = useState(1);
  const loading = useRef(false);

  // 底部的"加载触发器"
  const loadMoreRef = useRef(null);
  const isVisible = useIntersectionObserver(loadMoreRef, {
    threshold: 0.1,
  });

  // 模拟加载数据
  const loadMore = useCallback(async () => {
    if (loading.current) return;
    loading.current = true;

    // 模拟接口请求
    const newData = await fetchList(page);

    setList(prev => [...prev, ...newData]);
    setPage(prev => prev + 1);
    
    loading.current = false;
  }, [page]);

  // 监听底部元素是否出现 → 出现就加载
  useEffect(() => {
    if (isVisible) {
      loadMore();
    }
  }, [isVisible, loadMore]);

  return (
    <div>
      {/* 列表 */}
      {list.map(item => (
        <div key={item.id}>{item.content}</div>
      ))}

      {/* 👇 这就是无限滚动的"触发器" */}
      <div ref={loadMoreRef} style={{ height: 10 }} />
      
      {loading.current && <p>加载中...</p>}
    </div>
  );
}
执行流程:
  1. 页面渲染,展示第一页数据
  2. 你往下滚动
  3. 快到底部时,loadMoreRef 这个 div 进入屏幕
  4. isVisible 变成 true
  5. useEffect 监听到 isVisible 变化
  6. 自动调用 loadMore()
  7. 加载新数据,拼接到列表
  8. 列表变长,继续滚动 → 循环

为什么这样写超级强?
  • 不用监听 scroll(性能爆炸好)
  • 不用算距离、不用算高度
  • 浏览器原生优化,丝滑不卡顿
  • 逻辑干净,不容易出 bug
  • 懒加载、无限滚动 一套 Hook 通用

绝对不能踩的致命坑:

坑 1:数组变异方法 setState(push 陷阱)
  • 错误setState(arr.push(newItem))
    • 原因push 返回的是新长度(数字),不是新数组。
    • 后果 :state 变成数字,后续 .map 直接报错。
    • 原则 :React 状态必须是不可变数据 (Immutable)
  • 正确setState([...arr, newItem])(扩展运算符生成新数组)。
  • 进阶setState(prev => [...prev, newItem])(函数式更新,依赖前一个状态)。
坑 2:useCallback 空依赖陷阱
  • 现象useCallback(() => { console.log(count) }, [])
  • 问题 :空依赖会永久捕获 首次渲染的变量值。即使 count 变了,函数里拿到的永远是初始值(闭包陷阱)。
  • 解法
    1. 把变量加入依赖数组 [count]
    2. 或用 useRef 存储最新值中转。
坑 3:memo 与普通函数的区别
  • 现象 :子组件加了 memo,但点击父组件状态还是重渲染了。
  • 原因 :父组件每次渲染都会重新创建函数,导致传给子组件的 props 引用变了。
  • 解法 :函数必须套上 useCallback,保证引用稳定。

黄金组合公式(开发必背)

想要实现高性能子组件,必须组合使用:

  1. 子组件 :用 memo 包裹。

    javascript 复制代码
    const Child = memo(({ onClick }) => <button onClick={onClick}>按钮</button>);
  2. 父组件 :函数用 useCallback 缓存。

    javascript 复制代码
    const handleClick = useCallback(() => { /* 逻辑 */ }, [依赖]);
  3. 传参 :将缓存后的函数传给子组件。

    javascript 复制代码
    <Child onClick={handleClick} />

总结与记忆口诀

  1. 存值不重渲染用 useRef
  2. 缓存计算值用 useMemo
  3. 缓存函数用 useCallback
  4. 组件门禁用 memo
  5. setState 永远用新引用,拒绝 push。
相关推荐
2401_885885047 分钟前
易语言彩信接口怎么调用?E语言Post实现多媒体数据批量下发
前端
a11177619 分钟前
Three.js 的前端 WebGL 页面合集(日本 开源项目)
前端·javascript·webgl
Kk.080234 分钟前
项目《基于Linux下的mybash命令解释器》(一)
前端·javascript·算法
程序员鱼皮1 小时前
又一个新项目开源,让 AI 帮你盯全网热点!
javascript·ai·程序员·编程·ai编程
MXN_小南学前端1 小时前
前端开发中 try...catch 到底怎么用?使用场景和最佳实践
javascript·vue.js
星空椰1 小时前
JavaScript 基础进阶:分支、循环与数组实战总结
开发语言·javascript·ecmascript
小李子呢02111 小时前
前端八股---闭包和作用域链
前端
IT_陈寒2 小时前
Redis的内存溢出坑把我整懵了,分享这个血泪教训
前端·人工智能·后端
m0_738120722 小时前
渗透测试基础ctfshow——Web应用安全与防护(五)
前端·网络·数据库·windows·python·sql·安全
Z_Wonderful2 小时前
基于 Vite 的 React+Vue 混部完整模板(含目录结构、依赖清单、启动脚本)
前端·vue.js·react.js