普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月22日首页

React记录之context:useContext、use-context-selector

作者 web_bee
2026年1月22日 17:34

原生context、useContext详解

React 的 Context API 是一种组件间共享数据的机制,它允许你在组件树中传递数据而不必手动逐层传递 props,特别适合"全局"数据的共享(如主题、用户认证信息等)。

基本使用:

创建context:

import { createContext, useContext } from 'react';

export type ThemeType = 'light' | 'dark';

export interface ThemeContextType {
  theme: ThemeType;
  toggleTheme: () => void;
}

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

type ThemeProviderProps = {
  children: React.ReactNode;
} & ThemeContextType;

// 2. 创建 Provider 组件
export const ThemeProvider = ({
  children,
  theme,
  toggleTheme,
}: ThemeProviderProps) => {
  return (
    <ThemeContext.Provider value={{
      theme,
      toggleTheme,
    }}>
      {children}
    </ThemeContext.Provider>
  );
};

// 3. 自定义 Hook(可选,提升可读性)
export const useTheme = () => useContext(ThemeContext);

顶层组件 top.tsx

"use client";

import React, { useState } from 'react';
import { ThemeContext, ThemeContextType, ThemeType } from './context';
import Button from '../../components/button';

function App() {
  const [theme, setTheme] = useState<ThemeType>('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  const value: ThemeContextType = { theme, toggleTheme };

  return (
    <ThemeContext.Provider value={value}>
      <div
        style={{
          padding: '20px',
          background: theme === 'dark' ? '#000' : '#fff',
          color: theme === 'dark' ? '#fff' : '#000'
        }}
      >
        <h1>Current theme: {theme}</h1>
        <Button />
      </div>
    </ThemeContext.Provider>
  );
}

export default App;

Button组件

import React from 'react';
import { useTheme } from '../hook-api/use-context/context';

export default function Button() {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>Toggle Theme {theme}</button>
  );
}

使用场景

  • 全局主题(亮色/暗色模式)
  • 用户认证状态(登录用户信息)
  • 多语言国际化(i18n)
  • 全局配置或状态(如购物车、通知设置)

注意事项:

性能问题:当 Provider 的 value 发生变化时,所有使用该 Context 的子组件都会重新渲染(即使只用到部分字段)。为避免不必要的重渲染:

  • value 拆分为多个 Context;
  • 使用 useMemo 稳定 value 引用;
  • 将不依赖 Context 的子组件提取到 Provider 外部。

不要滥用:Context 不是万能的状态管理工具。对于复杂状态逻辑,建议结合 useReducer 或使用 Redux、Zustand 等状态库。

use-context-selector

use-context-selector 是一个 React 上下文(Context)优化库,它解决了 React 原生 useContext 在性能上的一个关键问题:当上下文值变化时,所有使用该上下文的组件都会重新渲染,即使它们只依赖上下文中的一小部分数据。

核心特性

  1. 选择性订阅:允许组件只订阅上下文中的特定部分数据
  2. 精确更新:只有当下文中的选定部分变化时才会触发组件更新
  3. 与原生Context API兼容:使用方式与React原生Context相似
  4. 轻量级:体积小,对应用包大小影响小

基本使用:

App.tsx

'use client'

import React, { StrictMode } from 'react';
import { MyProvider } from './context';
import CounterA from './components/CounterA';
import CounterB from './components/CounterB';

function App() {
  return (
    <StrictMode>
      <MyProvider>
        <CounterA />
        <CounterB />
      </MyProvider>
    </StrictMode>
  );
}

export default App;

context.tsx

'use client'

import { useState } from 'react';
import{ createContext } from 'use-context-selector';

const MyContext = createContext({} as any);

export function MyProvider({ children }: any) {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);

  const state: any = {
    countA,
    setCountA,
    countB,
    setCountB,
  };

  return (
    <MyContext.Provider value={state}>
      {children}
    </MyContext.Provider>
  );
}

export default MyContext;

CounterA.tsx

'use client'


import React from 'react';
import { useContextSelector, useContext } from 'use-context-selector';
import MyContext from '../context';

function CounterA() {
  const countA = useContextSelector(MyContext, (v) => v.countA);
  const setCountA = useContextSelector(MyContext, (v) => v.setCountA);

  const increment = () =>
    setCountA((s) => s -1);

  console.log('CounterA rendered');

  return (
    <div>
      <p>{new Date().getTime()}</p>
      <p>Counter A: {countA}</p>
      <button onClick={increment}>
        Increment A
      </button>
    </div>
  );
}

export default CounterA;

CounterB.tsx

'use client'

import React from 'react';
import { useContextSelector, useContext } from 'use-context-selector';
import MyContext from '../context';

function CounterB() {
  const countB = useContextSelector(MyContext, (v) => v.countB);
  const setCountB = useContextSelector(MyContext, (v) => v.setCountB);

  const increment = () =>
    setCountB((s) => s -1);

  console.log('CounterB rendered');

  return (
    <div>
      <button onClick={increment}>
        Increment B
      </button>
      <p>{new Date().getTime()}</p>
      <p>Counter B: {countB}</p>
    </div>
  );
}

export default CounterB;
昨天以前首页

React性能优化相关hook记录:React.memo、useCallback、useMemo

作者 web_bee
2026年1月21日 11:12

React.memo

它是什么、做什么的,概念理解

React.memo 是 React 提供的一个高阶组件(Higher-Order Component, HOC) ,用于对函数组件进行浅层记忆化(shallow memoization) ,从而避免在 props 没有变化时进行不必要的重新渲染,提升性能。

怎么用:

import React from 'react';

const MyComponent = React.memo((props) => {
  return <div>{props.value}</div>;
});
  • MyComponent 是一个函数组件。
  • 使用 React.memo 包裹后,React 会在每次父组件重新渲染时,先比较当前 props 和上一次的 props。
  • 如果 props 浅比较相等(shallowly equal) ,则跳过本次渲染,直接复用上次的渲染结果。

⚠️ 注意:React.memo 只对 props 进行比较,不处理 state、context 或 hooks 的变化

浅比较(Shallow Comparison)规则

React.memo 默认使用 浅比较 来判断 props 是否变化:

  • 对于 原始类型(string、number、boolean、null、undefined、symbol) :值相等即视为相同。
  • 对于 对象、数组、函数仅比较引用是否相同(即 ===),即使内容完全一样,只要引用不同,就认为 props 发生了变化。
1. 示例:浅比较失效的情况
function Parent() {
  const [count, setCount] = useState(0);

  // 每次渲染都创建新对象 → 引用不同
  const data = { value: 'hello' };

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <Child data={data} /> {/* Child 会每次都重新渲染! */}
    </>
  );
}

const Child = React.memo(({ data }) => {
  console.log('Child rendered');
  return <div>{data.value}</div>;
});

虽然 data 内容没变,但每次都是新对象,引用不同 → React.memo 无效。

解决方法

  • 使用 useMemo 缓存对象:

    const data = useMemo(() => ({ value: 'hello' }), []);
    
  • 或确保传递的 prop 引用稳定(如使用 useCallback 处理函数)。


自定义比较函数(可选)

你可以传入第二个参数给 React.memo,提供自定义的比较逻辑:

const Child = React.memo(
  ({ a, b, onUpdate }) => {
    return <div>{a} - {b}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true:props 相等,不重新渲染
    // 返回 false:props 不同,需要重新渲染
    return prevProps.a === nextProps.a && prevProps.b === nextProps.b;
    // 注意:通常不比较函数(如 onUpdate),除非你确定它稳定
  }
);

📌 自定义比较函数的返回值含义与 shouldComponentUpdate 相反:

  • true 表示“不需要更新
  • false 表示“需要更新

使用场景:

推荐使用 React.memo 的情况:

  • 组件是 纯展示型(presentational) ,只依赖 props。
  • 组件 渲染开销较大(如包含复杂计算、大量 DOM 节点)。
  • 父组件频繁更新,但该子组件的 props 实际很少变化
  • 配合 useCallback / useMemo 确保传入的函数/对象引用稳定。

不推荐滥用:

  • 组件很小、渲染成本低 → 加 React.memo 反而增加比较开销。
  • props 中包含经常变化的对象/函数,且未做缓存 → React.memo 无效。
  • 组件依赖 Context 或内部有状态(state)→ React.memo 无法阻止因 context/state 变化导致的重渲染。

🔍 注意:React.memo 不能阻止以下情况的重渲染:

  • 组件自身调用 useStateuseReducer 触发更新。
  • 组件消费了 Context,而 Context 的值发生变化。
  • 父组件强制更新(如使用 key 变更)。

注意事项:

  • React.memo 是函数组件的性能优化工具,通过浅比较 props 避免重复渲染。
  • 只对 props 有效,且依赖引用稳定性。
  • 必须配合 useCallback(函数)和 useMemo(对象/数组)才能发挥最大效果。
  • 不要默认给所有组件加 React.memo,应基于性能分析(如 React DevTools Profiler)按需使用。
  • 自定义比较函数可用于复杂场景,但要小心性能开销。

💡 最佳实践:先写出清晰的代码,在发现性能瓶颈后再优化,避免过早优化带来的复杂性。

useCallback

它主要是用来缓存函数本身的; 当组件内的state改变,如果函数依赖没有改变就不重新创建函数;

前置知识:

react 如何触发页面的渲染:

import { useState } from 'react';

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

setState 时会出触发当前页面的重新更新;故当前页面内的所有组件也会 重新渲染;

问题来了,有一些组件可能并不需要重新渲染,可能它传递的props没有改变,但是组件还是会从新渲染;

如何来规避这些组件的无效渲染:

  • useCallback 缓存函数
  • useMemo 缓存函数返回结果(类似vue中的 computed)
  • React.memo 用于对传入的props进行浅比较,true则不刷新页面,false就重新加载组件

什么是 useCallback

useCallback 是 React 提供的一个 Hook,用于优化性能,它能够缓存函数,避免在组件重新渲染时不必要的函数重新创建。

基本语法

const memoizedCallback = useCallback(
  () => {
    // 回调函数体
  },
  [dependencies] // 依赖数组
);
  • 第一个参数:要缓存的函数。
  • 第二个参数:依赖数组(与 useEffect 类似),只有当依赖项发生变化时,才会返回一个新的函数;否则返回之前缓存的函数引用。

为什么需要 useCallback

在 React 中,当组件重新渲染时,其内部的所有函数都会被重新创建。对于传递给子组件的回调函数来说,这意味着:

  1. 每次父组件渲染都会创建一个新的函数实例
  2. 子组件会因为接收到的 props 不同而重新渲染,即使实际内容没有变化
  3. 在依赖数组中使用的函数如果不被缓存,可能导致 effect 无限执行

useCallback 通过缓存函数实例来解决这些问题。

useMemo

useMemo 主要用于缓存计算结果,避免在每次组件渲染时都重复执行开销较大的计算逻辑。

类似于vue中的computed

怎么用:

const memoizedValue = useMemo(() => {
  // 执行昂贵的计算
  return computeExpensiveValue(a, b);
}, [a, b]); // 依赖数组
  • 第一个参数:一个函数,返回需要缓存的值。
  • 第二个参数:依赖数组(deps),只有当数组中的值发生变化时,才会重新执行计算函数;否则返回之前缓存的结果。

核心作用跳过不必要的计算,提升性能。

使用场景:

在函数组件中,每次渲染都会重新执行整个函数体。如果其中有复杂计算(如遍历大数组、深度递归、格式化大量数据等),就会造成性能浪费。

✅ 场景 1:缓存复杂计算结果

const sortedList = useMemo(() => 
  list.sort((a, b) => a.name.localeCompare(b.name)), 
  [list]
);

✅ 场景 2:创建稳定对象/数组引用(配合 React.memo)

const config = useMemo(() => ({
  theme: 'dark',
  lang: 'zh'
}), []); // 确保引用不变,避免子组件不必要重渲染

✅ 场景 3:避免在渲染中创建新实例

// ❌ 每次渲染都新建 Date 对象(虽小但可能影响子组件)
const today = new Date();

// ✅ 如果不需要响应时间变化,可缓存
const today = useMemo(() => new Date(), []);

✅ 场景 4:结合 Context 避免 Provider 不必要更新

const value = useMemo(() => ({ user, updateUser }), [user]);
return <UserContext.Provider value={value}>...</UserContext.Provider>;

防止因 value 引用变化导致所有消费者重渲染。

注意事项与陷阱

⚠️ 1. 不要滥用 useMemo

  • 对于简单计算(如 a + b),使用 useMemo 反而增加内存和比较开销。
  • 先写清晰代码,再根据性能分析(Profiler)决定是否优化

⚠️ 2. 依赖项必须完整且正确

// ❌ 错误:缺少依赖
const result = useMemo(() => expensiveFn(x), []); // x 变化时不会更新!

// ✅ 正确
const result = useMemo(() => expensiveFn(x), [x]);

否则会导致 stale closure(闭包过期) —— 使用的是旧值。

⚠️ 3. 不要用 useMemo 执行副作用

// ❌ 错误:useMemo 不是 useEffect!
useMemo(() => {
  localStorage.setItem('data', JSON.stringify(data));
  return data;
}, [data]);

→ 副作用应放在 useEffect 中。

⚠️ 4. 数组/对象依赖项需稳定

// ❌ 每次渲染都创建新数组 → 依赖永远“变化”
const items = useMemo(() => filter(items, condition), [items, [condition]]);

// ✅ 应确保 condition 是稳定值(如 state 或 useMemo 缓存)

总结

  • useMemo 用于缓存计算结果,避免重复昂贵操作。

  • 它通过依赖数组控制何时重新计算。

  • 主要用于:

    • 优化性能(大计算、大数据处理)
    • 创建稳定对象/数组引用(配合 React.memo
    • 减少 Context Provider 的不必要更新
  • 不要为了优化而优化,优先保证代码可读性。

  • 务必正确填写依赖项,避免 stale closure。

💡 经验法则:当你发现某个计算在组件每次渲染时都执行,且该计算较重或结果用于 props 传递时,考虑 useMemo

❌
❌