理解 React 的 useMemo Hook

什么是 useMemo

useMemo 是一个 React Hook,用于记忆化(缓存)昂贵计算的结果。它只在依赖项发生变化时重新计算值,从而避免在每次渲染时进行不必要的重新计算。

语法

const memoizedValue = useMemo(() => {
  // 昂贵的计算
  return computeExpensiveValue(a, b);
}, [a, b]); // 依赖项数组

何时使用 useMemo

  1. 昂贵的计算 - 需要时间的复杂计算
  2. 派生值 - 从 props/state 计算得出的值
  3. 引用相等性 - 当你需要相同的对象/数组引用时
  4. 性能优化 - 防止子组件不必要的重新渲染

何时不使用 useMemo

  • 简单计算(开销不值得)
  • 无论如何每次渲染都会改变的值
  • 过早优化(只在有性能问题时才优化)

代码示例

示例 1: 昂贵的计算

import { useMemo, useState } from 'react';

function ExpensiveComponent({ items }: { items: number[] }) {
  const [filter, setFilter] = useState('');

  // ❌ 不好:每次渲染都重新计算
  const expensiveResult = items
    .filter(item => item > 100)
    .map(item => item * 2)
    .reduce((sum, item) => sum + item, 0);

  // ✅ 好:只在 items 改变时重新计算
  const expensiveResult = useMemo(() => {
    console.log('正在计算昂贵的结果...');
    return items
      .filter(item => item > 100)
      .map(item => item * 2)
      .reduce((sum, item) => sum + item, 0);
  }, [items]);

  return (
    <div>
      <input 
        value={filter} 
        onChange={(e) => setFilter(e.target.value)} 
      />
      <p>结果: {expensiveResult}</p>
    </div>
  );
}

示例 2: 派生值

// 来自你的 main.tsx - 这是一个很好的用例!
const doubleColumnMode = useMemo(
  () => openResearchId !== null,
  [openResearchId],
);

// 这确保了 doubleColumnMode 只在 openResearchId 改变时改变
// 尽管计算很简单,但它对引用稳定性很有好处

示例 3: 过滤和排序大型列表

import { useMemo, useState } from 'react';

function UserList({ users }: { users: User[] }) {
  const [searchTerm, setSearchTerm] = useState('');
  const [sortBy, setSortBy] = useState<'name' | 'email'>('name');

  // ✅ 记忆化的过滤和排序列表
  const filteredAndSortedUsers = useMemo(() => {
    console.log('正在过滤和排序用户...');
    
    let result = users.filter(user =>
      user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
      user.email.toLowerCase().includes(searchTerm.toLowerCase())
    );

    result.sort((a, b) => {
      if (sortBy === 'name') {
        return a.name.localeCompare(b.name);
      }
      return a.email.localeCompare(b.email);
    });

    return result;
  }, [users, searchTerm, sortBy]);

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="搜索用户..."
      />
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value as 'name' | 'email')}>
        <option value="name">按姓名排序</option>
        <option value="email">按邮箱排序</option>
      </select>
      
      <ul>
        {filteredAndSortedUsers.map(user => (
          <li key={user.id}>{user.name} - {user.email}</li>
        ))}
      </ul>
    </div>
  );
}

示例 4: 对象/数组属性的引用相等性

import { useMemo } from 'react';

function ParentComponent({ items }: { items: number[] }) {
  const [count, setCount] = useState(0);

  // ❌ 不好:每次渲染都创建新对象
  // ChildComponent 会不必要地重新渲染
  const config = {
    items: items,
    theme: 'dark'
  };

  // ✅ 好:除非 items 改变,否则保持相同的对象引用
  const config = useMemo(() => ({
    items: items,
    theme: 'dark'
  }), [items]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>计数: {count}</button>
      <ChildComponent config={config} />
    </div>
  );
}

// ChildComponent 使用 React.memo 来防止不必要的重新渲染
const ChildComponent = React.memo(({ config }: { config: Config }) => {
  console.log('ChildComponent 已渲染');
  return <div>{config.items.length}</div>;
});

示例 5: 复杂的数据转换

import { useMemo } from 'react';

function Dashboard({ transactions }: { transactions: Transaction[] }) {
  // ✅ 记忆化的统计计算
  const statistics = useMemo(() => {
    const total = transactions.reduce((sum, t) => sum + t.amount, 0);
    const average = total / transactions.length || 0;
    const categories = transactions.reduce((acc, t) => {
      acc[t.category] = (acc[t.category] || 0) + t.amount;
      return acc;
    }, {} as Record<string, number>);
    
    const topCategory = Object.entries(categories)
      .sort(([, a], [, b]) => b - a)[0]?.[0] || 'None';

    return {
      total,
      average,
      count: transactions.length,
      topCategory,
      categories
    };
  }, [transactions]);

  return (
    <div>
      <h2>统计信息</h2>
      <p>总计: ${statistics.total.toFixed(2)}</p>
      <p>平均: ${statistics.average.toFixed(2)}</p>
      <p>交易数: {statistics.count}</p>
      <p>顶级分类: {statistics.topCategory}</p>
    </div>
  );
}

示例 6: 格式化日期或数字

import { useMemo } from 'react';

function ProductCard({ product, locale }: { product: Product; locale: string }) {
  // ✅ 记忆化的格式化值
  const formattedPrice = useMemo(() => {
    return new Intl.NumberFormat(locale, {
      style: 'currency',
      currency: product.currency
    }).format(product.price);
  }, [product.price, product.currency, locale]);

  const formattedDate = useMemo(() => {
    return new Intl.DateTimeFormat(locale, {
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    }).format(new Date(product.createdAt));
  }, [product.createdAt, locale]);

  return (
    <div>
      <h3>{product.name}</h3>
      <p>价格: {formattedPrice}</p>
      <p>创建时间: {formattedDate}</p>
    </div>
  );
}

示例 7: 与其他 Hooks 结合使用

import { useMemo, useState, useEffect } from 'react';

function SearchableList({ items }: { items: Item[] }) {
  const [search, setSearch] = useState('');
  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);

  // ✅ 记忆化的过滤项
  const filteredItems = useMemo(() => {
    return items.filter(item => {
      const matchesSearch = item.name.toLowerCase().includes(search.toLowerCase());
      const matchesCategory = !selectedCategory || item.category === selectedCategory;
      return matchesSearch && matchesCategory;
    });
  }, [items, search, selectedCategory]);

  // ✅ 记忆化的唯一分类
  const categories = useMemo(() => {
    return Array.from(new Set(items.map(item => item.category)));
  }, [items]);

  return (
    <div>
      <input
        value={search}
        onChange={(e) => setSearch(e.target.value)}
        placeholder="搜索..."
      />
      <select
        value={selectedCategory || ''}
        onChange={(e) => setSelectedCategory(e.target.value || null)}
      >
        <option value="">所有分类</option>
        {categories.map(cat => (
          <option key={cat} value={cat}>{cat}</option>
        ))}
      </select>
      <ul>
        {filteredItems.map(item => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

常见模式

模式 1: 条件派生值

const isEnabled = useMemo(
  () => user && user.permissions.includes('admin'),
  [user]
);

模式 2: 数组/对象创建

const options = useMemo(
  () => items.map(item => ({ label: item.name, value: item.id })),
  [items]
);

模式 3: 昂贵的函数调用

const result = useMemo(
  () => expensiveFunction(data),
  [data]
);

性能提示

  1. 先进行性能分析 - 使用 React DevTools Profiler 来识别性能瓶颈
  2. 不要过度使用 - useMemo 本身也有开销
  3. 包含所有依赖项 - 缺少依赖项可能导致 bug
  4. 与 React.memo 结合使用 - 一起使用以获得最佳效果

与其他 Hooks 的对比

  • useMemo - 记忆化一个
  • useCallback - 记忆化一个函数
  • React.memo - 记忆化一个组件
// useMemo - 用于值
const expensiveValue = useMemo(() => compute(), [deps]);

// useCallback - 用于函数
const handleClick = useCallback(() => {
  doSomething();
}, [deps]);

// React.memo - 用于组件
const MemoizedComponent = React.memo(Component);
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐