React 18 新特性与 Hooks 进阶实战

前言

💡 痛点: React 18 的 Concurrent Mode 改变了什么?useTransition/useDeferredValue 怎么选?Server Components 和 Client Components 如何划分?Suspense 的边界在哪里?

🎯 解决方案: 从新特性→Hooks进阶→状态管理→性能优化→TypeScript→架构模式,系统掌握 React 18。
#mermaid-svg-T2aLbtSLYf2bswEA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-T2aLbtSLYf2bswEA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T2aLbtSLYf2bswEA .error-icon{fill:#552222;}#mermaid-svg-T2aLbtSLYf2bswEA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T2aLbtSLYf2bswEA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA .marker.cross{stroke:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T2aLbtSLYf2bswEA p{margin:0;}#mermaid-svg-T2aLbtSLYf2bswEA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster-label text{fill:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster-label span{color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster-label span p{background-color:transparent;}#mermaid-svg-T2aLbtSLYf2bswEA .label text,#mermaid-svg-T2aLbtSLYf2bswEA span{fill:#333;color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .node rect,#mermaid-svg-T2aLbtSLYf2bswEA .node circle,#mermaid-svg-T2aLbtSLYf2bswEA .node ellipse,#mermaid-svg-T2aLbtSLYf2bswEA .node polygon,#mermaid-svg-T2aLbtSLYf2bswEA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .rough-node .label text,#mermaid-svg-T2aLbtSLYf2bswEA .node .label text,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape .label,#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape .label{text-anchor:middle;}#mermaid-svg-T2aLbtSLYf2bswEA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .rough-node .label,#mermaid-svg-T2aLbtSLYf2bswEA .node .label,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape .label,#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape .label{text-align:center;}#mermaid-svg-T2aLbtSLYf2bswEA .node.clickable{cursor:pointer;}#mermaid-svg-T2aLbtSLYf2bswEA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA .arrowheadPath{fill:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-T2aLbtSLYf2bswEA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-T2aLbtSLYf2bswEA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T2aLbtSLYf2bswEA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-T2aLbtSLYf2bswEA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T2aLbtSLYf2bswEA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-T2aLbtSLYf2bswEA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster text{fill:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster span{color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-T2aLbtSLYf2bswEA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-T2aLbtSLYf2bswEA rect.text{fill:none;stroke-width:0;}#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape p,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape .label rect,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T2aLbtSLYf2bswEA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-T2aLbtSLYf2bswEA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-T2aLbtSLYf2bswEA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 状态管理
React 18 新特性
服务端渲染
Hooks 进阶
useMemo

记忆化
useReducer

复杂状态
useCallback

回调缓存
useContext

上下文
useRef

可变引用
Suspense
Suspense

加载边界
Suspense List

编排顺序
并发渲染
Concurrent Mode

并发渲染
useTransition

过渡更新
useDeferredValue

延迟值
React 18 SSR

Streaming SSR
Server Components

服务端组件
useId/useSyncExternalStore

新 Hooks
Zustand

轻量状态
Jotai

原子化
Redux Toolkit

标准实践

React 18 vs React 17 核心差异:

特性 React 17 React 18 收益
并发渲染 ❌ 不支持 ✅ Concurrent Mode 更流畅的 UI
Suspense ⚠️ 基础支持 ✅ Streaming SSR 流式加载
Automatic Batching ❌ 需手动 ✅ 自动批处理 减少渲染
Server Components ❌ 无 ✅ 支持 零客户端 JS
新 Hooks - useTransition/useDeferredValue/useId 更细粒度控制
Strict Mode 部分检查 双重调用+严格检查 开发期暴露问题

一、React 18 新特性

1.1 Concurrent Mode 并发渲染

tsx 复制代码
// ===== 并发渲染原理 =====

// React 18 之前:渲染是同步、不可中断的
// React 18:渲染可以被中断、优先级调度

// setState 不再阻塞 UI
// React 可以同时处理多个更新,根据优先级决定渲染顺序

// 渲染优先级:用户交互 > 过渡更新 > 后台预渲染

import { useState, useTransition } from 'react';

// useTransition:将某些更新标记为"过渡更新"
// 过渡更新可以被更高优先级的交互打断
function SearchResults() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState([]);

  function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
    const newQuery = e.target.value;
    setQuery(newQuery);

    // 将数据库查询标记为过渡更新
    startTransition(() => {
      setResults(searchDatabase(newQuery)); // 可以被打断
    });
  }

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending ? <Spinner /> : <ResultsList results={results} />}
    </div>
  );
}

// useDeferredValue:延迟非紧急更新
function SearchBar() {
  const [text, setText] = useState('');
  // text 作为状态更新
  // deferredText 延迟更新,可能比 text "旧"
  const deferredText = useDeferredValue(text);

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      {/* deferredText 渲染时可能显示旧数据 */}
      <SlowList text={deferredText} />
    </div>
  );
}

// ===== Automatic Batching 自动批处理 =====

// React 18 之前:多个 setState 只有在事件处理函数结束后才批量更新
// React 18:所有 setState 自动批处理(包括 Promise/setTimeout)

// React 17
function handleClick() {
  fetch('/api/user').then(() => {
    setLoading(false);  // 触发一次重渲染
    setError(null);     // 触发一次重渲染(不会合并!)
  });
}

// React 18:自动批处理,fetch 完成后只触发一次重渲染
function handleClick() {
  fetch('/api/user').then(() => {
    setLoading(false);
    setError(null);
    // → 只触发一次重渲染
  });
}

// flushSync:强制同步更新
import { flushSync } from 'react-dom';

function handleUpload(file: File) {
  flushSync(() => {
    setUploading(true);
  });
  // 这个 setState 不会和上面的合并
  flushSync(() => {
    setFile(file);
  });
}

1.2 Suspense 与 Streaming SSR

tsx 复制代码
// ===== Suspense 边界 =====

// Suspense 组件:当子组件还在加载时,显示 fallback
import { Suspense } from 'react';

function App() {
  return (
    <>
      {/* 数据加载 */}
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile userId={userId} />
      </Suspense>

      {/* 多个 Suspense,独立的加载状态 */}
      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts userId={userId} />
      </Suspense>
      
      <Suspense fallback={<CommentsSkeleton />}>
        <UserComments userId={userId} />
      </Suspense>
    </>
  );
}

// ===== SuspenseList 编排 =====

// 控制多个 Suspense 的显示顺序
import { Suspense, SuspenseList } from 'react';

function Dashboard() {
  return (
    <SuspenseList revealOrder="forwards">
      {/* 其他内容先显示 */}
      <Suspense fallback={<HeaderSkeleton />}>
        <DashboardHeader />
      </Suspense>
      
      {/* 按顺序显示 */}
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </SuspenseList>
  );
}

// revealOrder: "together" | "forwards" | "backwards"

// ===== Streaming SSR =====

// 服务端流式渲染:HTML 分块发送
// 1. 服务端立即返回 HTML shell
// 2. JS bundle 加载时,React 在服务端渲染剩余内容
// 3. Streaming 到客户端

// app/routes/page.tsx(Remix/Next.js App Router)
export async function loader() {
  const data = await fetchDashboardData();
  return json(data);
}

// 服务端组件(RSC)
async function DashboardPage() {
  // 服务端直接查询数据
  const data = await getDashboardData();
  
  return (
    <div>
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart data={data.chart} />
      </Suspense>
      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders data={data.orders} />
      </Suspense>
    </div>
  );
}

二、Hooks 进阶

2.1 useState 与状态模式

tsx 复制代码
// ===== useState 进阶用法 =====

import { useState, useCallback } from 'react';

// 1. 函数式更新(基于前一个状态)
const [count, setCount] = useState(0);
setCount(prev => prev + 1);      // ✅ 推荐
setCount(count + 1);             // ⚠️ 可能过时

// 2. 延迟初始化(复杂初始值)
const [data, setData] = useState(() => {
  // 只在首次渲染时执行
  const stored = localStorage.getItem('data');
  return stored ? JSON.parse(stored) : initialData;
});

// 3. 复合状态(相关状态打包)
// ❌ 不推荐:两个相关状态
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ 推荐:打包成对象
const [name, setName] = useState({ first: '', last: '' });
setName(prev => ({ ...prev, first: 'John' }));

// 4. useReducer 复杂状态
type State = { count: number; step: number };
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step };
    case 'decrement':
      return { ...state, count: state.count - state.step };
    case 'reset':
      return { count: 0, step: 1 };
  }
}

const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

// 5. Immer + useReducer(不可变更新简化)
import { useImmerReducer } from 'use-immer';

const [state, dispatch] = useImmerReducer((draft, action) => {
  switch (action.type) {
    case 'updateUser':
      draft.user.name = action.payload.name;
      break;
    case 'addPost':
      draft.posts.push(action.payload);
      break;
  }
}, initialState);

2.2 useEffect 深度解析

tsx 复制代码
// ===== useEffect 依赖控制 =====

import { useEffect, useRef } from 'react';

// 1. 清理函数
useEffect(() => {
  const timer = setInterval(() => {
    setTime(Date.now());
  }, 1000);

  // 返回清理函数
  return () => clearInterval(timer);
}, []);

// 2. 依赖数组控制
// []:只在挂载/卸载时执行
// [value]:value 变化时执行
// 无依赖:每次渲染后都执行(⚠️ 慎用)

// 3. 异步 useEffect(需要包装)
useEffect(() => {
  async function fetchData() {
    const res = await fetch(`/api/user/${userId}`);
    const data = await res.json();
    setUser(data);
  }
  
  fetchData();
}, [userId]);

// 4. 条件执行 useEffect
useEffect(() => {
  if (!userId) return;
  const controller = new AbortController();
  
  fetch(`/api/user/${userId}`, { signal: controller.signal })
    .then(res => res.json())
    .then(setUser);
  
  return () => controller.abort();
}, [userId]);

// 5. Ref 版本(存储最新值,不触发重新执行)
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref.current;
}

// 使用
const prevCount = usePrevious(count);
// prevCount 更新但不会触发额外渲染

2.3 useCallback 与 useMemo

tsx 复制代码
// ===== useCallback vs useMemo =====

import { useState, useCallback, useMemo, memo } from 'react';

// useCallback:缓存函数引用
// 用于:作为 props 传递给子组件、作为 useEffect 依赖、作为事件处理器

const handleClick = useCallback((id: string) => {
  setSelectedId(id);
}, []); // 依赖为空,函数引用永远不变

// useMemo:缓存计算结果
// 用于:复杂计算、对象/数组字面量、组件引用

const sortedItems = useMemo(() => {
  return items
    .filter(item => item.active)
    .sort((a, b) => b.score - a.score);
}, [items]); // items 变化才重新计算

// ===== 配合 memo 优化子组件 =====

// 子组件:只有 props 变化时才重新渲染
const ExpensiveList = memo(function ExpensiveList({ 
  items, 
  onSelect 
}: { 
  items: Item[]; 
  onSelect: (id: string) => void;
}) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onSelect(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

// 父组件:传入 useCallback 包装的函数
function Parent() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState<string | null>(null);

  // items 变化时生成新的 onSelect
  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
  }, []);

  // derivedItems 变化时才重新创建 ExpensiveList
  const derivedItems = useMemo(
    () => items.filter(i => i.type === 'active'),
    [items]
  );

  return (
    <ExpensiveList 
      items={derivedItems} 
      onSelect={handleSelect}
    />
  );
}

// ===== 不要过度优化 =====

// ❌ 过度优化:每次渲染都创建新对象
const style = useMemo(() => ({ color: 'red' }), []);
const options = useMemo(() => ({ limit: 10 }), []);

// ✅ 简单值不需要 useMemo
const style = { color: 'red' };  // 字面量不会变
const options = { limit: 10 };

// ✅ 只有复杂计算或昂贵操作才需要
const sorted = useMemo(() => {
  return bigArray.sort((a, b) => b.timestamp - a.timestamp);
}, [bigArray]);

三、自定义 Hooks

3.1 基础自定义 Hooks

tsx 复制代码
// ===== useLocalStorage =====

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

function useLocalStorage<T>(
  key: string, 
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  
  // SSR 兼容
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue;
    
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  // 同步到 localStorage
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(storedValue));
    } catch (error) {
      console.error('localStorage error:', error);
    }
  }, [key, storedValue]);

  const setValue = useCallback((value: T | ((prev: T) => T)) => {
    setStoredValue(prev => {
      const newValue = value instanceof Function ? value(prev) : value;
      return newValue;
    });
  }, []);

  return [storedValue, setValue];
}

// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');

// ===== useDebounce =====

import { useState, useEffect } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 使用
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) {
      fetchResults(debouncedQuery);
    }
  }, [debouncedQuery]);

  return <input value={query} onChange={e => setQuery(e.target.value)} />;
}

// ===== useFetch =====

import { useState, useEffect } from 'react';

interface FetchState<T> {
  data: T | null;
  error: Error | null;
  isLoading: boolean;
}

function useFetch<T>(url: string): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    error: null,
    isLoading: true,
  });

  useEffect(() => {
    const controller = new AbortController();
    
    async function fetchData() {
      try {
        setState(prev => ({ ...prev, isLoading: true, error: null }));
        
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        
        const data = await res.json();
        setState({ data, error: null, isLoading: false });
      } catch (error: any) {
        if (error.name !== 'AbortError') {
          setState({ data: null, error, isLoading: false });
        }
      }
    }

    fetchData();
    return () => controller.abort();
  }, [url]);

  return state;
}

// 使用
function UserProfile({ userId }: { userId: string }) {
  const { data: user, isLoading, error } = useFetch<User>(
    `/api/users/${userId}`
  );
  
  if (isLoading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;
  return <div>{user.name}</div>;
}

3.2 复杂业务 Hooks

tsx 复制代码
// ===== usePagination =====

import { useState, useMemo } from 'react';

interface PaginationResult<T> {
  currentPage: number;
  pageSize: number;
  total: number;
  totalPages: number;
  currentItems: T[];
  pageNumbers: number[];
  goToPage: (page: number) => void;
  nextPage: () => void;
  prevPage: () => void;
  isFirst: boolean;
  isLast: boolean;
}

function usePagination<T>(
  items: T[], 
  initialPageSize = 10
): PaginationResult<T> {
  const [currentPage, setCurrentPage] = useState(1);
  const [pageSize, setPageSize] = useState(initialPageSize);

  const total = items.length;
  const totalPages = Math.ceil(total / pageSize);

  const currentItems = useMemo(() => {
    const start = (currentPage - 1) * pageSize;
    return items.slice(start, start + pageSize);
  }, [items, currentPage, pageSize]);

  const pageNumbers = useMemo(() => {
    const pages: number[] = [];
    const delta = 2; // 当前页两侧显示的页数
    
    for (let i = 1; i <= totalPages; i++) {
      if (
        i === 1 || 
        i === totalPages ||
        (i >= currentPage - delta && i <= currentPage + delta)
      ) {
        pages.push(i);
      } else if (pages[pages.length - 1] !== '...') {
        pages.push('...' as any);
      }
    }
    
    return pages;
  }, [currentPage, totalPages]);

  const goToPage = (page: number) => {
    setCurrentPage(Math.max(1, Math.min(page, totalPages)));
  };

  const nextPage = () => goToPage(currentPage + 1);
  const prevPage = () => goToPage(currentPage - 1);

  return {
    currentPage,
    pageSize,
    total,
    totalPages,
    currentItems,
    pageNumbers,
    goToPage,
    nextPage,
    prevPage,
    isFirst: currentPage === 1,
    isLast: currentPage === totalPages,
  };
}

// ===== useIntersectionObserver =====

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

interface UseIntersectionObserverOptions {
  threshold?: number;
  root?: Element | null;
  rootMargin?: string;
  freezeOnceVisible?: boolean;
}

function useIntersectionObserver(
  options: UseIntersectionObserverOptions = {}
): [React.RefObject<HTMLDivElement>, boolean] {
  const { threshold = 0, root = null, rootMargin = '0px', freezeOnceVisible = false } = options;
  
  const ref = useRef<HTMLDivElement>(null);
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    if (freezeOnceVisible && isVisible) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        setIsVisible(entry.isIntersecting);
      },
      { threshold, root, rootMargin }
    );

    observer.observe(element);
    return () => observer.disconnect();
  }, [threshold, root, rootMargin, freezeOnceVisible, isVisible]);

  return [ref, isVisible];
}

// 使用:无限滚动
function InfiniteList() {
  const [ref, isVisible] = useIntersectionObserver({ threshold: 0.1 });
  const [items, setItems] = useState(initialItems);

  useEffect(() => {
    if (isVisible && hasMore) {
      loadMore();
    }
  }, [isVisible]);

  return (
    <div>
      {items.map(item => <Item key={item.id} data={item} />)}
      <div ref={ref}>
        {isLoading && <Spinner />}
      </div>
    </div>
  );
}

四、状态管理

4.1 Zustand 轻量状态

tsx 复制代码
// ===== Zustand 基础 =====

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

// 定义 Store 类型
interface UserState {
  user: User | null;
  isAuthenticated: boolean;
  login: (user: User) => void;
  logout: () => void;
}

const useUserStore = create<UserState>()(
  persist(
    (set) => ({
      user: null,
      isAuthenticated: false,
      
      login: (user) => set({ user, isAuthenticated: true }),
      logout: () => set({ user: null, isAuthenticated: false }),
    }),
    { name: 'user-storage' }
  )
);

// ===== 派生状态(Selector)=====

// 组件中:自动订阅,仅 user 变化时重新渲染
function UserProfile() {
  const user = useUserStore((state) => state.user);
  return <div>{user?.name}</div>;
}

// 派生状态:计算属性
const useUserName = () => useUserStore((state) => state.user?.name ?? 'Guest');

// 多个订阅:选择多个值
function UserBadge() {
  const { user, isAuthenticated } = useUserStore(
    useShallow((state) => ({
      user: state.user,
      isAuthenticated: state.isAuthenticated,
    }))
  );
  
  return isAuthenticated ? <Badge>{user?.name}</Badge> : null;
}

// ===== 切片模式(大型应用)=====

// stores/userSlice.ts
const createUserSlice = (set) => ({
  user: null,
  login: (user) => set({ user }),
  logout: () => set({ user: null }),
});

// stores/uiSlice.ts
const createUISlice = (set) => ({
  sidebarOpen: false,
  toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
});

// stores/index.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

export const useStore = create(
  devtools(
    (...a) => ({
      ...createUserSlice(...a),
      ...createUISlice(...a),
    })
  )
);

// ===== 异步 Actions =====

interface PostState {
  posts: Post[];
  isLoading: boolean;
  error: string | null;
  fetchPosts: () => Promise<void>;
  createPost: (data: CreatePostInput) => Promise<void>;
}

const usePostStore = create<PostState>()((set, get) => ({
  posts: [],
  isLoading: false,
  error: null,

  fetchPosts: async () => {
    set({ isLoading: true, error: null });
    try {
      const posts = await api.get('/posts');
      set({ posts, isLoading: false });
    } catch (error: any) {
      set({ error: error.message, isLoading: false });
    }
  },

  createPost: async (data) => {
    const newPost = await api.post('/posts', data);
    set((state) => ({ posts: [newPost, ...state.posts] }));
  },
}));

4.2 React Query 数据获取

tsx 复制代码
// ===== TanStack Query(React Query)=====

import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 创建客户端
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,  // 5 分钟内不重新请求
      gcTime: 10 * 60 * 1000,    // 缓存保留 10 分钟
      retry: 3,
      refetchOnWindowFocus: false,
    },
  },
});

// 基础查询
function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => api.get(`/users/${userId}`),
    enabled: !!userId,  // userId 为空时不请求
  });

  if (isLoading) return <Skeleton />;
  if (error) return <Error error={error} />;
  return <Profile user={data} />;
}

// 变异(POST/PUT/DELETE)
function CreatePost() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (newPost: CreatePostInput) => 
      api.post('/posts', newPost),
    
    onSuccess: () => {
      //  invalidate 重新获取列表
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
    
    onError: (error) => {
      console.error('Create failed:', error);
    },
  });

  function handleSubmit(data: CreatePostInput) {
    mutation.mutate(data);
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? '提交中...' : '发布'}
      </button>
    </form>
  );
}

// 乐观更新
function ToggleTodo() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: ({ id, completed }: { id: string; completed: boolean }) =>
      api.patch(`/todos/${id}`, { completed }),
    
    onMutate: async ({ id, completed }) => {
      // 取消所有 incoming 请求
      await queryClient.cancelQueries({ queryKey: ['todos'] });
      
      // 保存旧数据
      const previousTodos = queryClient.getQueryData(['todos']);
      
      // 乐观更新
      queryClient.setQueryData(['todos'], (old: Todo[]) =>
        old.map(todo => 
          todo.id === id ? { ...todo, completed } : todo
        )
      );
      
      return { previousTodos };
    },
    
    onError: (err, variables, context) => {
      // 失败时回滚
      queryClient.setQueryData(['todos'], context?.previousTodos);
    },
    
    onSettled: () => {
      // 重新同步
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  return (
    <TodoItem 
      todo={todo} 
      onToggle={() => mutation.mutate({ id: todo.id, completed: !todo.completed })}
    />
  );
}

五、性能优化

5.1 React.memo 与组件优化

tsx 复制代码
// ===== React.memo =====

import { memo, useMemo, useCallback } from 'react';

// 浅比较 Props
const Button = memo(function Button({ 
  onClick, 
  children 
}: { 
  onClick: () => void; 
  children: React.ReactNode;
}) {
  console.log('Button rendered');
  return <button onClick={onClick}>{children}</button>;
});

// 自定义比较函数(深度比较)
const ListItem = memo(
  function ListItem({ item, onSelect }: ListItemProps) {
    return (
      <div onClick={() => onSelect(item.id)}>
        {item.name}
      </div>
    );
  },
  (prevProps, nextProps) => {
    // 返回 true 表示 props 没变,不需要重新渲染
    return (
      prevProps.item.id === nextProps.item.id &&
      prevProps.item.name === nextProps.item.name &&
      prevProps.onSelect === nextProps.onSelect
    );
  }
);

// ===== 列表渲染优化 =====

function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map(user => (
        // 每个 ListItem 都是 memo 的
        <ListItem key={user.id} item={user} />
      ))}
    </ul>
  );
}

// ===== useMemo 优化列表 =====

function FilteredUserList({ users, filter }: Props) {
  // 只有 users 或 filter 变化时才重新计算
  const filteredUsers = useMemo(() => {
    return users.filter(user => 
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]);
  
  return <UserList users={filteredUsers} />;
}

5.2 代码分割与懒加载

tsx 复制代码
// ===== React.lazy 懒加载 =====

import { lazy, Suspense } from 'react';

// 路由懒加载
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

// 组件懒加载
const HeavyChart = lazy(() => import('./components/HeavyChart'));

// Suspense 包裹
function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={
        <Suspense fallback={<Loading />}>
          <Dashboard />
        </Suspense>
      } />
    </Routes>
  );
}

// ===== 带错误边界的懒加载 =====

import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <ErrorBoundary fallback={<ErrorPage />}>
      <Suspense fallback={<PageSkeleton />}>
        <HeavyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

// ===== 预加载 =====

// Hover 时预加载
function Navigation() {
  return (
    <nav>
      <Link 
        to="/dashboard"
        onMouseEnter={() => {
          // 用户 hover 时预加载
          import('./pages/Dashboard');
        }}
      >
        Dashboard
      </Link>
    </nav>
  );
}

六、TypeScript 与 React

6.1 Props 与 Events 类型

tsx 复制代码
// ===== 基础 Props 类型 =====

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  loading?: boolean;
  children: React.ReactNode;
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

function Button({ 
  variant = 'primary', 
  size = 'md', 
  children,
  onClick,
  disabled,
  loading,
}: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled || loading}
    >
      {loading ? <Spinner /> : children}
    </button>
  );
}

// ===== Generic Props =====

interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>
          {renderItem(item, index)}
        </li>
      ))}
    </ul>
  );
}

// 使用
<List 
  items={users} 
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

// ===== 事件处理器类型 =====

// 常用事件类型
onChange: React.ChangeEventHandler<HTMLInputElement>
onSubmit: React.FormEventHandler<HTMLFormElement>
onClick: React.MouseEventHandler<HTMLButtonElement>
onKeyDown: React.KeyboardEventHandler<HTMLDivElement>
onFocus: React.FocusEventHandler<HTMLInputElement>
onScroll: React.UIEventHandler<HTMLDivElement>

// Form 事件
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  const formData = new FormData(e.currentTarget);
}

// Input 事件
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  const value = e.target.value;
  const checked = (e.target as HTMLInputElement).checked;
}

6.2 Context 类型安全

tsx 复制代码
// ===== 类型安全的 Context =====

interface ThemeContextValue {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
  setTheme: (theme: 'light' | 'dark') => void;
}

const ThemeContext = React.createContext<ThemeContextValue | null>(null);

function useTheme(): ThemeContextValue {
  const context = React.useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// 使用
function ThemeToggle() {
  const { theme, toggleTheme } = useTheme();
  return <button onClick={toggleTheme}>{theme === 'light' ? '🌙' : '☀️'}</button>;
}

// Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const value = useMemo(() => ({
    theme,
    toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
    setTheme,
  }), [theme]);

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

七、总结

React 18 新特性决策树

复制代码
更新是否是紧急的?
├── 是(用户输入、点击)→ 直接 setState
└── 否(数据列表、搜索过滤)
      └── 是否需要等待数据?→ useTransition
        ├── 需要 → useTransition + isPending
        └── 不需要 → useDeferredValue

Hooks 选择指南

场景 Hook 说明
基础状态 useState 简单值
复杂状态 useReducer 多个相关状态
副作用 useEffect DOM 操作、订阅、定时器
记忆函数 useCallback 作为 props 传递的回调
记忆计算 useMemo 昂贵计算
DOM 引用 useRef 不想触发渲染的可变值
全局状态 Zustand/Context 跨组件共享
服务端数据 React Query 请求缓存、乐观更新
防抖 自定义 useDebounce
分页 自定义 usePagination
懒加载 React.lazy 减少首屏包体积

性能优化清单

检查项 优化方向
列表渲染 key + React.memo
回调传递 useCallback
派生计算 useMemo
条件渲染 提前 return + 拆分组件
代码分割 React.lazy + Suspense
状态位置 提升到需要的最上层
重新渲染 React DevTools Profiler 定位

本文涵盖 React 18 完整知识:Concurrent Mode 原理 + useTransition/useDeferredValue + Automatic Batching + Streaming SSR/Suspense + Hooks 进阶(useReducer + useEffect + useCallback/useMemo)+ 自定义 Hooks(useLocalStorage/useDebounce/useFetch/usePagination/useIntersectionObserver)+ Zustand 轻量状态管理 + React Query 数据获取(乐观更新)+ 性能优化(memo + 懒加载)+ TypeScript 类型安全(泛型组件 + 事件类型 + Context)。

相关推荐
Shadow(⊙o⊙)1 小时前
QT常用控件3.0,font字体设置,toolTip提示,focusPolicy焦点定位原则,中型控件StyleSheet样式表。
服务器·开发语言·前端·c++·qt
六月的可乐1 小时前
【干货】小程序虚拟瀑布流探索小结
前端·react.js·小程序
techdashen1 小时前
Rust 项目管理动态 — 2026 年 3 月
前端·人工智能·rust
原则猫12 小时前
HOOKS 背后机制
前端
码语智行12 小时前
首页导航跳转功能深度解析-系统内和系统外
前端
阿猫的故乡13 小时前
Vue过渡动画从入门到装X:淡入淡出、滑动、列表动画、第三方库全搞定
前端·javascript·vue.js
IManiy13 小时前
总结之Vibe Coding前端骨架
前端
JS菌13 小时前
AI Agent 沙箱双层防护体系:从权限过滤到内核隔离的完整实现
前端·人工智能·后端
Aphasia31113 小时前
从输入URL到页面展示全流程
前端·面试