跳转至

React 深度实践

📚 章节目标

本章节将深入讲解 React 框架的核心概念和高级特性,包括 Hooks 、 Context 、状态管理、性能优化等,帮助学习者掌握 React 框架的深度应用。

学习目标

  1. 深入理解 React Hooks 原理和最佳实践
  2. 掌握 React Context API 的高级应用
  3. 熟练使用 TanStack Query(原 React Query)进行数据获取
  4. 掌握 React 性能优化技巧
  5. 了解 React Server Components

🎣 React Hooks 深度解析

1. Hooks 基础原理

1.1 Hooks 的设计动机

JavaScript
// 类组件的问题:相关逻辑分散在不同生命周期方法中
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 }; // 类组件中状态必须是对象
  }

  // 生命周期问题:componentDidMount和componentDidUpdate中存在重复逻辑
  componentDidMount() {
    document.title = `Count: ${this.state.count}`;
  }

  componentDidUpdate() {
    document.title = `Count: ${this.state.count}`;
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Increment
        </button>
      </div>
    );
  }
}

// Hooks解决方案:相关逻辑集中在一起,代码更简洁
function Counter() {
  const [count, setCount] = useState(0); // 声明式状态

  // useEffect统一处理挂载和更新,消除重复代码
  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]); // 仅count变化时执行

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

1.2 Hooks 的实现原理

JavaScript
// 简化的Hooks实现原理(React内部使用链表结构,此处用数组模拟)
let hooks = [];          // 存储所有Hook的状态值
let currentHookIndex = 0; // 当前正在执行的Hook索引

function useState(initialValue) {
  const index = currentHookIndex; // 闭包捕获当前索引,确保setState更新正确的状态

  if (hooks[index] === undefined) {
    hooks[index] = initialValue; // 仅首次渲染时初始化
  }

  const setState = (newValue) => {
    hooks[index] = newValue; // 通过闭包中的index定位到对应状态
    render(); // 触发重新渲染
  };

  currentHookIndex++; // 索引递增,为下一个Hook预留位置
  return [hooks[index], setState];
}

function useEffect(callback, deps) {
  const index = currentHookIndex;
  const prevDeps = hooks[index]; // 获取上一次渲染时的依赖数组

  // 浅比较依赖:首次渲染(prevDeps为空)或任一依赖变化时执行回调
  const hasChangedDeps = !prevDeps || !deps.every((dep, i) => dep === prevDeps[i]);

  if (hasChangedDeps) {
    callback();
    hooks[index] = deps; // 保存本次依赖用于下次比较
  }

  currentHookIndex++;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
function render() {
  currentHookIndex = 0; // 每次渲染重置索引,保证Hooks调用顺序一致
  root.render(<App />);
}

2. 核心 Hooks 详解

2.1 useState

JavaScript
// 基础用法
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

// 函数式更新(当新状态依赖旧状态时,使用函数形式确保正确性)
function Counter() {
  const [count, setCount] = useState(0);

  const increment = () => {
    // 使用函数式更新,每次都基于最新的pending状态计算
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // 连续调用两次,最终count+2
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment Twice</button>
    </div>
  );
}

// 数据拉取应放在 effect 中,而不是放在 state 初始化函数里
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;

    fetchUser(userId).then((data) => {
      if (!cancelled) {
        setUser(data);
      }
    });

    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (!user) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

// 对象和数组状态(需要保持不可变性,每次更新返回新对象)
function Form() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    age: 0,
  });

  const handleChange = (field, value) => {
    // 展开运算符创建新对象,仅覆盖变化的字段(不可变更新模式)
    setFormData(prev => ({
      ...prev,
      [field]: value,
    }));
  };

  return (
    <form>
      <input
        value={formData.username}
        onChange={(e) => handleChange('username', e.target.value)}
      />
      <input
        value={formData.email}
        onChange={(e) => handleChange('email', e.target.value)}
      />
    </form>
  );
}

2.2 useEffect

JavaScript
// 基础用法
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]); // 依赖数组

  if (!user) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

// 清理函数(组件卸载或依赖变化时执行,防止内存泄漏)
function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    // 返回清理函数:roomId变化时先断开旧连接,再建立新连接
    return () => {
      connection.disconnect();
    };
  }, [roomId]); // roomId变化时重新执行Effect

  return <div>Connected to {roomId}</div>;
}

// 多个Effect
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  useEffect(() => {
    if (user) {
      fetchPosts(user.id).then(setPosts);
    }
  }, [user]);

  return (
    <div>
      <h1>{user?.name}</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

// 性能优化:防抖搜索,避免每次按键都发送请求
function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (query.trim() === '') {
      setResults([]);
      return; // 空查询直接返回,不设置定时器
    }

    // 延迟500ms执行搜索,实现防抖效果
    const timer = setTimeout(() => {
      search(query).then(setResults);
    }, 500);

    // 依赖变化时清除上一个定时器,只执行最后一次输入的搜索
    return () => clearTimeout(timer);
  }, [query]);

  return (
    <ul>
      {results.map(result => (
        <li key={result.id}>{result.title}</li>
      ))}
    </ul>
  );
}

2.3 useContext

JavaScript
// 创建Context
const ThemeContext = React.createContext('light');
const UserContext = React.createContext(null);

// 提供者
function App() {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState({ name: 'John' });

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <UserContext.Provider value={user}>
        <Toolbar />
      </UserContext.Provider>
    </ThemeContext.Provider>
  );
}

// 消费者
function Toolbar() {
  const { theme, setTheme } = useContext(ThemeContext);
  const user = useContext(UserContext);

  return (
    <div className={theme}>
      <p>User: {user.name}</p>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

// 自定义Hook简化Context使用,并添加安全校验
function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    // 未在Provider内使用时抛出明确错误,便于调试
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

function Button() {
  const { theme } = useTheme();
  return <button className={theme}>Click me</button>;
}

2.4 useReducer

JavaScript
// 基础用法
const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return initialState;
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

// 复杂状态管理(当状态逻辑复杂时,useReducer比useState更清晰)
const initialState = {
  todos: [],
  filter: 'all',
  loading: false,  // 异步加载状态
  error: null,     // 错误信息
};

// 纯函数reducer:接收当前状态和action,返回新状态
function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_TODOS_START':       // 开始请求:显示加载、清除错误
      return { ...state, loading: true, error: null };
    case 'FETCH_TODOS_SUCCESS':     // 请求成功:保存数据、关闭加载
      return { ...state, loading: false, todos: action.payload };
    case 'FETCH_TODOS_ERROR':       // 请求失败:保存错误、关闭加载
      return { ...state, loading: false, error: action.payload };
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, action.payload] };
    case 'REMOVE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload),
      };
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
    default:
      return state;
  }
}

function TodoApp() {
  // useReducer返回当前状态和dispatch函数(类似Redux模式)
  const [state, dispatch] = useReducer(reducer, initialState);

  // 组件挂载时获取待办数据
  useEffect(() => {
    dispatch({ type: 'FETCH_TODOS_START' });
    fetchTodos()
      .then(todos => dispatch({ type: 'FETCH_TODOS_SUCCESS', payload: todos }))
      .catch(error => dispatch({ type: 'FETCH_TODOS_ERROR', payload: error }));
  }, []); // 空依赖数组:仅在挂载时执行一次

  const addTodo = (text) => {
    const todo = { id: Date.now(), text, completed: false };
    dispatch({ type: 'ADD_TODO', payload: todo });
  };

  if (state.loading) return <div>Loading...</div>;
  if (state.error) return <div>Error: {state.error.message}</div>;

  return (
    <div>
      <TodoForm onSubmit={addTodo} />
      <TodoList todos={state.todos} filter={state.filter} />
    </div>
  );
}

2.5 useMemo

JavaScript
// 缓存计算结果
function ExpensiveComponent({ a, b }) {
  const expensiveValue = useMemo(() => {
    console.log('Computing expensive value...');
    return a * b + Math.sqrt(a + b);
  }, [a, b]);

  return <div>Result: {expensiveValue}</div>;
}

// 优化列表渲染(仅当users或filter变化时才重新过滤)
function UserList({ users, filter }) {
  const filteredUsers = useMemo(() => {
    return users.filter(user =>
      user.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [users, filter]); // 依赖数组:只有这两个值变化时才重新计算

  return (
    <ul>
      {filteredUsers.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 缓存复杂对象(避免每次渲染都创建新对象引用,导致子组件不必要的重渲染)
function Chart({ data, config }) {
  const chartOptions = useMemo(() => ({
    type: config.type,
    data: {
      labels: data.map(d => d.label),
      datasets: [{
        data: data.map(d => d.value),
        backgroundColor: config.colors,
      }],
    },
    options: {
      responsive: true,
      plugins: {
        legend: { position: 'top' },
      },
    },
  }), [data, config]);

  return <Chart options={chartOptions} />;
}

2.6 useCallback

JavaScript
// 缓存函数
function ParentComponent() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []); // 空依赖数组,函数只创建一次

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

// 避免子组件不必要的渲染(useCallback + React.memo配合使用)
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');

  // useCallback缓存函数引用,防止每次渲染创建新函数导致子组件重渲染
  const handleChange = useCallback((value) => {
    setText(value);
  }, []); // 空依赖:函数永远保持同一引用

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      {/* count变化时,ChildComponent不会重渲染,因为props未变 */}
      <ChildComponent value={text} onChange={handleChange} />
    </div>
  );
}

// React.memo:浅比较props,仅props变化时才重新渲染
const ChildComponent = React.memo(function ChildComponent({ value, onChange }) {
  console.log('ChildComponent rendered');
  return <input value={value} onChange={(e) => onChange(e.target.value)} />;
});

2.7 useRef

JavaScript
// 访问DOM元素
function TextInput() {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus</button>
    </div>
  );
}

// 保存可变值(useRef在组件整个生命周期内保持同一引用)
function Timer() {
  const [count, setCount] = useState(0);
  const intervalRef = useRef(null); // 用ref保存定时器ID,修改时不触发重渲染

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      setCount(c => c + 1); // 使用函数式更新,避免闭包引用过期的count
    }, 1000);

    return () => {
      clearInterval(intervalRef.current); // 组件卸载时清除定时器
    };
  }, []);

  return <div>Count: {count}</div>;
}

// 保存上一次的值(利用useEffect在渲染后执行的特性)
function PreviousValue({ value }) {
  const prevRef = useRef(); // 初始值为undefined

  useEffect(() => {
    // Effect在渲染完成后执行,此时DOM已展示当前值
    prevRef.current = value; // 保存当前值,下次渲染时作为"上一次的值"
  }, [value]);

  return (
    <div>
      <p>Current: {value}</p>
      <p>Previous: {prevRef.current}</p>
    </div>
  );
}

2.8 useLayoutEffect

JavaScript
// 同步执行,在DOM更新后、浏览器绘制前执行(适合DOM测量)
function MeasureExample() {
  const [height, setHeight] = useState(0);
  // 回调ref:当DOM节点挂载/卸载时调用
  const measuredRef = useCallback(node => {
    if (node !== null) {
      setHeight(node.getBoundingClientRect().height); // 获取元素实际高度
    }
  }, []);

  return (
    <>
      <h1 ref={measuredRef}>Hello, world!</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

// 避免闪烁(useLayoutEffect同步执行,用户看不到中间状态)
function ScrollToBottom({ messages }) {
  const containerRef = useRef(null);

  // useLayoutEffect保证滚动在浏览器绘制前完成,避免闪烁
  useLayoutEffect(() => {
    if (containerRef.current) {
      containerRef.current.scrollTop = containerRef.current.scrollHeight;
    }
  }, [messages]); // 消息列表更新时自动滚动到底部

  return (
    <div ref={containerRef} style={{ overflow: 'auto', height: '300px' }}>
      {messages.map(msg => (
        <div key={msg.id}>{msg.text}</div>
      ))}
    </div>
  );
}

3. 自定义 Hooks

3.1 数据获取 Hook

JavaScript
// useFetch - 通用数据获取Hook(封装loading/error/data三种状态)
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  // 用ref保存options,避免options对象引用变化导致Effect重新执行
  const optionsRef = useRef(options);
  optionsRef.current = options;

  useEffect(() => {
    // AbortController用于在组件卸载或url变化时取消未完成的请求
    const controller = new AbortController();

    const fetchData = async () => {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, {
          ...optionsRef.current,
          signal: controller.signal, // 关联取消信号
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const json = await response.json();
        setData(json);
      } catch (err) {
        // 忽略因组件卸载导致的AbortError
        if (err.name !== 'AbortError') {
          setError(err);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchData();

    return () => {
      controller.abort(); // 清理:取消进行中的请求
    };
  }, [url]); // 仅url变化时重新请求

  return { data, loading, error };
}

// 使用示例
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!user) return null;

  return <div>{user.name}</div>;
}

3.2 本地存储 Hook

JavaScript
// useLocalStorage - 将状态持久化到localStorage
function useLocalStorage(key, initialValue) {
  // 惰性初始化:首次渲染时从localStorage读取
  const [storedValue, setStoredValue] = useState(() => {
    try {  // try/catch捕获异常
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue; // 已存在则解析,否则用默认值
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value) => {
    try {
      // 支持函数式更新(与useState行为一致)
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore)); // 同步写入localStorage
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue]; // API与useState完全一致
}

// 使用示例
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Switch to {theme === 'light' ? 'dark' : 'light'} mode
    </button>
  );
}

3.3 防抖 Hook

JavaScript
// useDebounce - 防抖Hook(延迟更新值,适用于搜索输入等场景)
function useDebounce(value, delay) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // 值变化后延迟delay毫秒再更新
    const timer = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 值再次变化时清除上一个定时器,实现防抖
    return () => {
      clearTimeout(timer);
    };
  }, [value, delay]);

  return debouncedValue; // 返回防抖后的值
}

// 使用示例
function SearchInput() {
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // 执行搜索
      search(debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]);

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}

3.4 窗口大小 Hook

JavaScript
// useWindowSize - 响应式监听窗口尺寸变化
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize); // 监听窗口resize事件
    return () => window.removeEventListener('resize', handleResize); // 卸载时移除监听
  }, []); // 空依赖:仅挂载/卸载时执行

  return size;
}

// 使用示例
function ResponsiveComponent() {
  const { width } = useWindowSize();

  return (
    <div>
      {width < 768 ? <MobileView /> : <DesktopView />}
    </div>
  );
}

3.5 表单 Hook

JavaScript
// useForm - 通用表单管理Hook(管理值、校验、触摸状态)
function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);  // 表单字段值
  const [errors, setErrors] = useState({});              // 校验错误信息
  const [touched, setTouched] = useState({});            // 字段是否被触摸过

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));

    // 仅对已触摸过的字段实时校验(避免用户还未输入就显示错误)
    if (touched[name] && validate) {
      setErrors(prev => ({
        ...prev,
        [name]: validate(values)[name],
      }));
    }
  };

  // 失焦时标记字段为已触摸,并触发校验
  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));

    if (validate) {
      setErrors(prev => ({
        ...prev,
        [name]: validate(values)[name],
      }));
    }
  };

  // 高阶函数:返回表单提交处理器
  const handleSubmit = (callback) => (e) => {
    e.preventDefault();

    // 提交时校验所有字段
    const validationErrors = validate ? validate(values) : {};
    setErrors(validationErrors);
    // 标记所有字段为已触摸(显示所有错误)
    setTouched(
      Object.keys(values).reduce((acc, key) => ({ ...acc, [key]: true }), {})
    );

    // 无错误时调用回调
    if (Object.keys(validationErrors).length === 0) {
      callback(values);
    }
  };

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    handleSubmit,
  };
}

// 使用示例
function LoginForm() {
  const { values, errors, handleChange, handleBlur, handleSubmit } = useForm(
    { username: '', password: '' },
    (values) => {
      const errors = {};
      if (!values.username) errors.username = 'Username is required';
      if (!values.password) errors.password = 'Password is required';
      return errors;
    }
  );

  const onSubmit = (values) => {
    console.log('Form submitted:', values);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        name="username"
        value={values.username}
        onChange={handleChange}
        onBlur={handleBlur}
        placeholder="Username"
      />
      {errors.username && <span>{errors.username}</span>}

      <input
        name="password"
        type="password"
        value={values.password}
        onChange={handleChange}
        onBlur={handleBlur}
        placeholder="Password"
      />
      {errors.password && <span>{errors.password}</span>}

      <button type="submit">Submit</button>
    </form>
  );
}

🌐 React Context API 高级应用

1. Context 最佳实践

1.1 分离 Context

JavaScript
// 不好的做法:所有状态放在一个Context(任一状态变化会导致所有消费者重渲染)
const AppContext = React.createContext();

function AppProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [user, setUser] = useState(null);
  const [language, setLanguage] = useState('en');

  // 问题:value对象每次渲染都是新引用,且包含所有状态
  return (
    <AppContext.Provider value={{ theme, setTheme, user, setUser, language, setLanguage }}>
      {children}
    </AppContext.Provider>
  );
}

// 好的做法:按职责分离Context,各自独立更新
const ThemeContext = React.createContext();
const UserContext = React.createContext();
const LanguageContext = React.createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
  return (
    <UserContext.Provider value={{ user, setUser }}>
      {children}
    </UserContext.Provider>
  );
}

1.2 优化 Context 性能

JavaScript
// 不好的做法:整个Context变化导致所有消费者重新渲染
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [colors, setColors] = useState({
    primary: '#007bff',
    secondary: '#6c757d',
  });

  return (
    <ThemeContext.Provider value={{ theme, setTheme, colors, setColors }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 好的做法:将状态值和修改函数分到不同Context,减少不必要的重渲染
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  const [colors, setColors] = useState({
    primary: '#007bff',
    secondary: '#6c757d',
  });

  return (
    // 只读数据和修改方法分离:读取theme的组件不会因setTheme引用变化而重渲染
    <ThemeContext.Provider value={{ theme, colors }}>
      <ThemeDispatchContext.Provider value={{ setTheme, setColors }}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeContext.Provider>
  );
}

// 消费者Hook:按需订阅,只读组件用useTheme,修改组件用useThemeDispatch
function useTheme() {
  return useContext(ThemeContext);
}

function useThemeDispatch() {
  return useContext(ThemeDispatchContext);
}

2. 复杂场景应用

2.1 多层 Context 嵌套

JavaScript
function App() {
  return (
    <ThemeProvider>
      <AuthProvider>
        <LanguageProvider>
          <Router>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/dashboard" element={<Dashboard />} />
            </Routes>
          </Router>
        </LanguageProvider>
      </AuthProvider>
    </ThemeProvider>
  );
}

function Dashboard() {
  const { theme } = useTheme();
  const { user } = useAuth();
  const { language } = useLanguage();

  return (
    <div className={theme}>
      <h1>Welcome, {user.name}</h1>
      <p>Language: {language}</p>
    </div>
  );
}

2.2 Context 组合

JavaScript
// 创建组合Hook
function useApp() {
  const theme = useTheme();
  const auth = useAuth();
  const language = useLanguage();

  return {
    theme,
    auth,
    language,
  };
}

// 使用
function MyComponent() {
  const { theme, auth, language } = useApp();

  return (
    <div>
      <p>Theme: {theme}</p>
      <p>User: {auth.user?.name}</p>
      <p>Language: {language}</p>
    </div>
  );
}

📊 TanStack Query 数据获取(原 React Query)

1. 基础使用

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

// 获取数据(useQuery自动管理缓存、重试、后台刷新)
function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],  // 查询键:用于缓存标识和自动重获取
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <div>{data.name}</div>;
}

// 更新数据(useMutation处理写操作,配合缓存失效实现数据同步)
function UpdateUser({ userId }) {
  const queryClient = useQueryClient(); // 获取查询客户端实例

  const mutation = useMutation({
    mutationFn: (userData) =>
      fetch(`/api/users/${userId}`, {
        method: 'PUT',
        body: JSON.stringify(userData),
      }).then(res => res.json()),
    onSuccess: () => {
      // 更新成功后使相关缓存失效,触发自动重新获取
      queryClient.invalidateQueries({ queryKey: ['user', userId] });
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    mutation.mutate(Object.fromEntries(formData));
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Saving...' : 'Save'}
      </button>
    </form>
  );
}

2. 高级特性

2.1 缓存和重新验证

JavaScript
// 自动重新验证
function UserProfile({ userId }) {
  const { data } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()),
    staleTime: 5 * 60 * 1000, // 5分钟内不重新获取
    gcTime: 10 * 60 * 1000, // 10分钟后清除缓存(TanStack Query v5 已将 cacheTime 重命名为 gcTime)
    refetchOnWindowFocus: true, // 窗口聚焦时重新获取
    refetchOnReconnect: true, // 网络重连时重新获取
  });

  return <div>{data?.name}</div>;
}

// 手动重新验证
function RefreshButton() {
  const queryClient = useQueryClient();

  const handleClick = () => {
    queryClient.invalidateQueries({ queryKey: ['user'] });
  };

  return <button onClick={handleClick}>Refresh</button>;
}

2.2 分页和无限滚动

JavaScript
// 分页
function UserList() {
  const [page, setPage] = useState(1);

  const { data, isLoading } = useQuery({
    queryKey: ['users', page],
    queryFn: () => fetch(`/api/users?page=${page}`).then(res => res.json()),
  });

  return (
    <div>
      {isLoading ? (
        <div>Loading...</div>
      ) : (
        <>
          <ul>
            {data.users.map(user => (
              <li key={user.id}>{user.name}</li>
            ))}
          </ul>
          <button
            onClick={() => setPage(p => p - 1)}
            disabled={page === 1}
          >
            Previous
          </button>
          <button
            onClick={() => setPage(p => p + 1)}
            disabled={!data.hasMore}
          >
            Next
          </button>
        </>
      )}
    </div>
  );
}

// 无限滚动(结合useInfiniteQuery和IntersectionObserver实现)
function InfiniteUserList() {
  const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } =
    useInfiniteQuery({
      queryKey: ['users'],
      queryFn: ({ pageParam }) =>
        fetch(`/api/users?page=${pageParam}`).then(res => res.json()),
      initialPageParam: 1, // TanStack Query v5 必须显式指定初始页码
      // 根据上一页响应决定下一页参数,返回undefined表示没有更多数据
      getNextPageParam: (lastPage) => lastPage.hasMore ? lastPage.nextPage : undefined,
    });

  const observerRef = useRef(); // 目标元素ref,用于触发加载更多

  useEffect(() => {
    // IntersectionObserver:当目标元素进入视口时触发回调
    const observer = new IntersectionObserver(
      (entries) => {
        // 元素可见 && 还有下一页 && 当前未在加载中
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1 } // 完全可见时触发
    );

    if (observerRef.current) {
      observer.observe(observerRef.current);
    }

    return () => {
      if (observerRef.current) {
        observer.unobserve(observerRef.current);
      }
    };
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

  return (
    <div>
      {data?.pages.map((page) => (
        <ul key={page.page}>
          {page.users.map(user => (
            <li key={user.id}>{user.name}</li>
          ))}
        </ul>
      ))}
      <div ref={observerRef} />
      {isFetchingNextPage && <div>Loading more...</div>}
    </div>
  );
}

2.3 乐观更新

JavaScript
function TodoList() {
  const queryClient = useQueryClient();

  const addTodoMutation = useMutation({
    mutationFn: (text) =>
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify({ text }),
      }).then(res => res.json()),
    // onMutate在请求发出前执行,用于乐观更新
    onMutate: async (newTodo) => {
      // 取消正在进行的查询,避免覆盖乐观更新的数据
      await queryClient.cancelQueries({ queryKey: ['todos'] });

      // 保存之前的数据(用于回滚)
      const previousTodos = queryClient.getQueryData(['todos']);

      // 乐观更新:立即在UI上显示新数据,无需等待服务器响应
      queryClient.setQueryData(['todos'], (old) => [
        ...old,  // ...展开运算符:展开数组/对象
        { id: Date.now(), text: newTodo, completed: false },
      ]);

      return { previousTodos }; // 传递给onError用于回滚
    },
    onError: (err, newTodo, context) => {
      // 请求失败时回滚到之前的数据
      queryClient.setQueryData(['todos'], context.previousTodos);
    },
    onSettled: () => {
      // 无论成功或失败,都重新获取以确保数据一致性
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    const text = e.target.elements.text.value;
    addTodoMutation.mutate(text);
    e.target.reset();
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input name="text" placeholder="Add todo" />
        <button type="submit" disabled={addTodoMutation.isPending}>
          Add
        </button>
      </form>
    </div>
  );
}

⚡ React 性能优化

1. 组件优化

1.1 React.memo

JavaScript
// 避免不必要的渲染
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
  console.log('ExpensiveComponent rendered');
  return <div>{data.map(item => <div key={item.id}>{item.name}</div>)}</div>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState([]);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <ExpensiveComponent data={data} />
    </div>
  );
}

// 自定义比较函数(默认浅比较不够用时,手动控制重渲染条件)
const MemoizedComponent = React.memo(
  function MyComponent({ user, onClick }) {
    return <div onClick={() => onClick(user.id)}>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    // 返回true表示props相同(不重渲染),false表示需要重渲染
    return prevProps.user.id === nextProps.user.id;
  }
);

1.2 useMemo 和 useCallback

JavaScript
function ParentComponent() {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState([]);

  // 缓存计算结果
  const expensiveValue = useMemo(() => {
    console.log('Computing expensive value...');
    return items.reduce((sum, item) => sum + item.value, 0);
  }, [items]);

  // 缓存函数
  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Total: {expensiveValue}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

2. 列表优化

2.1 虚拟列表

JavaScript
import { FixedSizeList } from 'react-window'; // 虚拟列表库,仅渲染可见区域的DOM

function VirtualizedList({ items }) {
  // 行渲染组件:style由react-window计算,包含定位信息
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <FixedSizeList
      height={400}           // 容器高度
      itemCount={items.length} // 总数据量
      itemSize={35}           // 每行固定高度(px)
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

2.2 key 优化

JavaScript
// 不好的做法:使用index作为key
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <li key={index}>{todo.text}</li>
      ))}
    </ul>
  );
}

// 好的做法:使用唯一ID作为key
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
}

3. 代码分割

3.1 React.lazy

JavaScript
import { lazy, Suspense } from 'react';

// lazy()实现动态导入,每个页面组件会被打包为独立chunk
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
const Contact = lazy(() => import('./Contact'));

function App() {
  return (
    // Suspense包裹懒加载组件,在加载期间显示fallback内容
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </Suspense>
  );
}

3.2 路由级代码分割

JavaScript
import { lazy, Suspense } from 'react';

// 路由配置数组:每个路由对应一个懒加载组件,访问时才按需下载
const routes = [
  {
    path: '/',
    component: lazy(() => import('./pages/Home')),
  },
  {
    path: '/dashboard',
    component: lazy(() => import('./pages/Dashboard')),
  },
  {
    path: '/settings',
    component: lazy(() => import('./pages/Settings')),
  },
];

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        {routes.map(route => (  // map转换每个元素;filter筛选;reduce累积
          <Route
            key={route.path}
            path={route.path}
            element={<route.component />}
          />
        ))}
      </Routes>
    </Suspense>
  );
}

🚀 React Server Components

1. 基础概念

JavaScript
// 服务器组件(默认):在服务器端执行,可直接访问数据库,不增加客户端JS体积
async function BlogPost({ id }) {
  const post = await db.post.findUnique({ where: { id } }); // 服务端直接查询DB
  return <article>{post.content}</article>;
}

// 客户端组件:需要'use client'指令声明,支持交互和浏览器API
'use client';
import { useState } from 'react';

function LikeButton() {
  const [likes, setLikes] = useState(0); // 客户端状态,服务器组件中不可用
  return <button onClick={() => setLikes(likes + 1)}>{likes}</button>;
}

2. 混合使用

JavaScript
// 服务器组件(可嵌入客户端组件作为子组件)
async function BlogPage({ params }) {  // async定义异步函数;await等待Promise完成
  const post = await db.post.findUnique({ where: { id: params.id } });  // await等待异步操作完成
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      {/* 服务器组件中使用客户端组件,通过props传递序列化数据 */}
      <LikeButton postId={post.id} />
    </div>
  );
}

// 客户端组件:处理用户交互逻辑
'use client';
import { useState } from 'react';

function LikeButton({ postId }) {
  const [likes, setLikes] = useState(0);

  // 客户端发起API请求(不能直接访问数据库)
  const handleLike = async () => {  // const不可重新赋值;let块级作用域变量
    const response = await fetch(`/api/posts/${postId}/like`, {
      method: 'POST',
    });
    const data = await response.json();
    setLikes(data.likes);
  };

  return <button onClick={handleLike}>{likes} likes</button>;
}

🆕 React 19 新特性

版本说明:React 19 于 2024 年 12 月正式发布;截至 2026-03-26,React 官方最新稳定版本为 19.2。本章涉及的 Actions、新 Hooks、React Compiler、React Server Components 与相关 Hook 行为,均以官方文档为准。

1. Actions 与表单新 API

1.1 useActionState (原 useFormState )

JavaScript
// React 19 引入 useActionState 管理异步 Action 的 pending/error/result 状态
// 注意:React 18 react-dom 中的 useFormState 已重命名为 useActionState(从 react 导入)
import { useActionState } from 'react';

async function updateName(prevState, formData) {
  const name = formData.get('name');
  try {
    await saveName(name);
    return { success: true, message: '保存成功' };
  } catch (err) {
    return { success: false, message: err.message };
  }
}

function UpdateNameForm() {
  // [state, dispatch, isPending] = useActionState(action, initialState)
  const [state, dispatch, isPending] = useActionState(updateName, null);

  return (
    <form action={dispatch}>
      <input name="name" placeholder="新名称" />
      <button type="submit" disabled={isPending}>
        {isPending ? '保存中...' : '保存'}
      </button>
      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

1.2 useFormStatus

JavaScript
// useFormStatus 读取父级 <form> 的提交状态(必须在 form 的子组件中使用)
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? '提交中...' : '提交'}
    </button>
  );
}

function Form() {
  async function handleSubmit(formData) {
    await submitData(formData);
  }

  return (
    <form action={handleSubmit}>
      <input name="email" type="email" />
      <SubmitButton /> {/* SubmitButton 内部可读取 form 的 pending 状态 */}
    </form>
  );
}

1.3 useOptimistic

JavaScript
// useOptimistic 在异步操作完成前立即展示乐观 UI,操作结束后自动回退或保留真实值
import { useOptimistic, useState } from 'react';

function MessageList({ messages, sendMessage }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    // 更新函数:接收当前状态和乐观值,返回乐观状态
    (currentMessages, newMessage) => [
      ...currentMessages,
      { text: newMessage, sending: true }, // sending 标记表示仍在请求中
    ]
  );

  async function handleSubmit(formData) {
    const text = formData.get('message');
    addOptimisticMessage(text); // 立即显示(带 sending 标记)
    await sendMessage(text);    // 实际发送,完成后 optimisticMessages 自动同步真实 messages
  }

  return (
    <>
      <ul>
        {optimisticMessages.map((msg, i) => (
          <li key={i} style={{ opacity: msg.sending ? 0.5 : 1 }}>
            {msg.text} {msg.sending && '(发送中...)'}
          </li>
        ))}
      </ul>
      <form action={handleSubmit}>
        <input name="message" />
        <button type="submit">发送</button>
      </form>
    </>
  );
}

1.4 use() Hook

JavaScript
// use() 可在渲染期间直接读取 Promise 或 Context,支持条件调用(不遵循 Hooks 规则)
import { use, Suspense } from 'react';

// 读取 Promise(配合 Suspense 使用)
function UserProfile({ userPromise }) {
  const user = use(userPromise); // 挂起直到 Promise resolve
  return <p>Hello, {user.name}</p>;
}

function App() {
  const userPromise = fetchUser(userId); // 在父组件创建 Promise
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

// 条件读取 Context(突破普通 Hooks 不能在条件语句中调用的限制)
function Notification({ isLoggedIn }) {
  if (!isLoggedIn) return null;
  // use() 可在条件判断后使用,useContext 则不行
  const theme = use(ThemeContext);
  return <div className={theme.class}>通知内容</div>;
}

2. Server Actions

JavaScript
// Server Actions:在 Next.js App Router 中直接在客户端调用服务器函数
// 文件顶部 'use server' 声明,或函数内内联 'use server'

// app/actions.ts(服务端函数文件)
'use server';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.post.create({ data: { title, content } });
  revalidatePath('/posts'); // 使 /posts 页面缓存失效,触发重新生成
}

// app/posts/new/page.tsx(客户端调用 Server Action)
import { createPost } from '../actions';
import { useActionState } from 'react';

export default function NewPostPage() {
  const [state, dispatch, isPending] = useActionState(createPost, null);

  return (
    <form action={dispatch}>
      <input name="title" placeholder="标题" required />
      <textarea name="content" placeholder="内容" />
      <button type="submit" disabled={isPending}>
        {isPending ? '发布中...' : '发布文章'}
      </button>
    </form>
  );
}

3. React Compiler (实验性)

JavaScript
// React Compiler(前身 React Forget)在编译时自动添加记忆化,
// 无需手动使用 useMemo/useCallback/React.memo

// 安装
// npm install -D babel-plugin-react-compiler

// babel.config.js
module.exports = {
  plugins: [
    ['babel-plugin-react-compiler', {
      target: '19', // React 19 的默认目标
    }],
  ],
};

// vite.config.ts(使用 @vitejs/plugin-react)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: [['babel-plugin-react-compiler']],
      },
    }),
  ],
});

// 启用 Compiler 后,以下代码不再需要手动优化
// React Compiler 会自动推断哪些值需要缓存
function ProductList({ products, filter }) {
  // Compiler 自动等价于 useMemo + 精确依赖追踪
  const filtered = products.filter(p => p.category === filter);

  return (
    <ul>
      {filtered.map(p => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

// ⚠️ 注意:React Compiler 要求组件遵循 React 规则(纯函数、不直接修改 props/state)
// 可用 react-compiler-healthcheck 工具检测代码兼容性

4. 其他 React 19 变化

JSX
// 1. ref 可直接作为 prop 传递(无需 forwardRef)
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}
// 旧写法(React 18):需要 React.forwardRef(({...props}, ref) => ...)

// 2. Context.Provider 简写(不再需要 <ThemeContext.Provider>)
const ThemeContext = createContext('light');

function App() {
  return (
    // React 19:直接使用 <ThemeContext> 替代 <ThemeContext.Provider>
    <ThemeContext value="dark">
      <Page />
    </ThemeContext>
  );
}

// 3. useDeferredValue 初始值(新增 initialValue 参数)
function SearchResults({ query }) {
  // 首次渲染使用 initialValue,后续渲染 defer 到空闲时更新
  const deferredQuery = useDeferredValue(query, '');
  return <Results query={deferredQuery} />;
}

// 4. 原生 <form> action 支持
function SimpleForm() {
  return (
    <form action={async (formData) => {
      // action 直接接收 FormData,期间自动管理 pending 状态
      await submitForm(Object.fromEntries(formData));
    }}>
      <input name="email" />
      <button>提交</button>
    </form>
  );
}

📝 练习题

1. 基础题

题目 1 :实现一个 useCounter Hook

JavaScript
function useCounter(initialValue = 0) {
  // 实现计数器Hook
  // 返回 { count, increment, decrement, reset }
}

// 使用示例
function Counter() {
  const { count, increment, decrement, reset } = useCounter(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

2. 进阶题

题目 2 :实现一个 useAsync Hook

JavaScript
function useAsync(asyncFunction, immediate = true) {
  // 实现异步Hook
  // 返回 { data, error, loading, run }
}

// 使用示例
function UserProfile({ userId }) {
  const { data, error, loading } = useAsync(  // 解构赋值:从对象/数组提取值
    () => fetch(`/api/users/${userId}`).then(res => res.json()),  // 箭头函数:简洁的函数语法
    !!userId
  );

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{data?.name}</div>;  // ?.可选链对象为null/undefined时安全返回undefined
}

3. 面试题

题目 3 :解释 React 的渲染机制和优化策略

JavaScript
// 答案要点:
// 1. React的渲染流程:状态变化 → 虚拟DOM计算 → diff算法 → 真实DOM更新
// 2. 优化策略:
//    - 使用React.memo避免不必要的渲染
//    - 使用useMemo缓存计算结果
//    - 使用useCallback缓存函数
//    - 代码分割和懒加载
//    - 虚拟列表优化长列表
//    - 避免内联对象和函数

🎯 本章总结

本章节深入讲解了 React 框架的核心概念和高级特性,包括 Hooks 、 Context 、 TanStack Query 、性能优化等。关键要点:

  1. Hooks 原理:理解 Hooks 的设计动机和实现原理
  2. 自定义 Hooks:掌握自定义 Hooks 的编写技巧
  3. Context API:掌握 Context 的高级应用和性能优化
  4. TanStack Query:掌握数据获取和状态管理
  5. 性能优化:掌握 React 性能优化的各种技巧

下一步将深入学习 Vue 框架的深度实践。

⚠️ 核验说明(2026-03-26):本页已纳入 2026-03-26 全站统一复核批次。若文中涉及外部模型、API、版本号、价格或第三方产品名称,请以官方文档和实际运行环境为准。


最后更新日期: 2026-03-26