【React】 Hooks useTransition 解析与性能优化实践

1.背景

  1. useTransition 是 React 18 引入的一个并发模式下的 Hook,用于区分紧急和非紧急的状态更新,提升应用的响应性和用户体验;
  2. 它可以管理 UI 中的过渡状态,特别是在处理长时间运行的状态更新时。
  3. 它允许你将某些更新标记为“过渡”状态,这样 React 可以优先处理更重要的更新,比如用户输入,同时延迟处理过渡更新。
  4. 以下是其核心要点、使用场景和注意事项:

2.核心功能

  1. 标记过渡状态:将非紧急的状态更新标记为“过渡”(Transition),允许 React 延迟处理这些更新,优先处理高优先级任务(如用户交互)。
  2. 保持 UI 响应:在状态转换期间,组件仍能保持响应,避免界面卡顿。
  3. 提供加载状态:通过 isPending 指示过渡任务是否正在进行,可配合加载指示器(如 Spinner)提升用户体验。

3.基本用法

3.1 eg:

import { useTransition } from 'react';
 
function MyComponent() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);
 
  const handleClick = () => {
    startTransition(() => {
      setCount(c => c + 1); // 标记为过渡更新
    });
  };
 
  return (
    <div>
      {isPending && <Spinner />} {/* 显示加载指示器 */}
      <button onClick={handleClick}>Increment</button>
      <p>Count: {count}</p>
    </div>
  );
}

3.1 参数

useTransition 不需要任何参数

3.2 返回值

useTransition 返回一个数组 包含两个元素[isPending, startTransition]

  1. isPending:布尔值,表示过渡任务是否正在进行。
  2. startTransition:函数,用于包裹低优先级的状态更新。

4、使用场景

4.1 大量数据渲染

例如,过滤或排序大型列表时,避免界面卡顿。

function DataGrid() {
  const [data, setData] = useState([]);
  const [isPending, startTransition] = useTransition();
  const [filter, setFilter] = useState('');

  const handleFilterChange = (newFilter) => {
    setFilter(newFilter);
    startTransition(() => {
      const filteredData = processLargeDataSet(newFilter);
      setData(filteredData);
    });
  };

  return (
    <div>
      <input value={filter} onChange={e => handleFilterChange(e.target.value)} />
      {isPending ? <LoadingGrid /> : <VirtualizedGrid data={data} />}
    </div>
  );
}

4.2 路由切换

预加载下一页数据时保持当前页响应。

function App() {
  const [isPending, startTransition] = useTransition();
  const [currentPage, setCurrentPage] = useState('home');

  const navigate = (page) => {
    startTransition(() => {
      setCurrentPage(page);
    });
  };

  return (
    <div>
      <Navigation onNavigate={navigate} />
      {isPending ? <PageTransitionSpinner /> : <Page name={currentPage} />}
    </div>
  );
}

4.3 表单验证

实时响应用户输入,同时延迟更新验证结果。

function ComplexForm() {
  const [formData, setFormData] = useState({});
  const [errors, setErrors] = useState({});
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
    startTransition(() => {
      const validationErrors = validateFormField(name, value);
      setErrors(prev => ({ ...prev, [name]: validationErrors }));
    });
  };

  return (
    <form>
      <input name="email" onChange={handleChange} value={formData.email || ''} />
      {isPending ? <ValidatingIndicator /> : (errors.email && <ErrorMessage error={errors.email} />)}
    </form>
  );
}

4.4 搜索或筛选功能

实时响应用户输入,同时延迟更新结果展示。

function SearchResults() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = (e) => {
    setQuery(e.target.value);
    startTransition(() => {
      const searchResults = performSearch(e.target.value);
      setResults(searchResults);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending ? <div>Loading...</div> : (
        <ul>
          {results.map(result => (
            <li key={result.id}>{result.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

5. 场景模拟

创建了一个简单的输入框和一个列表,用于展示基于输入关键词的结果。

mockjs文档地址:https://github.com/nuysoft/Mock/wiki/Getting-Started

5.1 安装使用到的插件

pnpm add mockjs antd 
pnpm add -D @types/mockjs @type/node

package.json

{
  ....
  "dependencies": {
    "antd": "^5.24.9",
    "mockjs": "^1.1.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@types/mockjs": "^1.0.10",
    "@types/node": "^22.15.3",
    "@types/react": "^19.1.2",
    .....
  }
}

5.2 编写 vite.config.ts

结合 vite插件实现一个api, 这个api可以帮助我们模拟数据。

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import type { Plugin } from 'vite'
import mockjs from 'mockjs'
// 使用 URL 模块解析 URL
import url from 'node:url'

/**
 * 创建一个 Vite Mock 服务器插件
 * @returns Vite 插件实例
 */
const viteMockServer = (): Plugin => {

  return {
    // 插件名称
    name: 'vite-plugin-mock',
    // 配置开发服务器
    configureServer(server) {
      // 添加中间件处理 mock 请求
      server.middlewares.use('/api/mock/list', (req, res) => {
        // 解析请求 URL 中的查询参数
        const parseurl = url.parse(req.originalUrl ?? '', true).query;
        // 设置响应头为 JSON 格式
        res.setHeader('Content-Type', 'application/json');
        // 使用 mockjs 生成模拟数据
        const data = mockjs.mock({
          'list|2000': [
            {
              id: '@guid',
              // name: '@cword(2, 4)',
              name: parseurl.key,
              age: '@integer(18, 30)',
              address: '@county(true)',
            },
          ],
        });
        // 将数据转换为 JSON 字符串并发送响应
        res.end(JSON.stringify(data));
      })
    }
  }
}

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), viteMockServer()],
})

编写完成访问我们的接口 http://localhost:5174/api/list?keyWord=xx 5174为默认端口,可以自行更改,返回数据如下

{
    "list": [
        {
            "id": "DCe1D11e-8D24-31e6-fE76-5AbAA6cf7E6F",
            "name": "a",
            "age": 25,
            "address": "西藏自治区 日喀则地区 仲巴县"
        },
        {
            "id": "EAb5ffb6-b43B-93cC-cDfb-18A7Ce15BFda",
            "name": "a",
            "age": 21,
            "address": "安徽省 马鞍山市 花山区"
        }
        .......
   ]
}

5.3 业务组件编写

App.tsx

  1. 输入框和状态管理 使用 useState Hook 管理输入框的值和结果列表。 每次输入框的内容变化时,handleInputChange 函数会被触发,它会获取用户输入的值,并进行 API 请求。
  2. API 请求 在 handleInputChange 中,输入的值会作为查询参数发送到 /api/list API。API 返回的数据用于更新结果列表。 为了优化用户体验,我们将结果更新放在 startTransition 函数中,这样 React 可以在处理更新时保持输入框的响应性。
  3. 使用 useTransition useTransition 返回一个布尔值 isPending,指示过渡任务是否仍在进行中。 当用户输入时,如果正在加载数据,我们会显示一个简单的“loading…”提示,以告知用户当前操作仍在进行。
  4. 列表渲染 使用 List 组件展示返回的结果,列表项显示每个结果的 name 和 address。
// 使用 React 和 Ant Design 组件实现一个模拟带搜索功能的列表
import { Input, List, Spin } from 'antd'
import { useState, useTransition, useCallback } from 'react'

interface ListItem {
  id: string;
  name: string;
  address: string;
  age: number;
}

function App() {
  const [list, setList] = useState<ListItem[]>([]);
  const [inputValue, setInputValue] = useState('');
  const [isPending, startTransition] = useTransition();

  // 处理输入框变化,使用 useCallback 优化性能
  const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setInputValue(value);

    // 发起请求获取数据
    fetch(`/api/mock/list?key=${value}`)
      .then((res) => res.json())
      .then((res) => {
        // 使用 startTransition 包裹状态更新,优化大数据渲染性能
        startTransition(() => {
          setList(res.list)
        });
      })
      .catch(err => {
        console.error('获取数据失败:', err);
      });
  }, []);

  return (
    <>
      <Input 
        placeholder="请输入搜索关键词"
        value={inputValue} 
        onChange={handleInputChange} 
      />
      {isPending ? (
        <Spin tip="加载中..." />
      ) : (
        <List
          dataSource={list}
          renderItem={(item) => (
            <List.Item>{item.address}</List.Item>
          )}
        />
      )}
    </>
  );
}

export default App;

5.4 测试使用

为了更好的测试结果可以在性能中降级 cpu 渲染速度

在这里插入图片描述

6. 注意事项

startTransition必须是同步的

错误做法

startTransition(() => {
  // ❌ 在调用 startTransition 后更新状态
  setTimeout(() => {
    setPage('/about');
  }, 1000);
}); 

正确做法

setTimeout(() => {
  startTransition(() => {
    // ✅ 在调用 startTransition 中更新状态
    setPage('/about');
  });
}, 1000);

async await 错误做法

startTransition(async () => {
  await someAsyncFunction();
  // ❌ 在调用 startTransition 后更新状态
  setPage('/about');
});

正确做法

await someAsyncFunction();
startTransition(() => {
  // ✅ 在调用 startTransition 中更新状态
  setPage('/about');
});

7. 原理剖析

useTransition 的核心原理是将一部分状态更新处理为低优先级任务,这样可以将关键的高优先级任务先执行,而低优先级的过渡更新则会稍微延迟处理。这在渲染大量数据、进行复杂运算或处理长时间任务时特别有效。React 通过调度机制来管理优先级:

  1. 高优先级更新:直接影响用户体验的任务,比如表单输入、按钮点击等。
  2. 低优先级更新:相对不影响交互的过渡性任务,比如大量数据渲染、动画等,这些任务可以延迟执行。
  +-----------------------+
  |         App           |
  |                       |
  |  +--------------+     |
  |  |    Input     |     |
  |  +--------------+     |
  |                       |
  |  +--------------+     |
  |  |   Display    |     |
  |  +--------------+     |
  +-----------------------+

  用户输入
  |
  v
  [高优先级更新] ---> [调度器] ---> [React 更新组件]
  |
  +---> [低优先级过渡更新] --> [调度器] --> [等待处理]

8.扩展

useTransition 与防抖的区别?

seTransition与 防抖在前端开发中都是用于优化性能的手段,但它们的核心原理、应用场景和实现方式存在显著区别,以下是详细对比:

核心原理对比

  • useTransition
    • 基于React的并发模式(Concurrent Features),通过将状态更新标记为“过渡”(Transition)实现。
    • 允许React在后台处理低优先级任务,优先响应高优先级操作(如用户输入),从而保持界面流畅。
    • 更新过程可中断,避免长时间阻塞主线程,提升用户体验。
  • 防抖(Debounce)
    • 延迟函数执行,直到事件触发后的一段时间内没有再次触发。
    • 适用于需要等待用户停止操作后再执行的场景(如搜索输入、表单验证)。

应用场景对比

  • useTransition适用场景
    • 大数据列表过滤:当用户输入搜索内容时,通过useTransition将筛选任务标记为低优先级,避免阻塞输入框的实时更新,保持输入流畅性。
    • 复杂UI更新:在渲染需要消耗大量时间的页面时,使用useTransition优化视图切换体验。
    • 表单提交与筛选器切换:需要延迟渲染的操作(如异步数据加载)中,useTransition可确保用户交互的即时响应。
  • 防抖适用场景
    • 搜索框输入检测:等待用户停止输入后再执行搜索,减少无效请求。
    • 手机号和邮箱验证:在用户停止输入后验证格式,避免实时验证的性能开销。
    • 窗口大小变化后的重新渲染:在窗口调整结束后执行布局计算,避免频繁重绘。

优缺点对比

  • useTransition优点
    • 更新协调过程可中断,避免长时间阻塞主线程。
    • 用户操作可及时得到响应,提升交互体验。
    • 不需要开发者手动配置时间间隔,React自动优化。
  • useTransition缺点
    • 仅适用于React 18及以上版本,且需配合并发模式使用。
    • 对于简单场景,可能引入不必要的复杂性。
  • 防抖优点
    • 可确保在用户停止操作后执行函数,减少无效请求。
    • 实现简单,适用于等待用户输入的场景。
  • 防抖缺点
    • 可能导致用户输入长时间得不到响应。
    • 无法处理需要即时反馈的操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值