[前端-React] Hook

目录

useState

一、基础语法

二、核心用法

[1. 为组件添加状态(最基础用法)](#1. 为组件添加状态(最基础用法))

[2. 根据先前的 state 更新 state(函数式更新)](#2. 根据先前的 state 更新 state(函数式更新))

[3. 更新状态中的对象和数组(不可变更新)](#3. 更新状态中的对象和数组(不可变更新))

(1)更新对象状态

(2)更新数组状态

[4. 避免重复创建初始状态](#4. 避免重复创建初始状态)

[5. 使用 key 重置状态](#5. 使用 key 重置状态)

[6. 存储前一次渲染的信息](#6. 存储前一次渲染的信息)

[三、关键补充:set 函数的核心特性](#三、关键补充:set 函数的核心特性)

useEffect

一、基础语法

二、核心用法

[1. 连接到外部系统](#1. 连接到外部系统)

[2. 在自定义 Hook 中封装 Effect](#2. 在自定义 Hook 中封装 Effect)

[3. 控制非 React 小部件](#3. 控制非 React 小部件)

[4. 使用 Effect 请求数据](#4. 使用 Effect 请求数据)

[5. 指定响应式依赖项](#5. 指定响应式依赖项)

[6. 在 Effect 中根据先前 state 更新 state](#6. 在 Effect 中根据先前 state 更新 state)

[7. 删除不必要的对象依赖项](#7. 删除不必要的对象依赖项)

[8. 删除不必要的函数依赖项](#8. 删除不必要的函数依赖项)

[9. 从 Effect 读取最新的 props 和 state](#9. 从 Effect 读取最新的 props 和 state)

useContext

[一、基础 语法](#一、基础 语法)

二、解释核心用法

[1. 向组件树深层传递数据](#1. 向组件树深层传递数据)

[2. 通过 context 更新传递的数据](#2. 通过 context 更新传递的数据)

[3. 指定后备方案默认值](#3. 指定后备方案默认值)

[4. 覆盖组件树一部分的 context](#4. 覆盖组件树一部分的 context)

[5. 在传递对象和函数时优化重新渲染](#5. 在传递对象和函数时优化重新渲染)

useReducer

一、基础语法

二、核心用法

[1. 向组件添加 reducer(基础用法:替代复杂 useState)](#1. 向组件添加 reducer(基础用法:替代复杂 useState))

[2. 实现 reducer 函数(核心规则 + 复杂状态示例)](#2. 实现 reducer 函数(核心规则 + 复杂状态示例))

[(1)reducer 必须遵守的 3 个规则](#(1)reducer 必须遵守的 3 个规则)

[(2)复杂状态示例:管理一个 "待办列表(todos)"](#(2)复杂状态示例:管理一个 “待办列表(todos)”)

[3. 避免重新创建初始值(使用 init 函数)](#3. 避免重新创建初始值(使用 init 函数))

场景:从本地存储(localStorage)读取待办列表作为初始状态

[三、关键补充:dispatch 函数的特性](#三、关键补充:dispatch 函数的特性)

[四、useReducer vs useState:什么时候该用哪个?](#四、useReducer vs useState:什么时候该用哪个?)

useCallback

一、先基础语法

二、核心用

[1. 跳过组件的重新渲染(最常用场景)](#1. 跳过组件的重新渲染(最常用场景))

[2. 从记忆化回调中更新 state](#2. 从记忆化回调中更新 state)

[方式 1:函数式更新(推荐,无需依赖 state)](#方式 1:函数式更新(推荐,无需依赖 state))

[方式 2:依赖 state(当更新需要其他状态 /props 时)](#方式 2:依赖 state(当更新需要其他状态 /props 时))

[3. 防止频繁触发 Effect](#3. 防止频繁触发 Effect)

[4. 优化自定义 Hook](#4. 优化自定义 Hook)

[三、关键补充:useCallback 的使用误区](#三、关键补充:useCallback 的使用误区)

useMemo

一、基础语法

二、核心用法

[1. 跳过代价昂贵的重新计算](#1. 跳过代价昂贵的重新计算)

[2. 跳过组件的重新渲染](#2. 跳过组件的重新渲染)

[3. 防止过于频繁地触发 Effect](#3. 防止过于频繁地触发 Effect)

[4. 记忆另一个 Hook 的依赖](#4. 记忆另一个 Hook 的依赖)

[5. 记忆一个函数](#5. 记忆一个函数)

useRef

一、基础语法

二、核心用法

[1. 使用 ref 引用一个值(跨渲染存储普通值)](#1. 使用 ref 引用一个值(跨渲染存储普通值))

[场景 1:存储定时器 ID(用于组件卸载时清除)](#场景 1:存储定时器 ID(用于组件卸载时清除))

[场景 2:存储前一次的状态 /props](#场景 2:存储前一次的状态 /props)

[2. 通过 ref 操作 DOM(最常用场景)](#2. 通过 ref 操作 DOM(最常用场景))

[场景 1:聚焦输入框(页面加载后自动聚焦)](#场景 1:聚焦输入框(页面加载后自动聚焦))

[场景 2:获取 DOM 元素的尺寸(比如宽度、高度)](#场景 2:获取 DOM 元素的尺寸(比如宽度、高度))

[3. 避免重复创建 ref 的内容](#3. 避免重复创建 ref 的内容)

[场景:ref 初始值是复杂对象(避免重复创建)](#场景:ref 初始值是复杂对象(避免重复创建))


useState

一、基础语法

useState 接收 1 个参数 initialState(初始状态),返回一个数组,结构如下:

复制代码
const [state, setState] = useState(initialState);

逐个解释核心概念:

  1. initialState**(初始状态)** :组件第一次渲染时的状态值,可以是任意类型(数字、字符串、布尔值、对象、数组,甚至是 null/undefined);
  2. state**(当前状态)**:存储当前的状态值,组件渲染时会使用这个值渲染 UI;
  3. setState**(更新状态的函数,即你说的** setSomething**)** :触发状态更新的 "触发器",调用后会修改 state 的值,并让 React 重新渲染组件;
    • 注意:setState 是 "异步更新"(React 会批量处理多个更新,提升性能),不能在调用后立刻拿到最新的 state
    • 可以接收两种参数:直接传 "新状态值"(比如 setCount(5)),或传 "更新函数"(比如 setCount(prev => prev + 1))。

二、核心用法

1. 为组件添加状态(最基础用法)

这是 useState 最核心的用途:给原本 "无状态" 的函数组件添加动态状态,让组件能响应用户操作(比如点击、输入)并更新 UI。

场景:实现一个简单的计数器,点击按钮让数字加 1。

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

function Counter() {
  // 1. 添加状态:初始值为 0,返回 [当前计数, 更新计数的函数]
  const [count, setCount] = useState(0);

  // 2. 点击事件:调用 setCount 更新状态
  const handleIncrement = () => {
    setCount(count + 1); // 传新状态值
  };

  return (
    <div>
      <p>当前计数:{count}</p> {/* 渲染当前状态 */}
      <button onClick={handleIncrement}>加 1</button> {/* 触发状态更新 */}
    </div>
  );
}

👉 关键逻辑:

  • 组件第一次渲染时,count 是初始值 0
  • 点击按钮调用 setCount(count + 1)count 变为 1,React 重新渲染组件,页面显示最新的 1
  • 每次点击都会重复这个 "更新状态 → 重新渲染" 的流程。
2. 根据先前的 state 更新 state(函数式更新)

由于 setState 是异步的,当你需要 "基于上一次的状态" 计算新状态时(比如连续点击按钮多次),直接用 state 变量可能会拿到 "过时的值"(因为 React 批量处理更新时,state 还没来得及更新)。

此时需要用 "函数式更新":setState(prevState => newState)prevState 是 React 保证的 "最新的上一次状态"。

场景:连续点击按钮,确保每次都是基于最新计数加 1(避免漏更)。

复制代码
function Counter() {
  const [count, setCount] = useState(0);

  // 函数式更新:prevCount 是最新的上一次状态
  const handleIncrement = () => {
    setCount(prevCount => prevCount + 1); // 推荐:基于 prevCount 计算
  };

  // 错误示例:如果快速点击多次,可能会漏更(比如点 3 次只加 1 次)
  // const handleIncrement = () => {
  //   setCount(count + 1); // 直接用 count,可能拿到旧值
  // };

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={handleIncrement}>快速点击加 1</button>
    </div>
  );
}

👉 关键原则:

  • 只要新状态依赖 "上一次的状态",就用函数式更新(prevState => newState);
  • 新状态不依赖旧状态(比如直接设为固定值 setCount(10)),可以直接传新值。
3. 更新状态中的对象和数组(不可变更新)

state 是对象或数组时,不能直接修改原对象 / 数组(React 依赖 "状态引用变化" 来检测更新,直接修改原数据不会触发重渲染),必须返回一个 "新的对象 / 数组"(即 "不可变更新")。

(1)更新对象状态

用 "扩展运算符(...)" 复制原对象,再修改需要更新的属性。

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

function UserProfile() {
  // 状态是对象
  const [user, setUser] = useState({
    name: '张三',
    age: 25,
    address: { city: '北京', district: '朝阳区' } // 嵌套对象
  });

  // 更新顶层属性(name)
  const updateName = () => {
    setUser(prevUser => ({
      ...prevUser, // 复制原对象的所有属性
      name: '李四' // 覆盖要更新的属性
    }));
  };

  // 更新嵌套对象(address.city)
  const updateCity = () => {
    setUser(prevUser => ({
      ...prevUser,
      address: {
        ...prevUser.address, // 复制嵌套对象的所有属性
        city: '上海' // 覆盖嵌套对象的属性
      }
    }));
  };

  return (
    <div>
      <p>姓名:{user.name}</p>
      <p>年龄:{user.age}</p>
      <p>城市:{user.address.city}</p>
      <button onClick={updateName}>修改姓名</button>
      <button onClick={updateCity}>修改城市</button>
    </div>
  );
}
(2)更新数组状态

用数组的 mapfilterconcat 等方法(这些方法返回新数组),或扩展运算符,避免修改原数组。

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

function TodoList() {
  // 状态是数组
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 useState', done: false }
  ]);

  // 1. 添加数组元素(用 concat 或扩展运算符)
  const addTodo = () => {
    const newTodo = { id: Date.now(), text: '新的待办', done: false };
    setTodos(prevTodos => [...prevTodos, newTodo]); // 扩展运算符:新数组 = 原数组 + 新元素
  };

  // 2. 修改数组元素(用 map)
  const toggleTodo = (todoId) => {
    setTodos(prevTodos => 
      prevTodos.map(todo => 
        todo.id === todoId ? { ...todo, done: !todo.done } : todo
      )
    );
  };

  // 3. 删除数组元素(用 filter)
  const deleteTodo = (todoId) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== todoId));
  };

  return (
    <div>
      <button onClick={addTodo}>添加待办</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
            <button onClick={() => toggleTodo(todo.id)}>切换状态</button>
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

👉 关键禁忌:

  • 不要用 obj.key = value 直接修改对象;
  • 不要用 array.push()array.splice() 等修改原数组的方法;
  • 嵌套对象 / 数组要 "逐层复制",确保每一层的引用都变化,React 才能检测到更新。
4. 避免重复创建初始状态

如果 initialState 是 "代价昂贵的计算"(比如创建大数组、复杂对象、从本地存储读取并解析数据),直接写在 useState 里,会导致组件每次重渲染时都重新执行这个计算(虽然 React 会忽略重渲染时的新初始值,只在第一次渲染时使用,但仍会浪费性能)。

解决方案:把初始状态的计算逻辑包装成一个 "函数",传给 useState,React 会只在组件第一次渲染时执行这个函数,后续重渲染时跳过。

场景:初始状态是一个包含 10000 个元素的大数组(计算代价高)。

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

function BigList() {
  // 错误示例:每次重渲染都会创建新的大数组(浪费性能)
  // const [list, setList] = useState(Array.from({ length: 10000 }, (_, i) => i));

  // 正确示例:把计算逻辑包装成函数,传给 useState
  const [list, setList] = useState(() => {
    // 这个函数只在第一次渲染时执行
    return Array.from({ length: 10000 }, (_, i) => i); // 生成 0-9999 的数组
  });

  return <div>数组长度:{list.length}</div>;
}

👉 关键语法:

  • initialState 是函数时,React 会把它当作 "初始化函数",仅执行一次;
  • 如果初始状态是简单类型(数字、字符串、null),无需包装函数,直接传入即可(比如 useState(0)useState(''))。
5. 使用 key 重置状态

React 中,key 是组件的 "身份标识"。当组件的 key 变化时,React 会认为这是一个 "新组件",会重新初始化组件的状态(包括 useState 的初始状态),相当于 "重置" 组件。

场景:切换标签页时,重置当前标签页的状态(比如输入框内容、计数)。

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

// 子组件:有自己的状态(输入框内容)
function TabContent({ tabKey }) {
  const [inputValue, setInputValue] = useState('');

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        placeholder="输入内容..."
      />
      <p>当前输入:{inputValue}</p>
    </div>
  );
}

// 父组件:切换标签,通过 key 重置子组件状态
function TabSwitcher() {
  const [activeTab, setActiveTab] = useState('tab1');

  return (
    <div>
      <button onClick={() => setActiveTab('tab1')}>标签 1</button>
      <button onClick={() => setActiveTab('tab2')}>标签 2</button>
      {/* 关键:给子组件设置 key,key 变化时子组件状态重置 */}
      <TabContent tabKey={activeTab} key={activeTab} />
    </div>
  );
}

👉 关键效果:

  • 切换标签时,activeTab 变化 → 子组件的 key 变化 → React 销毁旧的 TabContent 组件,创建新的组件 → 新组件的 inputValue 重置为初始值 ''
  • 如果不设置 key,切换标签时子组件不会被销毁,inputValue 会保留之前的输入(这可能不是你想要的)。
6. 存储前一次渲染的信息

有时候你需要在组件中获取 "上一次渲染时的状态 /props"(比如显示 "从 XX 改为 XX"),可以用 useState 配合 useEffect 实现:用 useState 存储前一次的值,用 useEffect 在状态变化后更新这个 "前一次的值"。

场景:显示计数的 "上一次值" 和 "当前值"。

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

function CounterWithPrev() {
  const [count, setCount] = useState(0);
  const [prevCount, setPrevCount] = useState(0); // 存储前一次的 count

  // 关键:count 变化后,更新 prevCount
  useEffect(() => {
    setPrevCount(prev => count); // 用函数式更新确保拿到最新的 count
  }, [count]); // 依赖 count:count 变化时执行

  return (
    <div>
      <p>上一次计数:{prevCount}</p>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(prev => prev + 1)}>加 1</button>
    </div>
  );
}

👉 关键逻辑:

  • useEffect 监听 count 变化,当 count 更新后,useEffect 执行,把当前的 count 赋值给 prevCount
  • 因为 useEffect 是在组件渲染完成后执行,所以 prevCount 始终存储的是 "上一次渲染时的 count"。

补充:也可以用 useRef 存储前一次的值(更高效,不触发额外重渲染),但 useState + useEffect 是更基础的实现方式,适合刚学习的场景。

三、关键补充:set 函数的核心特性

你提到的 setSomething(nextState)(即 setState 函数)有几个重要特性,必须掌握:

  1. 异步性setState 是 "异步批量更新" 的,调用后不能立刻拿到最新的 state

    const [count, setCount] = useState(0);
    const handleClick = () => {
    setCount(1);
    console.log(count); // 输出 0(异步更新,还没生效)
    };

如果需要在状态更新后执行逻辑,用 useEffect 监听状态变化:

复制代码
useEffect(() => {
  console.log('count 更新后的最新值:', count);
}, [count]);
  1. 幂等性 :多次调用相同的 setState 不会触发多次重渲染,React 会合并成一次:

    const handleClick = () => {
    setCount(1);
    setCount(1);
    setCount(1); // 只会触发一次重渲染
    };

  2. 函数式更新的优先级:如果多次调用函数式更新,React 会按顺序执行,确保状态正确:

    const handleClick = () => {
    setCount(prev => prev + 1); // 1
    setCount(prev => prev + 1); // 2
    setCount(prev => prev + 1); // 3(最终 count 是 3,触发一次重渲染)
    };

useEffect

一、基础语法

  • setup(副作用函数):你要执行的副作用逻辑(比如请求数据、绑定事件),还可以返回一个"清理函数"(比如解绑事件、取消请求)。
  • dependencies(依赖数组,可选) :控制 setup 何时执行的"开关",React 会对比依赖项的前后值,只有变化时才重新运行 setup
    • 不传依赖:组件每次渲染后都执行(包括初始渲染+更新渲染)。
    • 传空数组 []:只在组件初始渲染后执行一次 (类似类组件 componentDidMount)。
    • 传具体依赖(如 [count, props.id]):组件初始渲染后执行,且只有依赖项变化时重新执行。

二、核心用法

1. 连接到外部系统

指组件与 React 之外的系统交互(比如浏览器 API、第三方 SDK、WebSocket 连接等),需要在组件挂载时"连接",卸载时"断开",避免内存泄漏。

场景:监听浏览器窗口大小变化、连接 WebSocket 实时通讯。

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

function WindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth });

  // 连接外部系统(浏览器 resize 事件)
  useEffect(() => {
    // 副作用:绑定事件监听(连接)
    function handleResize() {
      setSize({ width: window.innerWidth });
    }
    window.addEventListener('resize', handleResize);

    // 清理函数:解绑事件(断开),组件卸载时执行
    return () => window.removeEventListener('resize', handleResize);
  }, []); // 空依赖:只连接一次

  return <div>窗口宽度:{size.width}px</div>;
}

👉 关键:外部连接必须在清理函数中"断开"(比如解绑事件、关闭连接),否则会导致内存泄漏。

2. 在自定义 Hook 中封装 Effect

将重复的副作用逻辑抽成自定义 Hook,复用在多个组件中(这是 useEffect 最强大的复用方式)。

场景 :多个组件都需要"监听窗口大小",抽成 useWindowSize 自定义 Hook。

复制代码
// 自定义 Hook:封装副作用逻辑
function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth });

  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth });
    }
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size; // 暴露结果给组件使用
}

// 组件A:复用自定义 Hook
function ComponentA() {
  const size = useWindowSize();
  return <div>A组件:{size.width}px</div>;
}

// 组件B:复用自定义 Hook
function ComponentB() {
  const size = useWindowSize();
  return <div>B组件:{size.width}px</div>;
}

👉 关键:自定义 Hook 命名必须以 use 开头,内部可以调用其他 Hook(如 useEffectuseState)。

3. 控制非 React 小部件

React 无法直接控制非 React 库的 DOM 元素(比如 jQuery 插件、Chart.js 图表、地图组件),useEffect 可以在组件挂载后初始化这些小部件,卸载时销毁。

场景 :例如,如果你有一个没有使用 React 编写的第三方地图小部件或视频播放器组件,你可以使用 Effect 调用该组件上的方法,使其状态与 React 组件的当前状态相匹配。此 Effect 创建了在 map-widget.js 中定义的 MapWidget 类的实例。当你更改 Map 组件的 zoomLevel prop 时,Effect 调用类实例上的 setZoom() 来保持同步:

复制代码
import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

👉 关键:用 useRef 保存 DOM 节点和小部件实例,避免因组件重渲染导致重复初始化。

4. 使用 Effect 请求数据

这是最常见的用法:组件渲染后请求接口数据,拿到数据后更新状态(触发组件重渲染)。

场景:请求用户列表数据并展示。

复制代码
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);

  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [person]);

  // ...

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);
..........

👉 关键:

  • useEffectsetup 不能是 async 函数(会返回 Promise,React 无法处理),需在内部定义 async 函数并调用。
  • 必要时用 AbortController 取消请求(避免组件卸载后仍执行 setState)。
  • 注意,ignore 变量被初始化为 false,并且在 cleanup 中被设置为 true。这样可以确保 你的代码不会受到"竞争条件"的影响:网络响应可能会以与你发送的不同的顺序到达。
5. 指定响应式依赖项

dependenciesuseEffect 的"触发开关",你必须显式声明 setup 中用到的所有 props/state(响应式值),否则会拿到"过时"的数据。

场景 :根据用户选择的分类(category 状态),请求对应的数据。

复制代码
function ProductList() {
  const [category, setCategory] = useState('phone'); // 响应式状态
  const [products, setProducts] = useState([]);

  useEffect(() => {
    // setup 中用到了 category,必须加入依赖数组
    async function fetchProducts() {
      const res = await fetch(`https://api.example.com/products?category=${category}`);
      const data = await res.json();
      setProducts(data);
    }

    fetchProducts();
  }, [category]); // 依赖 category:category 变化时重新请求

  return (
    <div>
      <button onClick={() => setCategory('phone')}>手机</button>
      <button onClick={() => setCategory('laptop')}>电脑</button>
      <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
    </div>
  );
}

👉 关键:

  • 依赖数组必须包含 setup 中所有用到的响应式值(props/state),否则 React 会报警告,且可能出现逻辑错误。
  • 不要漏写依赖,也不要写无关依赖(会导致不必要的重执行)。
6. 在 Effect 中根据先前 state 更新 state

因为 count 是一个响应式值,所以必须在依赖项列表中指定它。但是,这会导致 Effect 在每次 count 更改时再次执行 cleanup 和 setup。这并不理想。

当你需要基于"上一次的状态"更新当前状态时,用 setState(prevState => newState) 形式(无需将 prevState 加入依赖)。

场景 :点击按钮时,基于上一次的计数加 1(在 useEffect 中触发)。

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

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 3秒后,基于先前的 count 加 1(无需依赖 count)
    const timer = setTimeout(() => {
      setCount(prevCount => prevCount + 1); // 函数式更新:拿到最新的 prevCount
    }, 3000);

    return () => clearTimeout(timer);
  }, []); // 空依赖:只执行一次(无需加 count)

  return <div>Count: {count}</div>;
}

👉 关键:函数式更新(prevState => newState)能确保拿到最新的前一次状态,此时不需要将 state 加入依赖数组。

7. 删除不必要的对象依赖项

如果依赖项是"每次渲染都会重新创建的对象/数组"(比如 { a: 1 }[1,2]),即使内容没变,React 也会认为依赖变化,导致 useEffect 重复执行。此时需要"稳定化"依赖。

错误示例 :依赖对象每次渲染都重建,导致 useEffect 无限执行。

复制代码
function BadExample() {
  // 每次渲染都会创建新对象 { limit: 10 }
  const config = { limit: 10 };

  useEffect(() => {
    console.log('依赖变化,执行 Effect'); // 会无限触发,因为 config 每次都是新对象
    fetch(`https://api.example.com/data?limit=${config.limit}`);
  }, [config]); // 错误:依赖不稳定的对象

  return <div>...</div>;
}

正确做法1 :用 useMemo 缓存对象,让其只在内容变化时重建。

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

function GoodExample() {
  // 用 useMemo 缓存对象:只有依赖变化时才重新创建
  const config = useMemo(() => ({ limit: 10 }), []); // 空依赖:永久缓存

  useEffect(() => {
    fetch(`https://api.example.com/data?limit=${config.limit}`);
  }, [config]); // 现在 config 稳定,不会重复执行

  return <div>...</div>;
}

关键:对象/数组依赖需用 useMemo 缓存,或直接解构出原始值(如 [config.limit]),避免不必要的重执行。

正确做法2:直接在Effect内部创建对象。

8. 删除不必要的函数依赖项

如果 setup 中用到的函数是组件内部定义的,默认每次渲染都会重新创建,导致 useEffect 重复执行。此时需要用 useCallback 缓存函数,稳定依赖。

错误示例 :函数每次渲染重建,导致 useEffect 重复执行。

复制代码
function BadExample() {
  // 每次渲染都会创建新函数 handleFetch
  function handleFetch() {
    fetch('https://api.example.com/data');
  }

  useEffect(() => {
    handleFetch(); // 会重复执行,因为 handleFetch 每次都是新函数
  }, [handleFetch]); // 错误:依赖不稳定的函数
}

正确做法1 :用 useCallback 缓存函数,让其只在依赖变化时重建。

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

function GoodExample() {
  // 用 useCallback 缓存函数:只有依赖变化时才重新创建
  const handleFetch = useCallback(() => {
    fetch('https://api.example.com/data');
  }, []); // 空依赖:永久缓存

  useEffect(() => {
    handleFetch();
  }, [handleFetch]); // 现在 handleFetch 稳定,只执行一次
}

👉 关键:组件内部的函数作为依赖时,需用 useCallback 缓存,避免因函数重建导致 useEffect 无效重执行。

**正确做法2:**避免使用在渲染期间创建的函数作为依赖项,请在 Effect 内部声明它:

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

function GoodExample() {

  useEffect(() => {

  function handleFetch() {
    fetch('https://api.example.com/data');
  }
    
    handleFetch();
  }, [handleFetch]); // 现在 handleFetch 稳定,只执行一次
}
9. 从 Effect 读取最新的 props 和 state

默认情况下,在 Effect 中读取响应式值时,必须将其添加为依赖项。这样可以确保你的 Effect 对该值的每次更改都"作出响应"。对于大多数依赖项,这是你想要的行为。

然而,有时你想要从 Effect 中获取 最新的 props 和 state,而不"响应"它们。例如,假设你想记录每次页面访问时购物车中的商品数量:

复制代码
function Page({ url, shoppingCart }) {
  useEffect(() => {
    logVisit(url, shoppingCart.length);
  }, [url, shoppingCart]); // ✅ 所有声明的依赖项
  // ...
}

如果你想在每次 url****更改后记录一次新的页面访问,而不是在 shoppingCart****更改后记录,该怎么办 ?你不能在不违反 响应规则 的情况下将 shoppingCart 从依赖项中移除。然而,你可以表达你 不希望 某些代码对更改做出"响应",即使它是在 Effect 内部调用的。使用 useEffectEvent Hook

,并将读取 shoppingCart 的代码移入其中:

复制代码
function Page({ url, shoppingCart }) {
  const onVisit = useEffectEvent(visitedUrl => {
    logVisit(visitedUrl, shoppingCart.length)
  });

  useEffect(() => {
    onVisit(url);
  }, [url]); // ✅ 所有声明的依赖项
  // ...
}

通过在 onVisit 中读取 shoppingCart,确保了 shoppingCart 不会使 Effect 重新运行。

useContext

一、基础 语法

  • 用法:useContext(SomeContext)
  • SomeContext:是通过 React.createContext(默认值) 创建的 "上下文容器",用来存储要传递的数据(类似一个 "全局数据仓库")。
  • useContext****作用 :在组件中调用 useContext(SomeContext),就能直接获取到最近的 SomeContext.Provider 提供的值(如果没有 Provider,则获取创建 Context 时的默认值)。
  • 核心流程
    1. createContext 创建 Context(指定默认值,可选);
    2. Context.Provider 包裹组件树(通过 value 属性传递数据);
    3. 深层组件用 useContext(Context) 直接获取数据。

二、解释核心用法

1. 向组件树深层传递数据

这是 useContext 最基础的用法:当你需要给嵌套多层的组件传递数据时,不用一层一层写 props 传递,直接用 Context 跨层级传递。

场景:App 顶层有 "主题色" 数据,需要传递给嵌套在 3 层下的 Button 组件。

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

// 1. 创建 Context(默认值可选,这里设为 'light')
const ThemeContext = createContext('light');

// 顶层组件:用 Provider 包裹子组件树,传递数据
function App() {
  const theme = 'dark'; // 要传递的深层数据
  return (
    {/* Provider 的 value 属性:指定要传递给后代的数据 */}
    <ThemeContext.Provider value={theme}>
      <Layout /> {/* 中间组件(无需传递 theme props) */}
    </ThemeContext.Provider>
  );
}

// 中间组件(无需关心 theme,直接透传子组件)
function Layout() {
  return <Navbar />; // 第二层组件
}

function Navbar() {
  return <Button />; // 第三层组件(需要 theme)
}

// 深层组件:用 useContext 获取数据
function Button() {
  // 直接获取最近的 ThemeContext.Provider 传递的 value
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
      主题按钮
    </button>
  );
}

关键:中间组件(Layout、Navbar)完全不用处理 theme 数据,useContext 让深层组件直接 "跳过中间层" 获取顶层数据,解决了 prop drilling 痛点。

2. 通过 context 更新传递的数据

Context 不仅能传递 "静态数据",还能传递 "状态和修改状态的函数",实现深层组件修改顶层数据的效果(类似 "全局状态管理" 的简化版)。

场景:顶层有 "主题色" 状态,深层组件的按钮可以切换主题。

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

// 1. 创建 Context(默认值可以设为 { theme: '', toggleTheme: () => {} },提示类型)
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {},
});

// 顶层组件:管理状态 + 提供修改函数
function App() {
  const [theme, setTheme] = useState('light');

  // 定义修改状态的函数
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  // 传递给 Provider 的 value 包含"状态"和"修改函数"
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <Layout />
    </ThemeContext.Provider>
  );
}

// 深层组件:调用 toggleTheme 修改顶层状态
function Button() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  return (
    <button
      style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}
      onClick={toggleTheme} // 点击切换主题
    >
      当前主题:{theme}(点击切换)
    </button>
  );
}

👉 关键:Context 传递的 value 可以是对象,包含 state 和修改 state 的函数,深层组件调用函数就能触发顶层状态更新,进而让所有使用该 Context 的组件重新渲染。

3. 指定后备方案默认值

创建 Context 时可以传入 "默认值",当组件树中没有找到对应的 Context.Provider 时,useContext 会返回这个默认值(类似 "降级方案")。

场景:开发组件时,允许用户不提供 Provider,此时使用默认主题。

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

// 1. 创建 Context 时指定默认值:{ theme: 'light' }
const ThemeContext = createContext({ theme: 'light' });

// 组件 A:用户没有用 Provider 包裹
function ComponentA() {
  return <Button />; // 没有 Provider 嵌套
}

// 深层组件:获取默认值
function Button() {
  const { theme } = useContext(ThemeContext);
  // 因为没有 Provider,所以 theme 是默认值 'light'
  return <button style={{ background: theme }}>默认主题按钮</button>;
}

关键:

  • 默认值只有在没有 Provider 时才会生效 ;如果有 Provider,无论 Provider 的 value 是什么,都会覆盖默认值。
  • 默认值的作用是 "兜底",避免组件因获取不到值而报错,适合开发可复用组件时使用。
4. 覆盖组件树一部分的 context

可以在组件树的某个分支上,再套一层 Context.Provider,覆盖父级 Provider 传递的值,实现 "局部数据覆盖"。

场景:整个 App 是深色主题,但某个模块需要单独使用浅色主题。

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

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('dark'); // 全局主题:dark
  return (
    <ThemeContext.Provider value={theme}>
      <GlobalComponent /> {/* 用全局主题 dark */}
      {/* 局部覆盖:套一层 Provider,value 设为 'light' */}
      <ThemeContext.Provider value='light'>
        <LocalModule /> {/* 局部主题:light */}
      </ThemeContext.Provider>
    </ThemeContext.Provider>
  );
}

// 全局组件:获取全局主题 dark
function GlobalComponent() {
  const theme = useContext(ThemeContext);
  return <div>全局主题:{theme}</div>; // 显示 dark
}

// 局部模块:获取局部覆盖的主题 light
function LocalModule() {
  const theme = useContext(ThemeContext);
  return <div>局部主题:{theme}</div>; // 显示 light
}

关键:Context.Provider 可以嵌套,内层 Provider 的值会覆盖外层。组件会获取 "最近的" Provider 传递的值(就近原则)。

5. 在传递对象和函数时优化重新渲染

当 Context 的 value对象或函数时,会有一个坑:每次顶层组件重新渲染,都会创建新的对象 / 函数,导致所有使用该 Context 的组件也跟着重新渲染(即使数据没变化)。此时需要优化,避免不必要的重渲染。

问题示例value 是对象,每次 App 重渲染都会创建新对象,导致 Button 无效重渲染。

复制代码
function App() {
  const [count, setCount] = useState(0);
  // 每次 App 重渲染,都会创建新对象 { theme: 'dark' }
  return (
    <ThemeContext.Provider value={{ theme: 'dark' }}>
      <button onClick={() => setCount(count + 1)}>计数:{count}</button>
      <Button /> {/* 即使 theme 没变化,也会跟着重渲染 */}
    </ThemeContext.Provider>
  );
}

优化方案 :用 useMemo 缓存对象 / 函数,让 value 只在依赖变化时才更新。

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

const ThemeContext = createContext({ theme: 'light' });

function App() {
  const [count, setCount] = useState(0);
  const theme = 'dark';

  // 用 useMemo 缓存对象:只有依赖(theme)变化时,才创建新对象
  const contextValue = useMemo(() => ({
    theme,
    toggleTheme: () => { /* 修改主题的函数 */ }
  }), [theme]); // 依赖只有 theme,count 变化时不会重新创建

  return (
    <ThemeContext.Provider value={contextValue}>
      <button onClick={() => setCount(count + 1)}>计数:{count}</button>
      <Button /> {/* 只有 theme 变化时才重渲染,count 变化时不重渲染 */}
    </ThemeContext.Provider>
  );
}

关键:

  • 传递对象 / 函数时,必须用 useMemo 缓存 value(如果是函数,还可以用 useCallback 单独缓存函数);
  • 依赖数组只包含真正会变化的变量,避免因无关状态变化导致 value 重建,进而引发无效重渲染

useReducer

一、基础语法

useReducer 接收 3 个参数,返回一个数组(类似 useState),结构如下:

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

逐个解释核心概念:

  1. reducer**(状态处理器函数)**:最核心的部分,是一个 "纯函数"(输入相同,输出一定相同,不产生副作用),负责根据 "动作(action)" 计算新的状态。
  • 语法:function reducer(state, action) { /* 计算并返回新状态 */ }
    • state:当前的状态值(类似 useState 的当前状态);
    • action:描述 "要做什么" 的对象,必须包含 type 字段(动作类型,通常是字符串常量),可选包含 payload 字段(动作携带的数据);
    • 返回值:新的状态(React 会用这个新状态重新渲染组件)。
  1. initialArg**(初始状态参数)** :用于指定状态的初始值,具体含义取决于是否传了第 3 个参数 init
    • 没传 initinitialArg 就是状态的初始值(直接使用);
    • 传了 initinitialArg 是给 init 函数的 "参数",初始状态由 init(initialArg) 计算得出。
  1. init?****(初始状态初始化函数,可选) :一个函数,用于 "延迟计算初始状态"(比如从本地存储读取、处理 initialArg 后得到初始状态),调用时机是组件初始渲染时。
  2. dispatch**(动作分发函数)** :useReducer 返回的第二个值,是触发状态更新的 "触发器"。你通过调用 dispatch(action) 来发送一个 "动作",React 会自动调用 reducer 函数,传入当前 state 和这个 action,计算出 新状态并更新组件。

二、核心用法

1. 向组件添加 reducer(基础用法:替代复杂 useState)

当你的状态修改逻辑需要多个 if/else 或状态之间相互依赖时,用 useReducer 替代 useState,让逻辑更清晰。

场景 :实现一个 "计数器",支持 "加、减、重置" 三种操作(比单纯的 count +1 复杂,适合 useReducer)。

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

// 1. 定义 reducer 函数(纯函数:处理状态逻辑)
function countReducer(state, action) {
  // 根据 action.type 决定做什么操作
  switch (action.type) {
    case 'INCREMENT': // 加 1
      return state + 1; // 返回新状态(不要修改原 state!)
    case 'DECREMENT': // 减 1
      return state - 1;
    case 'RESET': // 重置
      return 0;
    default:
      // 遇到未知 action 时,抛出错误(避免笔误)
      throw new Error(`未知的动作类型:${action.type}`);
  }
}

// 2. 组件中使用 useReducer
function Counter() {
  // 初始化状态:初始值为 0(没传 init,initialArg 直接作为初始状态)
  const [count, dispatch] = useReducer(countReducer, 0);

  return (
    <div>
      <p>当前计数:{count}</p>
      {/* 3. 调用 dispatch 分发动作(触发状态更新) */}
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>加 1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>减 1</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>重置</button>
    </div>
  );
}

核心逻辑:

  • 你不用直接修改 count,而是通过 dispatch 发送一个 "动作"(比如 { type: 'INCREMENT' });
  • React 把当前 count 和这个动作传给 countReducer
  • reducer 计算出 新 count 并返回,React 用新状态重渲染组件。

对比 useState:如果用 useState 实现,需要写 3 个独立的修改函数(setCount(count+1)setCount(count-1)setCount(0)),逻辑分散;而 useReducer 把所有状态修改逻辑集中在 reducer 里,更易维护。

2. 实现 reducer 函数(核心规则 + 复杂状态示例)

reduceruseReducer 的灵魂,必须遵守 "纯函数规则",同时要处理 "复杂状态"(比如对象、数组)时,注意 "不可变更新"(不要直接修改原 state,要返回新的状态对象 / 数组)。

(1)reducer 必须遵守的 3 个规则
  1. 纯函数:不修改入参(stateaction 都不能直接改)、不产生副作用(不请求数据、不操作 DOM、不随机生成值);
  2. 必须返回新状态:哪怕状态没变化,也不能返回 undefined(可以返回原 state);
  3. 动作类型(action.type)建议用大写常量(比如 'ADD_ITEM'),避免笔误。
(2)复杂状态示例:管理一个 "待办列表(todos)"

状态是数组对象([{ id: 1, text: '学习 useReducer', done: false }]),支持 "添加、切换完成状态、删除" 操作。

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

// 1. 定义动作类型常量(避免笔误)
const ADD_TODO = 'ADD_TODO';
const TOGGLE_TODO = 'TOGGLE_TODO';
const DELETE_TODO = 'DELETE_TODO';

// 2. 定义 reducer 函数(处理复杂状态:数组对象)
function todoReducer(state, action) {
  switch (action.type) {
    case ADD_TODO:
      // 不可变更新:返回新数组(不修改原 state 数组)
      return [
        ...state, // 复制原有待办项
        {
          id: Date.now(), // 唯一 ID(用时间戳)
          text: action.payload, // 从 action.payload 拿待办文本
          done: false
        }
      ];
    case TOGGLE_TODO:
      // 不可变更新:映射数组,只修改目标项的 done 状态
      return state.map(todo => 
        todo.id === action.payload ? { ...todo, done: !todo.done } : todo
      );
    case DELETE_TODO:
      // 不可变更新:过滤掉要删除的项,返回新数组
      return state.filter(todo => todo.id !== action.payload);
    default:
      throw new Error(`未知动作:${action.type}`);
  }
}

// 3. 组件中使用
function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []); // 初始状态是空数组
  const [text, setText] = useState('');

  const handleAdd = () => {
    if (!text.trim()) return;
    // 分发 ADD_TODO 动作,携带 payload(待办文本)
    dispatch({ type: ADD_TODO, payload: text });
    setText(''); // 清空输入框
  };

  return (
    <div>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="输入待办..."
      />
      <button onClick={handleAdd}>添加</button>
      <ul>
        {todos.map(todo => (
          <li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
            {todo.text}
            <button onClick={() => dispatch({ type: TOGGLE_TODO, payload: todo.id })}>
              {todo.done ? '取消完成' : '标记完成'}
            </button>
            <button onClick={() => dispatch({ type: DELETE_TODO, payload: todo.id })}>
              删除
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

👉 关键注意点:

  • 复杂状态(对象 / 数组)必须 "不可变更新":不能用 state.push()(修改原数组)、state.todo.done = true(修改原对象),要通过扩展运算符(...)、mapfilter 等方法返回新的状态;
  • action.payload 是灵活的:可以是字符串、数字、对象等,用来传递修改状态所需的数据(比如待办文本、待办 ID)。
3. 避免重新创建初始值(使用 init 函数)

如果你的初始状态需要 "复杂计算"(比如从本地存储读取、处理大量数据),直接把计算逻辑写在 initialArg 里,会导致组件每次重渲染时都重新计算一次初始值(虽然 React 会忽略,但浪费性能)。

此时用第 3 个参数 init 函数,让初始状态只计算一次(组件初始渲染时执行,重渲染时不执行)。

场景:从本地存储(localStorage)读取待办列表作为初始状态
复制代码
import { useReducer } from 'react';

// 1. 定义 init 函数:计算初始状态(只执行一次)
function initTodoState(initialArg) {
  // initialArg 是传入的参数(这里是 'todos',本地存储的 key)
  const savedTodos = localStorage.getItem(initialArg);
  // 如果有保存的待办,解析为数组;没有则返回默认空数组
  return savedTodos ? JSON.parse(savedTodos) : [];
}

// 2. 复用之前的 todoReducer
function todoReducer(state, action) { /* ... 同上 ... */ }

function TodoList() {
  // 3. 使用 init 函数:initialArg 是 'todos'(传给 init 函数的参数)
  const [todos, dispatch] = useReducer(todoReducer, 'todos', initTodoState);

  // 可选:监听 todos 变化,同步到本地存储(副作用用 useEffect)
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  // ... 其余代码同上 ...
}

核心优势:

  • initTodoState 只在组件第一次渲染时执行,后续组件重渲染(比如添加、删除待办)时,不会再执行,避免重复计算;
  • 如果初始状态不需要复杂计算,直接传 initialArg 即可(不用 init 函数)。

三、关键补充:dispatch 函数的特性

dispatchuseReducer 返回的 "动作分发器",有两个重要特性你需要知道:

  1. dispatch****函数是稳定的 :组件重渲染时,dispatch 不会重新创建(和 useStatesetState 类似),所以可以安全地作为 useEffectuseCallback 的依赖,不用怕触发无效重渲染;

    useEffect(() => {
    // 可以放心把 dispatch 加入依赖,不会频繁触发
    console.log('dispatch 是稳定的');
    }, [dispatch]);

  2. dispatch****可以传递给子组件 :和 setState 一样,dispatch 可以作为 props 传给子组件,让子组件也能触发状态更新(适合深层组件修改顶层状态);

    // 子组件:接收 dispatch 并使用
    function TodoItem({ todo, dispatch }) {
    return (


  3. {todo.text}
    <button onClick={() => dispatch({ type: TOGGLE_TODO, payload: todo.id })}>
    切换状态
    </button>

  4. );
    }

    // 父组件:传递 dispatch
    function TodoList() {
    const [todos, dispatch] = useReducer(todoReducer, []);
    return (


      {todos.map(todo => (
      <TodoItem key={todo.id} todo={todo} dispatch={dispatch} />
      ))}

    );
    }

四、useReducer vs useState:什么时候该用哪个?

很多时候两者都能实现需求,但选择的核心是 "状态逻辑的复杂度":

|--------|----------------------------------|--------------------------------|
| 场景 | 推荐用 useState | 推荐用 useReducer |
| 状态类型 | 简单值(数字、字符串、布尔值) | 复杂状态(对象、数组),或状态之间相互依赖 |
| 修改逻辑 | 简单(直接赋值,比如 setCount(count+1) ) | 复杂(多个 if/else 、多条件判断、多种操作类型) |
| 代码维护 | 逻辑简单,无需集中管理 | 逻辑分散时需要集中管理(比如多个组件需要修改同一状态) |

简单总结:简单状态用 useState**,复杂状态逻辑用** useReducer

useCallback

一、先基础语法

useCallback 接收 2 个参数,返回一个 "记忆化的函数",结构如下:

复制代码
const memoizedFn = useCallback(fn, dependencies);

逐个解释核心概念:

  1. fn**(要缓存的函数)**:你需要缓存的组件内定义的函数(比如事件处理函数、传给子组件的回调),可以是普通函数、箭头函数,也可以是异步函数。
  2. dependencies**(依赖数组)**:控制函数缓存是否失效的 "开关",React 会浅对比依赖项的前后值:
    • 依赖项无变化:useCallback 返回之前缓存的函数(引用不变);
    • 依赖项有变化:useCallback 重新创建函数,返回新的引用;
    • 依赖数组必须包含 fn 中用到的所有响应式值(props、state、组件内变量 / 函数),否则会拿到 "过时的闭包值"。
  1. memoizedFn**(记忆化的函数)**:缓存后的函数,组件重渲染时若依赖没变化,引用始终不变。

二、核心用

1. 跳过组件的重新渲染(最常用场景)

React 组件默认会在 "自身 state 变化" 或 "接收的 props 变化" 时重新渲染。如果父组件传给子组件的 "函数 props" 每次渲染都重新创建(引用变化),哪怕子组件用 React.memo 包裹(浅对比 props),也会认为 props 变化而无效重渲染。

useCallback 缓存函数,让函数引用稳定,配合 React.memo,就能跳过子组件的无效重渲染。

场景:父组件传递事件处理函数给子组件,避免子组件频繁重渲染。

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

// 子组件:用 memo 包裹,浅对比 props(只有 props 真正变化时才重渲染)
const ChildButton = memo(({ onClick, label }) => {
  console.log(`子组件 "${label}" 渲染了`); // 仅在 onClick 或 label 变化时打印
  return <button onClick={onClick}>{label}</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // 用 useCallback 缓存函数:只有依赖变化时才重新创建函数
  const handleClick = useCallback(() => {
    console.log('点击了按钮');
  }, []); // 空依赖:函数永久缓存,引用不变

  return (
    <div>
      <p>父组件计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>父组件计数+1</button>
      {/* 传递缓存后的函数给子组件 */}
      <ChildButton onClick={handleClick} label="测试按钮" />
    </div>
  );
}

👉关键效果:

  • 点击 "父组件计数 + 1" 时,父组件的 count 变化导致父组件重渲染,但 handleClickuseCallback 缓存(依赖为空,引用不变);
  • 子组件 ChildButtonReact.memo 浅对比 onClick 引用没变化,所以不重新渲染,实现性能优化。

❌ 反例(不推荐):如果不用 useCallback,每次父组件重渲染都会创建新的 handleClick 函数(引用变化),哪怕函数逻辑没变,子组件也会跟着重渲染,造成无效开销。

2. 从记忆化回调中更新 state

当你需要在缓存的函数中更新 state,且 state 更新依赖 "前一次的 state" 时,有两种安全方式:要么用 "函数式更新"(无需依赖 state),要么把 state 加入 useCallback 的依赖数组。

场景:缓存一个 "计数 + 1" 的回调函数,依赖前一次的 count 状态。

方式 1:函数式更新(推荐,无需依赖 state)

如果 state 更新只依赖前一次的值,用 setState(prev => newState) 形式,此时不需要把 state 加入依赖数组,函数引用更稳定。

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

function Counter() {
  const [count, setCount] = useState(0);

  // 函数式更新:prevCount 是最新的前一次状态,无需依赖 count
  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // 空依赖:函数永久缓存

  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={increment}>加 1</button>
    </div>
  );
}
方式 2:依赖 state(当更新需要其他状态 /props 时)

如果 state 更新依赖多个值(比如 countstep),需要把这些依赖加入 useCallback 的依赖数组,确保函数拿到最新值。

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

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1); // 步长状态

  // 依赖 count 和 step:两者变化时,函数重新创建
  const increment = useCallback(() => {
    setCount(count + step); // 依赖 count 和 step
  }, [count, step]); // 必须加入依赖数组

  return (
    <div>
      <p>计数:{count},步长:{step}</p>
      <button onClick={increment}>加 {step}</button>
      <button onClick={() => setStep(step + 1)}>步长+1</button>
    </div>
  );
}

👉 关键注意点:

  • 函数中用到的响应式值(state/props),必须加入 useCallback 的依赖数组,否则会拿到 "过时的闭包值"(比如 count 已经变了,但函数里还是旧值);
  • 能用量化更新就尽量用(减少依赖,让函数更稳定)。
3. 防止频繁触发 Effect

useEffect 会在依赖项变化时执行,如果依赖项是 "每次渲染都重新创建的函数",会导致 useEffect 频繁触发(哪怕函数逻辑没变)。用 useCallback 缓存函数,让函数引用稳定,就能避免这种情况。

场景useEffect 依赖一个事件处理函数,只有函数逻辑相关的依赖变化时才触发 Effect。

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

function DataLogger() {
  const [data, setData] = useState('');
  const [logCount, setLogCount] = useState(0);

  // 用 useCallback 缓存日志函数:依赖 data
  const logData = useCallback(() => {
    console.log('当前数据:', data);
    setLogCount(prev => prev + 1);
  }, [data]); // 仅 data 变化时,函数重新创建

  // useEffect 依赖缓存后的 logData
  useEffect(() => {
    console.log('Effect 触发:日志函数更新');
    logData(); // 执行日志函数
  }, [logData]); // 依赖稳定,仅 data 变化时触发 Effect

  return (
    <div>
      <input
        type="text"
        value={data}
        onChange={(e) => setData(e.target.value)}
        placeholder="输入数据..."
      />
      <p>日志触发次数:{logCount}</p>
    </div>
  );
}

👉 关键效果:

  • 只有 data 变化时,logData 才会重新创建,useEffect 才会触发;
  • 如果不用 useCallback,每次组件重渲染(比如输入框输入时)都会创建新的 logData 函数,useEffect 会频繁触发,导致无效日志。
4. 优化自定义 Hook

自定义 Hook 中如果返回函数(比如事件回调、订阅函数),这些函数会在每次调用 Hook 时重新创建,导致使用 Hook 的组件可能出现无效重渲染。用 useCallback 缓存返回的函数,能让自定义 Hook 更高效、更稳定。

场景:封装一个 "监听窗口大小变化" 的自定义 Hook,返回 "手动刷新尺寸" 的回调函数,避免函数重复创建。

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

// 自定义 Hook:监听窗口大小
function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth });
  const sizeRef = useRef(size); // 用 ref 存储最新尺寸(避免闭包问题)

  // 同步尺寸到 ref(不触发重渲染)
  useEffect(() => {
    sizeRef.current = size;
  }, [size]);

  // 用 useCallback 缓存刷新函数:无依赖,永久稳定
  const refreshSize = useCallback(() => {
    setSize({ width: window.innerWidth });
    console.log('手动刷新尺寸:', window.innerWidth);
  }, []);

  // 监听窗口 resize 事件
  useEffect(() => {
    function handleResize() {
      setSize({ width: window.innerWidth });
    }
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return [size, refreshSize]; // 返回状态和缓存后的回调
}

// 组件使用自定义 Hook
function App() {
  const [size, refreshSize] = useWindowSize();
  console.log('App 渲染');

  return (
    <div>
      <p>窗口宽度:{size.width}px</p>
      <button onClick={refreshSize}>手动刷新尺寸</button>
    </div>
  );
}

👉 关键优化点:

  • 自定义 Hook 返回的 refreshSizeuseCallback 缓存,引用稳定,使用 Hook 的组件(如 App)重渲染时,refreshSize 不会重新创建;
  • 如果其他组件接收 refreshSize 作为 props,配合 React.memo 就能避免无效重渲染,让自定义 Hook 更具复用性和性能。

三、关键补充:useCallback 的使用误区

  1. 不要滥用 useCallback
    • useCallback 本身有缓存开销(存储函数引用、对比依赖项),如果函数是组件内部自用(不传给子组件、不作为 Effect 依赖),且逻辑简单,没必要用 useCallback------ 直接定义函数即可,反而更高效;
    • 只有当函数作为 props 传给子组件(且子组件用 React.memo 包裹),或作为 useEffect/ 其他 Hook 的依赖时,才需要用 useCallback
  1. 依赖数组不能漏写
    • 函数中用到的所有响应式值(state/props/ 组件内变量),必须加入依赖数组,否则会出现 "闭包陷阱"(函数里拿到的是旧值);
    • 可以开启 ESLint 的 react-hooks/exhaustive-deps 规则,自动检测漏写的依赖。
  1. useCallback 不能替代 useMemo
    • useCallback 缓存的是 "函数引用",useMemo 缓存的是 "计算结果"(可以是任意类型);
    • 缓存函数用 useCallback,缓存其他值(对象、数组、数字)用 useMemo
  1. 异步函数的缓存
    • 异步函数(async/await)也可以用 useCallback 缓存,只需确保依赖数组包含函数中用到的所有响应式值:

      const fetchData = useCallback(async () => {
      const res = await fetch(/api/data?page=${page});
      const data = await res.json();
      setData(data);
      }, [page]); // 依赖 page

useMemo

一、基础语法

useMemo 接收 2 个参数,返回 "缓存的计算结果",结构如下:

复制代码
const memoizedValue = useMemo(calculateValue, dependencies);

逐个解释核心概念:

  1. calculateValue**(计算函数)** :你要执行的 "代价昂贵的计算逻辑",必须是一个纯函数 (输入相同则输出相同,无副作用),最终返回一个 "需要被缓存的值"(可以是数字、字符串、对象、数组、函数等)。❗ 注意:这个函数会在组件渲染期间执行 ,不要在里面写副作用(比如请求数据、操作 DOM)------ 副作用请用 useEffect
  2. dependencies**(依赖数组)**:控制缓存是否失效的 "开关",React 会浅对比依赖项的前后值:
    • 只有当依赖项中有任一值发生变化时,calculateValue 才会重新执行,返回新结果并更新缓存;
    • 依赖项没变化时,useMemo 直接返回之前缓存的结果,跳过计算;
    • 依赖数组必须包含 calculateValue 中用到的所有响应式值(props、state、组件内定义的变量 / 函数),否则会拿到 "过时的缓存结果"。
  1. memoizedValue**(缓存的结果)** :calculateValue 执行后的结果,会被 React 缓存起来,组件重渲染时若依赖没变化,直接复用这个值。

二、核心用法

1. 跳过代价昂贵的重新计算

这是 useMemo 最核心的用法:当你有 "耗时的计算逻辑"(比如遍历大数据、复杂数学运算、深层数据转换),组件重渲染时(比如无关状态变化),不需要重复执行这些计算,用 useMemo 缓存结果。

场景:过滤并排序一个包含 10000 条数据的列表(计算代价高),只有当 "原始数据" 或 "过滤条件" 变化时才重新计算。

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

function BigList({ data }) {
  const [filterText, setFilterText] = useState('');

  // 代价昂贵的计算:过滤 + 排序 10000 条数据
  const filteredAndSortedData = useMemo(() => {
    console.log('重新计算过滤排序结果(仅依赖变化时执行)');
    return data
      .filter(item => item.name.includes(filterText)) // 过滤
      .sort((a, b) => a.age - b.age); // 排序
  }, [data, filterText]); // 依赖:只有 data 或 filterText 变化时,才重新计算

  return (
    <div>
      <input
        type="text"
        value={filterText}
        onChange={(e) => setFilterText(e.target.value)}
        placeholder="搜索名称..."
      />
      <ul>
        {filteredAndSortedData.map(item => (
          <li key={item.id}>{item.name}({item.age}岁)</li>
        ))}
      </ul>
    </div>
  );
}

// 模拟 10000 条测试数据
const mockData = Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  name: `用户${i}`,
  age: Math.floor(Math.random() * 50)
}));

function App() {
  return <BigList data={mockData} />;
}

👉 关键效果:

  • 当你输入过滤文本(filterText 变化)或 data 变化时,才会重新执行过滤排序;
  • 如果组件因其他原因重渲染(比如父组件传了无关 props),useMemo 直接返回缓存结果,跳过耗时计算,提升页面响应速度。

❌ 反例(不推荐):如果不用 useMemo,每次组件重渲染都会执行 data.filter(...).sort(...),哪怕 datafilterText 都没变化,会造成不必要的性能浪费。

2. 跳过组件的重新渲染

React 组件默认会在 "props 变化" 或 "自身 state 变化" 时重新渲染。如果父组件传递给子组件的 props 是 "每次渲染都会重新创建的对象 / 数组 / 函数"(比如 { a: 1 }[1,2]),哪怕内容没变,子组件也会认为 props 变化而重新渲染。

此时用 useMemo 缓存 props 的值,让 props 只有在内容变化时才重新创建,配合子组件的 React.memo(浅对比 props),就能跳过不必要的子组件重渲染。

场景:父组件传递一个对象给子组件,避免子组件无效重渲染。

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

// 子组件:用 memo 包裹,浅对比 props(只有 props 真正变化时才重渲染)
const Child = memo(({ userInfo }) => {
  console.log('子组件渲染了'); // 仅在 userInfo 内容变化时打印
  return <div>用户名:{userInfo.name},年龄:{userInfo.age}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const name = '张三';
  const age = 25;

  // 用 useMemo 缓存对象:只有 name/age 变化时,才重新创建 userInfo
  const userInfo = useMemo(() => ({
    name,
    age
  }), [name, age]); // 依赖:name/age 不变,userInfo 就不变

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>计数:{count}</button>
      {/* 传递缓存后的 userInfo 给子组件 */}
      <Child userInfo={userInfo} />
    </div>
  );
}

关键效果:

  • 点击 "计数" 按钮时,父组件的 count 变化导致父组件重渲染,但 nameage 没变化,userInfouseMemo 拿到缓存的旧对象(引用不变);
  • 子组件 Childmemo 包裹,浅对比 userInfo 的引用没变化,所以不重新渲染,实现性能优化。

反例(不推荐):如果不用 useMemo,每次父组件重渲染都会创建新的 { name, age } 对象(引用变化),哪怕内容没变,子组件也会重新渲染,造成无效开销。

3. 防止过于频繁地触发 Effect

useEffect 会在依赖项变化时执行,如果依赖项是 "每次渲染都重新创建的对象 / 数组 / 函数",会导致 useEffect 频繁触发(哪怕内容没变)。用 useMemo 缓存依赖项,能避免这种情况。

场景useEffect 依赖一个对象,只有对象内容变化时才执行副作用(比如请求数据)。

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

function DataFetcher() {
  const [page, setPage] = useState(1);
  const [limit, setLimit] = useState(10);

  // 用 useMemo 缓存请求参数对象
  const fetchParams = useMemo(() => ({
    page,
    limit
  }), [page, limit]); // 仅 page/limit 变化时,才重新创建对象

  // useEffect 依赖缓存后的 fetchParams
  useEffect(() => {
    console.log('请求数据(仅参数变化时执行)', fetchParams);
    // 模拟请求数据:fetch(`/api/data?page=${page}&limit=${limit}`)
  }, [fetchParams]); // 依赖项是缓存后的对象,引用稳定

  return (
    <div>
      <button onClick={() => setPage(page + 1)}>下一页</button>
      <button onClick={() => setLimit(limit + 5)}>增加每页条数</button>
    </div>
  );
}

关键效果:

  • 只有 pagelimit 变化时,fetchParams 才会重新创建,useEffect 才会触发请求;
  • 如果不用 useMemo,每次组件重渲染都会创建新的 { page, limit } 对象,useEffect 会频繁触发,导致无效请求。
4. 记忆另一个 Hook 的依赖

很多 Hook(比如 useEffectuseCallbackuseMemo 本身)都需要依赖数组,若依赖项是 "动态计算的值"(比如对象、数组),直接传入会导致依赖不稳定。用 useMemo 缓存这个依赖项,能让 Hook 正常工作。

场景useCallback 的依赖是一个动态计算的数组,用 useMemo 缓存数组。

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

function Demo() {
  const [ids, setIds] = useState([1, 2, 3]);
  const [prefix, setPrefix] = useState('item_');

  // 动态计算数组:给每个 id 加前缀
  const prefixedIds = useMemo(() => {
    return ids.map(id => `${prefix}${id}`);
  }, [ids, prefix]); // 仅 ids/prefix 变化时重新计算

  // useCallback 的依赖是缓存后的 prefixedIds(引用稳定)
  const handleClick = useCallback(() => {
    console.log('处理点击:', prefixedIds);
  }, [prefixedIds]); // 依赖稳定,useCallback 不会频繁重建

  return (
    <div>
      <button onClick={handleClick}>触发回调</button>
      <button onClick={() => setPrefix('new_item_')}>修改前缀</button>
    </div>
  );
}

关键逻辑:

  • prefixedIds 是动态计算的数组,用 useMemo 缓存后,引用稳定;
  • useCallback 依赖 prefixedIds,只有 prefixedIds 真正变化时,handleClick 才会重新创建,避免无效重渲染。
5. 记忆一个函数

虽然 useMemo 主要用于缓存 "计算结果",但也可以缓存函数(返回一个函数作为计算结果)。不过更推荐用 useCallback 缓存函数(useCallback 本质是 useMemo 的语法糖:useCallback(fn, deps) = useMemo(() => fn, deps)),仅在特殊场景下用 useMemo 记忆函数。

场景:缓存一个需要动态计算依赖的函数(比如函数内部用到动态数组)。

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

function Demo() {
  const [list, setList] = useState([1, 2, 3]);

  // 用 useMemo 缓存函数:函数内部依赖 list(动态数组)
  const processList = useMemo(() => {
    // 函数内部用到 list,list 变化时函数重新创建
    return (factor) => {
      return list.map(item => item * factor);
    };
  }, [list]); // 依赖 list

  return (
    <div>
      <button onClick={() => console.log(processList(2))}>处理列表</button>
      <button onClick={() => setList([4, 5, 6])}>更新列表</button>
    </div>
  );
}

👉 注意:

  • 缓存函数优先用 useCallback,只有当函数需要 "基于动态依赖创建" 时,才考虑 useMemo

  • 上面的例子用 useCallback 改写更简洁:

    const processList = useCallback((factor) => {
    return list.map(item => item * factor);
    }, [list]);

useRef

一、基础语法

useRef 接收 1 个参数 initialValue(初始值),返回一个不可变的 ref 对象,结构如下:

复制代码
const refObj = useRef(initialValue);

核心特性:

  1. ref 对象的结构 :ref 对象只有一个公开属性 current,你可以通过 refObj.current 读取或修改存储的值(比如 refObj.current = '新值');
  2. 跨渲染持久化 :组件每次重渲染时,useRef 返回的都是同一个 ref 对象(引用不变),current 属性存储的值也会一直保留,不会被重置;
  3. 不触发重渲染 :修改 ref.current 的值不会导致组件重新渲染(这是和 useState 最大的区别 ------setState 会触发重渲染);
  4. 初始值可以是任意类型 :可以是 DOM 元素、数字、字符串、对象、函数等,甚至是 null(常用作 DOM 引用的初始值)。

二、核心用法

1. 使用 ref 引用一个值(跨渲染存储普通值)

当你需要存储一个 "跨组件渲染仍需保留" 的值,且修改这个值不需要触发重渲染时,用 useRefuseState 更合适(避免不必要的重渲染)。

常见场景:存储定时器 ID、前一次的状态 /props、临时计算结果等。

场景 1:存储定时器 ID(用于组件卸载时清除)
复制代码
import { useRef, useEffect } from 'react';

function Timer() {
  // 用 ref 存储定时器 ID(跨渲染保留,修改不触发重渲染)
  const timerRef = useRef(null);

  useEffect(() => {
    // 启动定时器,将 ID 存入 ref.current
    timerRef.current = setInterval(() => {
      console.log('定时器运行中...');
    }, 1000);

    // 组件卸载时清除定时器(避免内存泄漏)
    return () => clearInterval(timerRef.current);
  }, []); // 空依赖:只启动一次定时器

  return <div>定时器已启动(查看控制台)</div>;
}

关键逻辑:

  • 定时器 ID 不需要触发组件重渲染,用 useRef 存储比 useState 更高效;
  • 组件卸载时,通过 timerRef.current 拿到定时器 ID,确保能正确清除。
场景 2:存储前一次的状态 /props
复制代码
import { useRef, useState, useEffect } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  // 用 ref 存储前一次的 count(跨渲染保留)
  const prevCountRef = useRef(0);

  useEffect(() => {
    // 每次 count 变化时,更新 ref 存储的前一次值
    prevCountRef.current = count;
  }, [count]); // 依赖 count:count 变化时执行

  return (
    <div>
      <p>当前计数:{count}</p>
      <p>前一次计数:{prevCountRef.current}</p>
      <button onClick={() => setCount(count + 1)}>加 1</button>
    </div>
  );
}

关键逻辑:

  • prevCountRef.current 存储前一次的 count,修改它不会触发重渲染;
  • 通过 useEffect 监听 count 变化,及时更新 ref 中的值,确保拿到正确的 "前一次状态"。
2. 通过 ref 操作 DOM(最常用场景)

useRef 最核心的用途之一是 "获取 DOM 元素的引用",从而直接操作 DOM(比如聚焦输入框、修改 DOM 样式、获取 DOM 尺寸等)------ 这是 React 中少数 "直接操作 DOM" 的合法场景。

核心流程

  1. useRef(null) 创建 ref 对象;
  2. 在目标 DOM 元素上添加 ref 属性,值为创建的 ref 对象(React 会自动将 DOM 元素赋值给 ref.current);
  3. 在组件渲染完成后(比如 useEffect 中),通过 ref.current 访问并操作 DOM。
场景 1:聚焦输入框(页面加载后自动聚焦)
复制代码
import { useRef, useEffect } from 'react';

function InputFocus() {
  // 1. 创建 ref 对象(初始值为 null)
  const inputRef = useRef(null);

  useEffect(() => {
    // 2. 组件渲染完成后,inputRef.current 就是输入框 DOM 元素
    inputRef.current.focus(); // 操作 DOM:聚焦输入框
  }, []); // 空依赖:只执行一次(组件初始渲染后)

  return (
    // 3. 将 ref 绑定到 DOM 元素
    <input ref={inputRef} placeholder="页面加载后自动聚焦..." />
  );
}
场景 2:获取 DOM 元素的尺寸(比如宽度、高度)
复制代码
import { useRef, useEffect, useState } from 'react';

function DOMSize() {
  const [width, setWidth] = useState(0);
  // 创建 ref 绑定到 div 元素
  const divRef = useRef(null);

  useEffect(() => {
    // 组件渲染完成后,获取 DOM 尺寸
    setWidth(divRef.current.offsetWidth);

    // 可选:监听窗口 resize,更新尺寸
    function handleResize() {
      setWidth(divRef.current.offsetWidth);
    }
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <div ref={divRef} style={{ width: '50%', height: '100px', background: '#f0f0f0' }}>
      这个 div 的宽度是:{width}px
    </div>
  );
}

关键注意点:

  • 必须在 "组件渲染完成后" 操作 DOM:useEffect 中执行(useEffect 的回调在组件渲染到 DOM 后执行),如果直接在组件顶层访问 ref.current,会得到 null(此时 DOM 还没渲染);
  • 不要滥用 DOM 操作:React 推荐通过状态(useState)控制 DOM,只有状态无法实现时(比如聚焦、获取尺寸),才用 ref 直接操作 DOM。
3. 避免重复创建 ref 的内容

如果 useRef 的初始值是 "代价昂贵的对象 / 数组 / 函数"(比如复杂对象、大数组),直接写在 initialValue 里,会导致组件每次重渲染时都重新创建这个初始值 (虽然 useRef 会忽略重渲染时的新初始值,只在第一次渲染时使用,但仍会造成不必要的性能浪费)。

解决方案:用 useMemo 或条件判断,确保初始值只创建一次。

场景:ref 初始值是复杂对象(避免重复创建)
复制代码
import { useRef, useMemo } from 'react';

function ExpensiveRef() {
  // 错误示例:每次重渲染都会创建新的复杂对象(浪费性能)
  // const dataRef = useRef({ name: '张三', age: 25, list: Array(10000).fill(0) });

  // 正确示例:用 useMemo 缓存初始值,只创建一次
  const initialData = useMemo(() => {
    return {
      name: '张三',
      age: 25,
      list: Array(10000).fill(0) // 代价昂贵的大数组
    };
  }, []); // 空依赖:只创建一次

  const dataRef = useRef(initialData);

  return <div>ref 存储复杂对象</div>;
}

关键逻辑:

  • useMemo 缓存初始值,确保复杂对象只在组件第一次渲染时创建,后续重渲染时复用;
  • 如果初始值是简单类型(数字、字符串、null),无需缓存,直接传入 useRef 即可(比如 useRef(0)useRef(null))。
相关推荐
小飞大王6661 小时前
JavaScript基础知识总结(六)模块化规范
开发语言·javascript·ecmascript
白龙马云行技术团队1 小时前
前端自适应动态架构图演进
前端
一枚前端小能手1 小时前
🎬 使用 Web 动画 API - 关键帧与交互控制实战指南
前端·javascript·api
西西学代码2 小时前
Flutter---异步编程
开发语言·前端·javascript
拉不动的猪2 小时前
CSS 像素≠物理像素:0.5px 效果的核心密码是什么?
前端·css·面试
前端市界2 小时前
Copilot新模型GPT-5.1太强了!自动生成完美Axios封装,同事都看傻了
前端·前端框架·github
米欧2 小时前
取消当前正在进行的所有接口请求
前端·javascript·axios
浪里行舟2 小时前
告别“拼接”,迈入“原生”:文心5.0如何用「原生全模态」重塑AI天花板?
前端·javascript·后端
OpenTiny社区2 小时前
救命!这个低代码工具太香了 ——TinyEngine 物料自动导入上手
前端·低代码·github