[前端-React] Hooks

目录

useState

一、基础语法

二、核心用法

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

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

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

(1)更新对象状态

(2)更新数组状态

4. 避免重复创建初始状态

5. 使用 key 重置状态

6. 存储前一次渲染的信息

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

useEffect

一、基础语法

二、核心用法

1. 连接到外部系统

2. 在自定义 Hook 中封装 Effect

3. 控制非 React 小部件

4. 使用 Effect 请求数据

5. 指定响应式依赖项

6. 在 Effect 中根据先前 state 更新 state

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

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

9. 从 Effect 读取最新的 props 和 state

useContext

一、基础 语法

二、解释核心用法

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

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

3. 指定后备方案默认值

4. 覆盖组件树一部分的 context

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

useReducer

一、基础语法

二、核心用法

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

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

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

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

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

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

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

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

useCallback

一、先基础语法

二、核心用

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

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

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

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

3. 防止频繁触发 Effect

4. 优化自定义 Hook

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

useMemo

一、基础语法

二、核心用法

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

2. 跳过组件的重新渲染

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

4. 记忆另一个 Hook 的依赖

5. 记忆一个函数

useRef

一、基础语法

二、核心用法

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

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

场景 2:存储前一次的状态 /props

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

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

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

3. 避免重复创建 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); // 只会触发一次重渲染
};
  1. 函数式更新的优先级:如果多次调用函数式更新,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]);
  1. dispatch 可以传递给子组件:和 setState 一样,dispatch 可以作为 props 传给子组件,让子组件也能触发状态更新(适合深层组件修改顶层状态);
// 子组件:接收 dispatch 并使用
function TodoItem({ todo, dispatch }) {
  return (
    <li>
      {todo.text}
      <button onClick={() => dispatch({ type: TOGGLE_TODO, payload: todo.id })}>
        切换状态
      </button>
    </li>
  );
}

// 父组件:传递 dispatch
function TodoList() {
  const [todos, dispatch] = useReducer(todoReducer, []);
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} dispatch={dispatch} />
      ))}
    </ul>
  );
}

四、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))。

Umi Max内置的核心工具 / Hooks

工具 / Hooks核心作用对应代码场景
history(Umi 封装的路由工具)管理浏览器路由,实现路由跳转、参数传递、历史记录操作(替代 React Router 原生的 useHistory)

节点点击跳转:history.push(/sponsorship/${node.data.userId}),跳转到指定用户的协作视图;

useIntl(国际化 Hooks)处理项目多语言切换,实现文案的国际化渲染(基于 react-intl 封装,更适配 Umi 生态)页面标题国际化:intl.formatMessage({ id: 'pages.sponsor.title' }, { ...参数 }),根据语言环境显示中文或英文。
useModel(全局状态管理 Hooks)读取 / 操作 Umi Max 内置的全局模型(Model)数据,实现跨组件状态共享(类似 Redux、Zustand 的简化版)获取全局用户信息:const { initialState } = useModel('@@initialState'),拿到当前登录用户的 userId、userName
useParams(URL 参数获取 Hooks)快速获取当前路由的动态参数(替代 React Router 原生的 useParams)1. 从 URL 中取用户 ID:const params = useParams<{ userId?: string }>(),比如 URL 是 /sponsorship/123,则 params.userId = '123';2. 动态切换协作视图:如果 URL 有 userId 参数,就展示该用户的协作关系;没有则展示当前登录用户的视图。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值