Concurrent Mode 与 useTransition 实践

0 阅读3分钟

React 版本:18.x 难度:高级

考察要点

  1. Concurrent Mode 工作原理
  2. useTransition 使用场景
  3. 可中断渲染策略
  4. 用户体验优化

解答:

1. 概念解释

基本定义

  • Concurrent Mode:新的渲染模式,支持可中断更新
  • useTransition:用于标记非紧急更新的 Hook
  • startTransition:将更新标记为可中断

工作原理

  • 将渲染工作分解成小单元
  • 支持更新的优先级排序
  • 允许中断和恢复渲染

应用场景

  • 大量数据渲染
  • 复杂计算处理
  • 页面切换动画
  • 搜索建议实现

2. 代码示例

基础示例:
import React, { useState, useTransition } from 'react';

interface ListProps {
  items: string[];
}

// 👉 模拟耗时计算
const expensiveCalculation = (items: string[]) => {
  console.log('Performing expensive calculation...');
  return items.map(item => {
    let result = item;
    for (let i = 0; i < 1000000; i++) {
      result += 'x';
    }
    return result.slice(0, 10);
  });
};

// 👉 列表组件
const ExpensiveList: React.FC<ListProps> = ({ items }) => {
  const processedItems = expensiveCalculation(items);
  
  return (
    <ul>
      {processedItems.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
};

// 👉 主组件
export const TransitionExample: React.FC = () => {
  const [isPending, startTransition] = useTransition();
  const [searchTerm, setSearchTerm] = useState('');
  const [items, setItems] = useState<string[]>([]);

  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    // 立即更新搜索框
    setSearchTerm(value);
    
    // 将列表更新标记为非紧急
    startTransition(() => {
      const newItems = Array.from({ length: 1000 }, (_, i) => 
        `Item ${i} ${value}`
      );
      setItems(newItems);
    });
  };

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={handleSearch}
        placeholder="Search..."
      />
      
      {isPending ? (
        <div>Loading...</div>
      ) : (
        <ExpensiveList items={items} />
      )}
    </div>
  );
};
进阶示例:
import React, { 
  Suspense, 
  useState, 
  useTransition, 
  useDeferredValue 
} from 'react';

// 👉 数据获取包装器
interface Resource<T> {
  read(): T;
}

function createResource<T>(promise: Promise<T>): Resource<T> {
  let status = 'pending';
  let result: T;
  let error: Error;

  const suspender = promise.then(
    (data) => {
      status = 'success';
      result = data;
    },
    (e) => {
      status = 'error';
      error = e;
    }
  );

  return {
    read() {
      switch (status) {
        case 'pending':
          throw suspender;
        case 'error':
          throw error;
        case 'success':
          return result;
        default:
          throw new Error('Impossible state');
      }
    }
  };
}

// 👉 数据组件
interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile: React.FC<{ resource: Resource<User> }> = ({ resource }) => {
  const user = resource.read();

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
};

// 👉 主应用组件
export const ConcurrentApp: React.FC = () => {
  const [userId, setUserId] = useState<number>(1);
  const [isPending, startTransition] = useTransition();
  const deferredUserId = useDeferredValue(userId);

  // 模拟数据获取
  const fetchUser = (id: number) => {
    return new Promise<User>((resolve) => 
      setTimeout(() => {
        resolve({
          id,
          name: `User ${id}`,
          email: `user${id}@example.com`
        });
      }, 1000)
    );
  };

  const userResource = createResource(fetchUser(deferredUserId));

  const handleUserChange = (newId: number) => {
    startTransition(() => {
      setUserId(newId);
    });
  };

  return (
    <div>
      <div>
        <button 
          onClick={() => handleUserChange(userId - 1)}
          disabled={userId <= 1}
        >
          Previous
        </button>
        <span>User ID: {userId}</span>
        <button 
          onClick={() => handleUserChange(userId + 1)}
        >
          Next
        </button>
      </div>

      {isPending && <div>Loading...</div>}
      
      <Suspense fallback={<div>Loading profile...</div>}>
        <UserProfile resource={userResource} />
      </Suspense>
    </div>
  );
};

3. 注意事项与最佳实践

❌ 常见错误示例

// ❌ 错误示范:在 transition 中更新紧急状态
const BadComponent: React.FC = () => {
  const [isPending, startTransition] = useTransition();
  const [urgentState, setUrgentState] = useState('');

  const handleClick = () => {
    startTransition(() => {
      // 错误:不应在 transition 中更新紧急状态
      setUrgentState('new value');
    });
  };
};

// ❌ 错误示范:不必要的 transition
const AnotherBadComponent: React.FC = () => {
  const [isPending, startTransition] = useTransition();
  
  const handleSimpleClick = () => {
    startTransition(() => {
      // 错误:简单更新不需要 transition
      console.log('clicked');
    });
  };
};

✅ 正确实现方式

// ✅ 正确示范:合理使用 transition
const GoodComponent: React.FC = () => {
  const [isPending, startTransition] = useTransition();
  const [urgentState, setUrgentState] = useState('');
  const [heavyState, setHeavyState] = useState<string[]>([]);

  const handleComplexUpdate = () => {
    // 立即更新紧急状态
    setUrgentState('new value');
    
    // 将耗时更新放入 transition
    startTransition(() => {
      setHeavyState(generateLargeDataSet());
    });
  };
};

// ✅ 正确示范:结合 Suspense 使用
const AnotherGoodComponent: React.FC = () => {
  const [resource, setResource] = useState<Resource<Data>>(initialResource);
  const [isPending, startTransition] = useTransition();

  const handleRefresh = () => {
    startTransition(() => {
      setResource(createResource(fetchData()));
    });
  };

  return (
    <Suspense fallback={<Spinner />}>
      <DataComponent resource={resource} />
    </Suspense>
  );
};

4. 性能优化

import React, { useMemo, useTransition, useDeferredValue } from 'react';

// 👉 优化的搜索实现
const OptimizedSearch: React.FC = () => {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const [isPending, startTransition] = useTransition();

  // 使用 useMemo 缓存搜索结果
  const searchResults = useMemo(() => {
    return performExpensiveSearch(deferredQuery);
  }, [deferredQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <div style={{ opacity: isPending ? 0.5 : 1 }}>
        {searchResults.map(result => (
          <ResultItem key={result.id} data={result} />
        ))}
      </div>
    </div>
  );
};

5. 测试策略

import { render, screen, fireEvent, act } from '@testing-library/react';

describe('TransitionExample', () => {
  it('should show loading state during transition', async () => {
    render(<TransitionExample />);
    
    const input = screen.getByPlaceholderText('Search...');
    
    await act(async () => {
      fireEvent.change(input, { target: { value: 'test' } });
    });

    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  it('should update search results after transition', async () => {
    render(<TransitionExample />);
    
    const input = screen.getByPlaceholderText('Search...');
    
    await act(async () => {
      fireEvent.change(input, { target: { value: 'test' } });
    });

    // 等待 transition 完成
    await screen.findByText(/Item 0 test/);
  });
});

这个实现展示了 Concurrent Mode 和 useTransition 的完整使用方案,包括:

  1. 基础和进阶的实现方式
  2. 错误处理和最佳实践
  3. 性能优化策略
  4. 测试方法

关键是要理解何时使用 transition,以及如何正确处理可中断更新来提升用户体验。