Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

hook:useDeferredValue

  • React 18 新增的并发特性 Hook,用于延迟更新某个值
  • 核心作用: 让紧急更新(如输入框)优先渲染,延迟非紧急更新(如列表)
  • 原理: 返回一个延迟版本的值,在紧急更新完成后才更新

基本语法

const deferredValue = useDeferredValue(value);

// value: 原始值
// deferredValue: 延迟更新的值(在紧急更新完成后才更新)

场景代码示例

  • 【场景 1】搜索框优化(最常用):

    • 输入时立即更新输入框,延迟更新搜索结果

      ❌ 不使用 useDeferredValue - 输入卡顿

      import { useState } from "react";
      
      function SearchApp() {
        const [query, setQuery] = useState("");
      
        return (
          <div>
            <input value={query} onChange={(e) => setQuery(e.target.value)} />
            {/* query 变化时,立即渲染大列表,造成卡顿 */}
            <SearchResults query={query} />
          </div>
        );
      }
      
      function SearchResults({ query }) {
        const results = [];
        // 模拟大量计算
        for (let i = 0; i < 10000; i++) {
          if (query && `Item ${i}`.includes(query)) {
            results.push(`Item ${i}`);
          }
        }
        return (
          <ul>
            {results.map((item) => (
              <li key={item}>{item}</li>
            ))}
          </ul>
        );
      }
      

      ✅ 使用 useDeferredValue - 输入流畅

      import { useState, useDeferredValue } from "react";
      
      function SearchApp() {
        const [query, setQuery] = useState("");
        const deferredQuery = useDeferredValue(query); // 延迟 query 的值
      
        return (
          <div>
            <input value={query} onChange={(e) => setQuery(e.target.value)} />
            {/* 使用延迟的值,不阻塞输入 */}
            <SearchResults query={deferredQuery} />
          </div>
        );
      }
      
      function SearchResults({ query }) {
        const results = [];
        for (let i = 0; i < 10000; i++) {
          if (query && `Item ${i}`.includes(query)) {
            results.push(`Item ${i}`);
          }
        }
        return (
          <ul>
            {results.map((item) => (
              <li key={item}>{item}</li>
            ))}
          </ul>
        );
      }
      
    • 效果: 输入框立即响应,搜索结果延迟更新

  • 【场景 2】实时过滤列表(常用):

    • 过滤大列表时保持输入流畅

      import { useState, useDeferredValue, useMemo } from "react";
      
      function ProductFilter() {
        const [filter, setFilter] = useState("");
        const deferredFilter = useDeferredValue(filter);
      
        // 使用延迟的值进行过滤
        const filteredProducts = useMemo(() => {
          return products.filter((p) =>
            p.name.toLowerCase().includes(deferredFilter.toLowerCase()),
          );
        }, [deferredFilter]);
      
        // 判断是否正在延迟更新
        const isStale = filter !== deferredFilter;
      
        return (
          <div>
            <input
              type="text"
              value={filter}
              onChange={(e) => setFilter(e.target.value)}
              placeholder="搜索商品..."
            />
            {isStale && <span> 搜索中...</span>}
      
            <ul style={{ opacity: isStale ? 0.5 : 1 }}>
              {filteredProducts.map((product) => (
                <li key={product.id}>{product.name}</li>
              ))}
            </ul>
          </div>
        );
      }
      
      const products = Array.from({ length: 10000 }, (_, i) => ({
        id: i,
        name: `商品 ${i}`,
      }));
      
  • 【场景 3】图表实时更新(常用):

    • 滑动滑块时,立即更新滑块位置,延迟更新图表

      import { useState, useDeferredValue } from "react";
      
      function ChartWithSlider() {
        const [value, setValue] = useState(50);
        const deferredValue = useDeferredValue(value);
      
        return (
          <div>
            <label>
              当前值: {value}
              <input
                type="range"
                min="0"
                max="100"
                value={value}
                onChange={(e) => setValue(Number(e.target.value))}
              />
            </label>
      
            {/* 图表使用延迟的值,不阻塞滑块移动 */}
            <ExpensiveChart value={deferredValue} />
          </div>
        );
      }
      
      function ExpensiveChart({ value }) {
        // 模拟复杂的图表渲染
        const data = [];
        for (let i = 0; i < 1000; i++) {
          data.push({ x: i, y: Math.sin((i * value) / 100) * 100 });
        }
      
        return (
          <div>
            {/* 复杂的图表组件 */}
            <svg width="500" height="200">
              {data.map((point, i) => (
                <circle key={i} cx={point.x / 2} cy={point.y + 100} r="1" />
              ))}
            </svg>
          </div>
        );
      }
      
  • 【场景 4】表格排序/分页(常用):

    • 切换排序或分页时保持响应

      import { useState, useDeferredValue, useMemo } from "react";
      
      function DataTable({ data }) {
        const [sortKey, setSortKey] = useState("name");
        const [sortOrder, setSortOrder] = useState("asc");
        const deferredSortKey = useDeferredValue(sortKey);
        const deferredSortOrder = useDeferredValue(sortOrder);
      
        const sortedData = useMemo(() => {
          const sorted = [...data].sort((a, b) => {
            if (deferredSortOrder === "asc") {
              return a[deferredSortKey] > b[deferredSortKey] ? 1 : -1;
            }
            return a[deferredSortKey] < b[deferredSortKey] ? 1 : -1;
          });
          return sorted;
        }, [data, deferredSortKey, deferredSortOrder]);
      
        const isStale =
          sortKey !== deferredSortKey || sortOrder !== deferredSortOrder;
      
        return (
          <div>
            <select value={sortKey} onChange={(e) => setSortKey(e.target.value)}>
              <option value="name">按名称</option>
              <option value="price">按价格</option>
              <option value="date">按日期</option>
            </select>
            <button
              onClick={() => setSortOrder(sortOrder === "asc" ? "desc" : "asc")}
            >
              {sortOrder === "asc" ? "升序" : "降序"}
            </button>
      
            {isStale && <div>排序中...</div>}
            <table style={{ opacity: isStale ? 0.5 : 1 }}>
              <tbody>
                {sortedData.map((item) => (
                  <tr key={item.id}>
                    <td>{item.name}</td>
                    <td>{item.price}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        );
      }
      
  • 【场景 5】地图缩放级别(进阶):

    • 拖动地图时,立即响应,延迟更新标记点

      import { useState, useDeferredValue } from "react";
      
      function MapView() {
        const [zoom, setZoom] = useState(10);
        const [center, setCenter] = useState({ lat: 0, lng: 0 });
      
        const deferredZoom = useDeferredValue(zoom);
        const deferredCenter = useDeferredValue(center);
      
        return (
          <div>
            <div>
              <button onClick={() => setZoom(zoom + 1)}>放大</button>
              <button onClick={() => setZoom(zoom - 1)}>缩小</button>
              <span>缩放级别: {zoom}</span>
            </div>
      
            {/* 地图立即响应用户操作 */}
            <Map zoom={zoom} center={center} onMove={setCenter} />
      
            {/* 标记点使用延迟的值,不阻塞地图拖动 */}
            <Markers zoom={deferredZoom} center={deferredCenter} />
          </div>
        );
      }
      
  • 【场景 6】配合 memo 优化子组件(常用):

    • 避免子组件频繁重新渲染

      import { useState, useDeferredValue, memo } from "react";
      
      function App() {
        const [text, setText] = useState("");
        const deferredText = useDeferredValue(text);
      
        return (
          <div>
            <input value={text} onChange={(e) => setText(e.target.value)} />
            {/* 使用 memo 包裹,只有 deferredText 变化时才重新渲染 */}
            <SlowList text={deferredText} />
          </div>
        );
      }
      
      // 使用 memo 优化
      const SlowList = memo(function SlowList({ text }) {
        console.log("SlowList 渲染");
        const items = [];
        for (let i = 0; i < 250; i++) {
          items.push(<SlowItem key={i} text={text} />);
        }
        return <ul>{items}</ul>;
      });
      
      function SlowItem({ text }) {
        const startTime = performance.now();
        while (performance.now() - startTime < 1) {
          // 模拟慢组件(每个组件消耗 1ms)
        }
        return <li>Item: {text}</li>;
      }
      

useDeferredValue vs useTransition

特性useDeferredValueuseTransition
用途延迟某个值的更新标记某个状态更新为低优先级
返回值延迟的值[isPending, startTransition]
控制粒度值级别更新级别
使用方式包裹值包裹 setState
是否需要包裹函数❌ 不需要✅ 需要 startTransition
适用场景父组件传递给子组件的 props在组件内部控制状态更新
加载状态需要手动对比判断提供 isPending

选择原则:

  • 控制传递给子组件的值 → 用 useDeferredValue
  • 控制自己的状态更新 → 用 useTransition
  • 两者可以结合使用

示例对比

// ========== 使用 useDeferredValue ==========
function SearchWithDeferred() {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query); // 延迟 query

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results query={deferredQuery} /> {/* 传递延迟的值 */}
    </>
  );
}

// ========== 使用 useTransition ==========
function SearchWithTransition() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    setQuery(value); // 立即更新输入框

    startTransition(() => {
      // 低优先级更新结果
      const filtered = data.filter((item) => item.includes(value));
      setResults(filtered);
    });
  };

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending && <div>搜索中...</div>}
      <Results data={results} />
    </>
  );
}

什么时候使用 useDeferredValue

场景是否适用原因
搜索框输入输入流畅,结果延迟更新
滑块拖动滑块流畅,图表延迟更新
过滤大列表避免阻塞输入
传递给子组件的 props避免子组件频繁重新渲染
控制自己的状态更新⚠️用 useTransition 更合适
简单的状态更新没必要,过度优化
网络请求应该用其他方式(如 debounce)

注意事项

  • ⚠️ deferredValue 会"落后"于原始值

    const [input, setInput] = useState("");
    const deferredInput = useDeferredValue(input);
    
    // 用户输入 "hello" 时:
    // 1. input 立即变为 "h",deferredInput 还是 ""
    // 2. input 变为 "he",deferredInput 可能还是 "h"
    // 3. ...依此类推
    
    // 可以用来判断是否正在延迟
    const isStale = input !== deferredInput;
    
  • ⚠️ 延迟的值最终会同步

    // React 确保最终 deferredValue 会等于 value
    // 但中间可能会跳过一些值
    
    setValue("a");
    setValue("b");
    setValue("c");
    // deferredValue 可能跳过 'b',直接从 'a' 到 'c'
    
  • ⚠️ 配合 memo 使用更有效

    // ❌ 效果不明显:子组件每次都重新创建
    function Parent() {
      const [text, setText] = useState("");
      const deferredText = useDeferredValue(text);
      return <Child text={deferredText} />; // Child 会因为 Parent 渲染而渲染
    }
    
    // ✅ 效果明显:子组件只在 deferredText 变化时渲染
    function Parent() {
      const [text, setText] = useState("");
      const deferredText = useDeferredValue(text);
      return <MemoChild text={deferredText} />;
    }
    
    const MemoChild = memo(function Child({ text }) {
      return <div>{text}</div>;
    });
    
  • ⚠️ 不要用于对象或数组(引用会变化)

    // ❌ 每次都是新对象,延迟无意义
    const obj = { value: input };
    const deferredObj = useDeferredValue(obj);
    
    // ✅ 使用基础类型或稳定的引用
    const deferredInput = useDeferredValue(input);
    const deferredList = useDeferredValue(memoizedList);
    
  • 可以用来显示加载状态

    const [query, setQuery] = useState("");
    const deferredQuery = useDeferredValue(query);
    const isStale = query !== deferredQuery;
    
    return (
      <div>
        <input value={query} onChange={(e) => setQuery(e.target.value)} />
        {isStale && <Spinner />} {/* 显示加载指示器 */}
        <Results query={deferredQuery} />
      </div>
    );
    
  • 可以嵌套使用

    const deferredA = useDeferredValue(a);
    const deferredB = useDeferredValue(deferredA);
    // deferredB 会比 deferredA 延迟更多
    

实用技巧

  • 技巧 1:配合 useMemo 优化计算

    function ExpensiveList({ filter }) {
      const deferredFilter = useDeferredValue(filter);
    
      // 只有 deferredFilter 变化时才重新计算
      const filteredItems = useMemo(() => {
        return items.filter((item) => item.includes(deferredFilter));
      }, [deferredFilter]);
    
      return (
        <ul>
          {filteredItems.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      );
    }
    
  • 技巧 2:多级延迟

    function MultiLevelDefer() {
      const [value, setValue] = useState(0);
      const deferred1 = useDeferredValue(value);
      const deferred2 = useDeferredValue(deferred1);
    
      return (
        <div>
          <input
            type="number"
            value={value}
            onChange={(e) => setValue(Number(e.target.value))}
          />
          <p>立即: {value}</p>
          <p>延迟1级: {deferred1}</p>
          <p>延迟2级: {deferred2}</p>
        </div>
      );
    }
    
  • 技巧 3:条件性延迟

    function ConditionalDefer({ shouldDefer, value }) {
      // 根据条件决定是否延迟
      const displayValue = shouldDefer ? useDeferredValue(value) : value;
    
      return <ExpensiveComponent value={displayValue} />;
    }
    

与其他优化方案对比

方案原理适用场景优点缺点
useDeferredValue延迟值的更新父传子的 props简单、React 原生需要 React 18
useTransition标记状态更新为低优先级组件内状态更新提供 isPending需要 React 18
debounce延迟函数执行减少 API 调用灵活、通用需要手动清理
throttle限制函数执行频率滚动、resize灵活、通用需要手动清理
memo避免不必要的渲染纯组件优化简单有效只能优化组件层面

浏览器兼容性

  • React 18+ 支持
  • 需要现代浏览器支持(ES2015+)
  • 不支持 IE 11

记住:useDeferredValue 返回一个"落后"的值,让紧急更新优先完成,延迟非紧急更新。非常适合优化需要传递给子组件的、计算密集的 props。