一文搞定React19新特性

React 19 带来了许多令人兴奋的改进,旨在提升开发者体验和应用性能,特别是在并发渲染、数据处理和与 Web 平台集成方面。

重要提示: React 19 目前(截至 2025 年初)仍处于 Canary 或 Beta 阶段,这意味着 API 可能仍会发生变化,并且某些功能可能尚未完全稳定。以下讲解基于当前可用的信息和官方文档。

React 19 核心目标:

  • 开发者体验提升: 简化常见模式,减少样板代码。
  • 性能优化: 通过 React Compiler 和改进的并发特性。
  • 数据处理: 引入 Actions 来统一客户端和服务器数据交互。
  • Web 平台整合: 更好地支持 Web Components 和资源加载。

1. React Compiler (曾用名: React Forget)

这是 React 19 最具变革性的特性之一,尽管它主要发生在幕后。

目标: 自动优化 React 代码,减少手动 useMemo, useCallback, React.memo 的需求。

工作原理 (概念性):

React Compiler 是一个 编译时 工具。它会分析你的 React 组件代码,理解状态和 props 的变化如何影响渲染输出。基于这种分析,它可以自动地:

  1. 记忆化 (Memoization): 自动包装那些计算成本高或者其依赖项未改变时不需要重新计算的值或函数,类似于手动使用 useMemouseCallback
  2. 优化渲染: 在某些情况下,它可以更智能地跳过不必要的重渲染,甚至比手动 React.memo 更精细。

代码讲解 (概念对比):

假设我们有这样一个组件,手动进行了优化:

jsx 复制代码
import React, { useState, useMemo, useCallback } from 'react';

// 假设这是一个昂贵的计算函数
function expensiveCalculation(data) {
  console.log('Performing expensive calculation...');
  // ... 复杂的计算逻辑 ...
  return data * 2;
}

function MyComponent({ data }) {
  const [count, setCount] = useState(0);

  // 1. 手动记忆化计算结果
  // 只有当 'data' prop 改变时,才会重新执行 expensiveCalculation
  const calculatedValue = useMemo(() => {
    return expensiveCalculation(data);
  }, [data]); // 显式声明依赖项 'data'

  // 2. 手动记忆化事件处理器
  // 确保传递给子组件的 handleClick 函数引用稳定
  // 只有当 'setCount' (理论上不变) 改变时才重新创建,实际上是稳定的
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // 显式声明空依赖项数组

  console.log('Rendering MyComponent');

  return (
    <div>
      <p>Data Prop: {data}</p>
      <p>Calculated Value: {calculatedValue}</p>
      <p>Count: {count}</p>
      {/* 假设有一个子组件需要稳定的回调 */}
      {/* <ChildComponent onClick={handleClick} /> */}
      <button onClick={handleClick}>Increment Count</button>
    </div>
  );
}

// 3. 手动记忆化组件本身
// 只有当 props (这里是 'data') 发生浅比较变化时,组件才会重新渲染
const MemoizedMyComponent = React.memo(MyComponent);

export default MemoizedMyComponent;

// --- 使用示例 ---
function App() {
  const [appState, setAppState] = useState(0);
  const [componentData, setComponentData] = useState(10);

  return (
    <div>
      <button onClick={() => setAppState(s => s + 1)}>Update App State (Triggers App Re-render)</button>
      <button onClick={() => setComponentData(d => d + 5)}>Update Component Data</button>
      <MemoizedMyComponent data={componentData} />
    </div>
  );
}

使用 React Compiler 后的理想情况 (开发者编写的代码可能如下):

开发者可能只需要编写更"纯粹"的 React 代码,编译器会自动处理优化。

jsx 复制代码
import React, { useState } from 'react';

// 假设这是同一个昂贵的计算函数
function expensiveCalculation(data) {
  console.log('Performing expensive calculation...');
  return data * 2;
}

// 开发者编写的代码可能不再需要手动 useMemo, useCallback, React.memo
function MyComponent({ data }) {
  const [count, setCount] = useState(0);

  // 编译器会自动分析,并在 'data' 未改变时复用上一次的值
  const calculatedValue = expensiveCalculation(data);

  // 编译器会自动分析,并可能提供一个稳定的函数引用
  const handleClick = () => {
    setCount(c => c + 1);
  };

  console.log('Rendering MyComponent');

  return (
    <div>
      <p>Data Prop: {data}</p>
      <p>Calculated Value: {calculatedValue}</p>
      <p>Count: {count}</p>
      {/* <ChildComponent onClick={handleClick} /> */}
      <button onClick={handleClick}>Increment Count</button>
    </div>
  );
}

// 编译器可能会自动应用类似 React.memo 的优化
export default MyComponent; // 不再需要手动包装 React.memo

// --- 使用示例 (保持不变) ---
function App() {
  const [appState, setAppState] = useState(0);
  const [componentData, setComponentData] = useState(10);

  return (
    <div>
      <button onClick={() => setAppState(s => s + 1)}>Update App State</button>
      <button onClick={() => setComponentData(d => d + 5)}>Update Component Data</button>
      <MyComponent data={componentData} />
    </div>
  );
}

讲解:

  • 减少心智负担: 开发者不再需要时刻思考何时使用 useMemo, useCallback, React.memo,以及正确管理它们的依赖项数组。这可以显著简化代码,减少因依赖项错误导致的 bug。
  • 潜在性能更好: 编译器对代码有更全面的理解,可能做出比手动优化更精细、更有效的优化决策。
  • 工作方式: 编译器不是魔法。它遵循 React 的规则,但通过静态分析来应用这些规则。它需要代码遵循一定的模式(例如,避免不必要的副作用、保持一定的纯度)。
  • 现状: React Compiler 最初会在 Meta 内部和一些大型应用中推广,然后逐步向社区开放。它可能需要一些配置才能启用,并且在初期可能需要开发者注意一些编码模式以帮助编译器更好地工作。它是一个 可选的、逐步采用 的特性。

虽然这里没有直接展示编译器生成的代码(因为它发生在构建阶段),但通过对比"之前"和"之后"的开发者代码,我们可以理解其带来的简化。


2. Actions

Actions 是 React 19 中用于处理用户交互(尤其是表单提交和数据变更)的核心机制。它旨在统一客户端和服务器数据交互的处理方式,并与 React 的并发特性(如 Transitions)深度集成。

核心概念:

  • Action 函数: 可以是普通的异步函数 (async function)。
  • 表单集成: 可以直接将 Action 函数传递给 <form>actionformAction 属性。
  • 状态管理: 提供了新的 Hooks (useActionState, useFormStatus) 来管理 Action 的执行状态(pending, error, success data)。
  • 乐观更新: 提供了 useOptimistic Hook 来在 Action 执行期间展示临时 UI 状态。
  • Transitions: Actions 默认在 Transitions 中运行,这意味着它们不会阻塞 UI 渲染。

2.1 Client Actions & <form> 集成

可以直接在客户端定义异步函数,并将其用作表单的 action

jsx 复制代码
import React, { useState } from 'react';

// 模拟一个异步 API 调用
async function updateUsernameApi(userId, newUsername) {
  console.log(`API Call: Updating user ${userId} to ${newUsername}`);
  await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟网络延迟
  if (newUsername.toLowerCase() === 'error') {
    throw new Error('Invalid username!');
  }
  console.log('API Call: Update successful');
  return { success: true, message: `Username updated to ${newUsername}` };
}

function UserProfileForm({ userId }) {
  const [message, setMessage] = useState('');
  const [error, setError] = useState('');
  const [isPending, setIsPending] = useState(false);

  // 定义一个客户端 Action 函数
  const handleUpdateUsername = async (formData) => {
    setIsPending(true); // 手动设置 pending 状态开始
    setMessage('');
    setError('');

    const newUsername = formData.get('username'); // 从 FormData 获取表单数据

    if (!newUsername) {
      setError('Username cannot be empty.');
      setIsPending(false);
      return; // 提前退出
    }

    try {
      // 直接调用异步 API
      const result = await updateUsernameApi(userId, newUsername);
      if (result.success) {
        setMessage(result.message); // 更新成功消息
      }
    } catch (err) {
      setError(err.message || 'Failed to update username.'); // 更新错误消息
    } finally {
      setIsPending(false); // 手动设置 pending 状态结束
    }
  };

  return (
    <form action={handleUpdateUsername} style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '5px' }}>
      <h3>Update Username (Client Action - Manual State)</h3>
      <label htmlFor="username">New Username:</label>
      <input type="text" id="username" name="username" required disabled={isPending} />

      {/* 提交按钮,也可用 <button type="submit"> */}
      <button type="submit" disabled={isPending}>
        {isPending ? 'Updating...' : 'Update'}
      </button>

      {/* 显示状态信息 */}
      {message && <p style={{ color: 'green' }}>{message}</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </form>
  );
}

export default function App() {
  return <UserProfileForm userId="123" />;
}

讲解:

  • <form action={handleUpdateUsername}> : React 19 允许将一个函数(包括异步函数)直接传递给 action 属性。当表单提交时,React 会自动:

    • 阻止浏览器的默认全页面提交行为。
    • 收集表单数据并创建一个 FormData 对象。
    • 调用你提供的 handleUpdateUsername 函数,并将 FormData 作为参数传入。
    • 将这个过程包装在一个 React Transition 中,避免阻塞 UI。
  • 手动状态管理: 在这个例子中,我们仍然需要手动管理 isPending, message, error 状态。这很快会变得繁琐。

2.2 useActionState Hook (曾用名: useFormState)

这个 Hook 旨在简化 Action 执行过程中的状态管理(pending, error, result)。

jsx 复制代码
import React from 'react';
// 在 React 19 Canary/Beta 中,它可能位于 react-dom
import { useActionState } from 'react-dom'; // 或者直接从 'react' 导入,取决于最终版本

// 模拟 API (同上)
async function updateUsernameApi(userId, newUsername) { /* ... API 实现 ... */
  console.log(`API Call: Updating user ${userId} to ${newUsername}`);
  await new Promise(resolve => setTimeout(resolve, 1000));
  if (newUsername.toLowerCase() === 'error') {
    throw new Error('Invalid username!');
  }
  if (!newUsername) {
    return { success: false, message: 'Username cannot be empty (server validation).' };
  }
  console.log('API Call: Update successful');
  return { success: true, message: `Username updated to ${newUsername}` };
}

function UserProfileFormWithHook({ userId }) {
  // useActionState 接收两个参数:
  // 1. Action 函数:这个函数现在接收两个参数 (previousState, formData)
  // 2. 初始状态 (initialState)
  // 它返回一个数组:
  // 1. state: 当前的状态,可以是成功结果、错误或初始状态
  // 2. formAction: 一个包装后的 Action 函数,应传递给 <form action={...}>
  // 3. isPending: 一个布尔值,指示 Action 当前是否正在执行
  const [state, submitAction, isPending] = useActionState(
    // Action 函数现在接收前一个状态作为第一个参数
    async (previousState, formData) => {
      const newUsername = formData.get('username');

      try {
        // 调用 API
        const result = await updateUsernameApi(userId, newUsername);
        // Action 函数应该返回下一个状态
        if (result.success) {
          console.log("Action succeeded, returning success state:", result);
          return { type: 'success', message: result.message };
        } else {
          console.log("Action failed (server validation), returning error state:", result);
          return { type: 'error', message: result.message };
        }
      } catch (err) {
        console.error("Action threw an error, returning error state:", err);
        // 如果 Action 抛出错误,useActionState 会捕获它并将其作为状态
        // 但通常推荐返回一个结构化的错误状态对象
        return { type: 'error', message: err.message || 'Failed to update username.' };
      }
    },
    // 初始状态
    { type: 'idle', message: '' }
  );

  return (
    // 将 useActionState 返回的 formAction 传递给 action 属性
    <form action={submitAction} style={{ border: '1px solid #ccc', padding: '15px', borderRadius: '5px', marginTop: '20px' }}>
      <h3>Update Username (useActionState)</h3>
      <label htmlFor="username-hook">New Username:</label>
      {/* 注意:name 属性是必须的,用于 FormData */}
      <input type="text" id="username-hook" name="username" required disabled={isPending} />

      <button type="submit" disabled={isPending}>
        {isPending ? 'Updating...' : 'Update'}
      </button>

      {/* 根据 state 的内容显示不同的消息 */}
      {state?.type === 'success' && <p style={{ color: 'green' }}>Success: {state.message}</p>}
      {state?.type === 'error' && <p style={{ color: 'red' }}>Error: {state.message}</p>}
      {/* 也可以显示初始或空闲状态 */}
      {/* {state?.type === 'idle' && <p>Please enter a username.</p>} */}
    </form>
  );
}

export default function App() {
  return (
    <div>
      {/* <UserProfileForm userId="123" /> */} {/* 上一个例子 */}
      <UserProfileFormWithHook userId="456" />
    </div>
  );
}

讲解:

  • useActionState(actionFn, initialState) :

    • actionFn(previousState, formData): 你提供的 Action 函数现在需要接收前一个状态 (previousState) 作为第一个参数,formData 作为第二个参数。它 必须返回 下一个状态对象。这个状态对象可以是任意结构,但通常包含区分成功/错误的信息(如 type 字段)和相关数据(如 message)。
    • initialState: Action 的初始状态。
  • 返回值 [state, submitAction, isPending] :

    • state: 保存 Action 的当前状态(初始状态、成功返回的状态或错误状态)。当 Action 开始时,它不会立即改变;当 Action 完成(成功或失败)时,它会更新为 Action 函数返回的新状态。
    • submitAction: 这是一个 包装后 的函数,你需要把它传递给 <form action={...}>。React 会使用这个包装函数来触发 Action 并管理状态。
    • isPending: 一个布尔值,表示 Action 是否正在执行(即 Promise 是否处于 pending 状态)。这对于禁用按钮或显示加载指示器非常有用。
  • 简化状态管理: useActionState 极大地简化了处理 Action 过程中的 pending、成功和错误状态的逻辑,开发者只需关注 Action 本身的业务逻辑和返回相应的状态对象。

  • 错误处理: 如果 Action 函数内部抛出未捕获的错误,useActionState 会捕获它,并且 state 会更新(具体更新内容可能取决于 React 版本,但通常会包含错误信息),isPending 会变为 false。最佳实践是在 catch 块中返回一个明确的错误状态对象。

2.3 useFormStatus Hook

这个 Hook 允许组件 内部 的任何子组件访问其 父级 <form> 的状态,特别是 pending 状态和提交的数据。这对于创建独立的、可重用的表单组件(如自定义提交按钮)非常有用。

jsx 复制代码
import React from 'react';
import { useActionState } from 'react-dom';
// useFormStatus 通常也从 react-dom 导入
import { useFormStatus } from 'react-dom';

// 模拟 API
async function submitDataApi(data) {
  console.log("API Call: Submitting data", data);
  await new Promise(resolve => setTimeout(resolve, 1500));
  console.log("API Call: Submission successful");
  return { success: true, submittedData: data };
}

// 一个自定义的提交按钮组件
function SubmitButton() {
  // useFormStatus 必须在 <form> 组件内部使用
  // 它没有参数
  // 返回一个包含表单状态的对象
  const { pending, data, method, action } = useFormStatus();

  // pending: 布尔值,表示父 <form> 的 Action 是否正在执行
  // data: FormData 对象,包含在 Action 启动时提交的数据 (仅在 pending 时有值)
  // method: 表单提交的方法 ('get' 或 'post')
  // action: 传递给 <form action={...}> 的 Action 函数引用

  console.log("SubmitButton useFormStatus:", { pending, data, method }); // 查看状态

  // 根据 pending 状态改变按钮文本和禁用状态
  return (
    <button type="submit" disabled={pending} style={{backgroundColor: pending ? '#ccc' : '#3498db', color: 'white', padding: '10px 15px', border: 'none', borderRadius: '4px', cursor: pending ? 'not-allowed' : 'pointer'}}>
      {pending ? 'Submitting...' : 'Submit Form'}
    </button>
  );
}

// 表单组件
function MyForm() {
  const [state, submitAction, isPending] = useActionState(
    async (prevState, formData) => {
      const name = formData.get('name');
      const email = formData.get('email');
      try {
        const result = await submitDataApi({ name, email });
        return { type: 'success', message: `Data submitted for ${result.submittedData.name}` };
      } catch (err) {
        return { type: 'error', message: err.message || 'Submission failed' };
      }
    },
    { type: 'idle', message: '' }
  );

  return (
    // 父 <form>,将 action 指向 useActionState 返回的 submitAction
    <form action={submitAction} style={{ border: '1px solid #eee', padding: '20px', borderRadius: '5px', marginTop: '20px' }}>
      <h3>Form with useFormStatus Button</h3>
      <div>
        <label htmlFor="name">Name:</label>
        <input type="text" id="name" name="name" required disabled={isPending} />
      </div>
      <div style={{ marginTop: '10px' }}>
        <label htmlFor="email">Email:</label>
        <input type="email" id="email" name="email" required disabled={isPending} />
      </div>
      <div style={{ marginTop: '15px' }}>
        {/* 在表单内部使用自定义提交按钮 */}
        {/* SubmitButton 会自动获取父表单的 pending 状态 */}
        <SubmitButton />
      </div>

      {/* 显示 useActionState 的结果 */}
      {state?.type === 'success' && <p style={{ color: 'green' }}>{state.message}</p>}
      {state?.type === 'error' && <p style={{ color: 'red' }}>{state.message}</p>}
      {/* 注意:useActionState 的 isPending 和 useFormStatus 的 pending 应该是同步的 */}
      {/* <p>useActionState isPending: {isPending ? 'true' : 'false'}</p> */}
    </form>
  );
}

export default function App() {
  return <MyForm />;
}

讲解:

  • useFormStatus() :

    • 必须在 <form> 元素的 内部 调用。

    • 它让你能够访问 最近的父级 <form> 的状态。

    • 返回一个对象 { pending, data, method, action }

      • pending: 最常用的属性,布尔值,指示表单 Action 是否正在进行中。
      • data: FormData 对象,包含触发当前 Action 的数据。只有在 pendingtrue 时才有意义,可以用来显示正在提交的数据。
      • method: 表单的 HTTP 方法(通常是 'POST' 或 'GET',对于 Action 来说可能不那么关键)。
      • action: 表单的 action 属性引用的函数。
  • 解耦: useFormStatus 的主要好处是解耦。SubmitButton 组件不需要从父组件接收 isPending prop。它只需要知道自己在一个 <form> 内部,就可以通过 useFormStatus 获取所需的状态。这使得创建可重用的、状态感知的表单控件变得容易。

  • useActionState 的关系: useFormStatus 读取的状态是由父级 <form>action(通常是 useActionState 返回的 submitAction)驱动的。当 submitAction 开始执行时,useFormStatuspending 会变为 true;当 submitAction 完成时,pending 会变为 false

2.4 useOptimistic Hook

这个 Hook 用于实现 乐观更新 (Optimistic Updates) 。当用户执行一个需要时间的操作(如添加评论、点赞)时,你可以立即更新 UI 到预期的最终状态,然后在后台执行实际的 Action。如果 Action 成功,UI 保持不变;如果 Action 失败,UI 会自动回滚到 Action 开始之前的状态。

jsx 复制代码
import React, { useState, useOptimistic } from 'react';
import { useActionState } from 'react-dom'; // 假设 Action 状态管理

// 模拟发送消息的 API
let messageIdCounter = 3; // 模拟数据库 ID
const initialMessages = [
  { id: 1, text: 'Hello!', sending: false },
  { id: 2, text: 'How are you?', sending: false },
];

async function sendMessageApi(text) {
  console.log(`API Call: Sending message "${text}"`);
  await new Promise(resolve => setTimeout(resolve, 1500)); // 模拟网络延迟
  // 模拟随机失败
  if (Math.random() < 0.3) {
    console.error("API Call: Failed to send message");
    throw new Error(`Failed to send message: ${text}`);
  }
  const newId = messageIdCounter++;
  console.log(`API Call: Message sent successfully with ID ${newId}`);
  return { id: newId, text: text }; // 返回成功发送的消息(带 ID)
}

function MessageList() {
  const [messages, setMessages] = useState(initialMessages);

  // useOptimistic Hook
  // 参数 1: passthrough - 实际的、最终确认的状态 (来自 useState)
  // 参数 2: updateFn(currentState, optimisticValue) - 一个函数,接收当前状态和乐观值,返回合并后的乐观状态
  // 返回值: [optimisticState, addOptimistic]
  //   optimisticState: 显示给用户的状态(可能是实际状态,也可能是乐观更新后的状态)
  //   addOptimistic: 一个函数,用于触发乐观更新
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages, // 传递真实的 messages 状态
    // 这个函数定义了如何将"乐观的"值合并到当前状态中
    (currentState, optimisticValue) => {
      // optimisticValue 是我们调用 addOptimistic 时传入的值
      console.log("useOptimistic updateFn called. Current state:", currentState, "Optimistic value:", optimisticValue);
      // 返回一个新的数组,包含当前所有消息和正在发送的乐观消息
      return [
        ...currentState,
        {
          id: `optimistic-${Date.now()}`, // 临时 ID
          text: optimisticValue.text,
          sending: true, // 标记为正在发送
        },
      ];
    }
  );

  // 使用 useActionState 来处理表单提交和 API 调用
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const messageText = formData.get('message');
      if (!messageText) return null; // 如果为空则不处理

      // 1. 触发乐观更新
      // 在调用 API 之前,立即调用 addOptimistic 更新 UI
      // 传入的值会作为 updateFn 的第二个参数 (optimisticValue)
      addOptimisticMessage({ text: messageText });

      try {
        // 2. 调用实际的 API
        const sentMessage = await sendMessageApi(messageText);
        // 3. 如果 API 成功,更新真实的 state (messages)
        //    当真实的 state (messages) 更新时,useOptimistic 会自动放弃乐观状态,
        //    并显示最新的真实 state。
        setMessages(currentMessages => [...currentMessages, { ...sentMessage, sending: false }]);
        return null; // Action 成功,重置错误状态
      } catch (err) {
        // 4. 如果 API 失败,useOptimistic 会自动回滚
        //    因为真实的 state (messages) 没有改变,optimisticMessages 会恢复到
        //    调用 addOptimistic 之前的状态。
        //    我们只需要返回错误状态给 useActionState。
        console.error("Send message action failed:", err);
        return err.message || "Failed to send message."; // 返回错误信息
      }
    },
    null // 初始错误状态为 null
  );

  return (
    <div style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '5px', marginTop: '20px' }}>
      <h3>Optimistic Message List</h3>
      <ul>
        {/* 渲染 optimisticMessages 而不是 messages */}
        {optimisticMessages.map(msg => (
          <li key={msg.id} style={{ opacity: msg.sending ? 0.6 : 1, color: msg.sending ? 'gray' : 'black' }}>
            {msg.text} {msg.sending && <small>(Sending...)</small>}
          </li>
        ))}
      </ul>

      {/* 提交表单 */}
      <form action={submitAction}>
        <input type="text" name="message" placeholder="Enter message" disabled={isPending} />
        <button type="submit" disabled={isPending}>
          {isPending ? 'Sending...' : 'Send'}
        </button>
        {error && <p style={{ color: 'red' }}>Error: {error}</p>}
      </form>
    </div>
  );
}

export default function App() {
  return <MessageList />;
}

讲解:

  • useOptimistic(actualState, updateFn) :

    • actualState: 组件中真实的、最终确定的状态(通常来自 useState 或父组件 props)。
    • updateFn(currentState, optimisticValue): 一个纯函数,定义了如何根据当前状态 currentState 和你提供的乐观值 optimisticValue 计算出临时的、乐观的 UI 状态。
  • 返回值 [optimisticState, addOptimistic] :

    • optimisticState: 你应该在 UI 中渲染这个状态。它要么等于 actualState(当没有乐观更新进行时),要么等于 updateFn 的返回值(当乐观更新进行时)。
    • addOptimistic(optimisticValue): 调用此函数来触发一次乐观更新。你传入的 optimisticValue 会被传递给 updateFn
  • 工作流程:

    1. 用户操作: 用户提交表单(例如,发送消息)。

    2. 触发乐观更新: 在调用异步 Action (如 API 请求) 之前 ,立即调用 addOptimistic(newValue)useOptimistic 会调用你的 updateFn 来计算 optimisticState,UI 几乎瞬间更新,显示出好像操作已经成功(例如,新消息出现在列表中,标记为 "sending")。

    3. 执行 Action: 同时,开始执行实际的异步 Action (调用 sendMessageApi)。

    4. Action 成功:

      • 异步 Action 成功完成。
      • 你更新 真实的 actualState (调用 setMessages),将成功的结果合并进去。
      • useOptimistic 检测到 actualState 发生变化时,它会 自动丢弃 之前的乐观状态,并将 optimisticState 更新为 新的 actualState。UI 现在显示最终确认的状态。
    5. Action 失败:

      • 异步 Action 失败 (API 调用抛出错误)。
      • 更新 actualState
      • useOptimistic 检测到 Action 完成(通常通过 useActionStateisPending 变为 false)但 actualState 没有 改变。它会自动 回滚 optimisticState 到调用 addOptimistic 之前的状态(即 Action 开始时的 actualState)。UI 会自动撤销之前的乐观更新。
      • 你可以使用 useActionState 返回的错误状态来显示错误消息。
  • 用户体验: 极大地提升了用户体验,尤其是在网络延迟较高的情况下,用户可以立即看到操作的反馈,感觉应用响应更快。


3. New Hook: use

use Hook 是一个非常独特的新 Hook,它允许你在 组件渲染逻辑中(包括条件语句和循环内部) "解包" (unwrap) Promise 或 Context 的值。

3.1 use with Promises (integrates with Suspense)

use 接收一个 Promise 时,它的行为类似于 await,但它适用于 React 组件:

  • 如果 Promise 已经 resolve,use 返回 resolve 的值。
  • 如果 Promise 处于 pending 状态,use抛出 这个 Promise。这个抛出会被最近的 <Suspense> 边界捕获,显示 fallback UI,直到 Promise resolve。
  • 如果 Promise 已经 reject,use抛出 rejection 的原因(通常是一个 Error 对象)。这个抛出会被最近的 <ErrorBoundary> 捕获。
jsx 复制代码
import React, { Suspense, useState, use } from 'react';

// 模拟一个获取数据的 API,返回 Promise
let dataCache = {};
let errorCache = {};
let promiseCache = {};

function fetchData(key, delay = 1000) {
  if (dataCache[key]) {
    console.log(`[${key}] Returning cached data:`, dataCache[key]);
    return dataCache[key]; // 返回缓存数据
  }
  if (errorCache[key]) {
    console.log(`[${key}] Throwing cached error:`, errorCache[key]);
    throw errorCache[key]; // 抛出缓存的错误
  }
  if (promiseCache[key]) {
    console.log(`[${key}] Throwing pending promise`);
    throw promiseCache[key]; // 抛出正在进行的 Promise
  }

  console.log(`[${key}] Initiating fetch...`);
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      if (key === 'errorData') {
        const error = new Error(`Failed to fetch ${key}`);
        errorCache[key] = error; // 缓存错误
        delete promiseCache[key]; // 清理 Promise 缓存
        console.log(`[${key}] Fetch failed:`, error);
        reject(error);
      } else {
        const result = `Data for ${key} (fetched at ${new Date().toLocaleTimeString()})`;
        dataCache[key] = result; // 缓存数据
        delete promiseCache[key]; // 清理 Promise 缓存
        console.log(`[${key}] Fetch successful:`, result);
        resolve(result);
      }
    }, delay);
  });

  promiseCache[key] = promise; // 缓存 Promise
  throw promise; // 第一次调用,抛出 Promise 以触发 Suspense
}

// 组件使用 use 来获取数据
function DataDisplay({ dataKey, delay }) {
  console.log(`Rendering DataDisplay for ${dataKey}`);
  try {
    // 在渲染逻辑中直接使用 use 来解包 Promise
    // 如果 fetchData 抛出 Promise -> Suspense fallback
    // 如果 fetchData 抛出 Error -> ErrorBoundary
    // 如果 fetchData 返回数据 -> use 返回数据
    const data = use(fetchData(dataKey, delay)); // <--- 使用 use Hook
    console.log(`DataDisplay for ${dataKey} received data:`, data);
    return <p>✅ {data}</p>;
  } catch (error) {
    // 如果 use 抛出的是错误(而不是 Promise),我们可以在这里处理或让 ErrorBoundary 捕获
    // 注意:通常由 ErrorBoundary 处理更好
    console.error(`DataDisplay for ${dataKey} caught error:`, error);
    // return <p style={{ color: 'orange' }}>⚠️ Error caught inside component: {error.message}</p>;
    // 重新抛出让 ErrorBoundary 处理
     throw error;
  }
}

// 简单的 Error Boundary
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error: error };
  }

  componentDidCatch(error, errorInfo) {
    console.error("ErrorBoundary caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || <h1>Something went wrong: {this.state.error?.message}</h1>;
    }
    return this.props.children;
  }
}


export default function App() {
  const [showData, setShowData] = useState(false);
  const [keyCounter, setKeyCounter] = useState(0); // 用于重新触发 fetch

  const resetCache = () => {
    dataCache = {};
    errorCache = {};
    promiseCache = {};
    setKeyCounter(c => c + 1); // 改变 key 来模拟重新加载
    console.log("Cache cleared, keyCounter updated.");
  }

  return (
    <div>
      <h2>Using `use` Hook with Promises & Suspense</h2>
      <button onClick={() => setShowData(s => !s)}>Toggle Data Display</button>
      <button onClick={resetCache} style={{marginLeft: '10px'}}>Clear Cache & Reload</button>

      {showData && (
        <div style={{ marginTop: '20px', padding: '15px', border: '1px dashed blue' }}>
          <ErrorBoundary fallback={<p style={{ color: 'red' }}>💥 Oops! An error occurred while loading data.</p>}>
            {/* 使用 Suspense 包裹可能挂起的组件 */}
            <Suspense fallback={<p>⏳ Loading data (Suspense Fallback)...</p>}>
              {/* 可以在 Suspense 内部渲染多个使用 use 的组件 */}
              <DataDisplay dataKey={`normalData-${keyCounter}`} delay={1500} />
              <DataDisplay dataKey={`anotherData-${keyCounter}`} delay={800} />

              {/* 演示错误处理 */}
              <h4>Attempting to load data that will fail:</h4>
              <ErrorBoundary fallback={<p style={{ color: 'red' }}>💥 Failed to load errorData (ErrorBoundary Fallback).</p>}>
                 <Suspense fallback={<p>⏳ Loading error data...</p>}>
                    <DataDisplay dataKey={`errorData-${keyCounter}`} delay={500} />
                 </Suspense>
              </ErrorBoundary>

              {/* 可以在条件语句中使用 use */}
              {keyCounter % 2 === 0 && (
                 <Suspense fallback={<p>⏳ Loading conditional data...</p>}>
                   <h4>Conditional Data (if keyCounter is even):</h4>
                   <DataDisplay dataKey={`conditionalData-${keyCounter}`} delay={1200} />
                 </Suspense>
              )}
            </Suspense>
          </ErrorBoundary>
        </div>
      )}
    </div>
  );
}

讲解:

  • use(promise) : 核心用法。将 Promise 传递给 use
  • Suspense 集成: use 是 React Suspense for Data Fetching 的关键部分。当 use 遇到未完成的 Promise 时,它会通知 React 暂停当前组件的渲染,并向上查找最近的 <Suspense> 边界,显示其 fallback 内容。一旦 Promise 完成,React 会重新尝试渲染该组件。
  • 条件和循环: 与传统的 Hooks 不同(如 useState, useEffect 必须在顶层调用),use 可以在条件语句 (if) 和循环 (for, map) 中调用。这极大地简化了根据条件或列表项获取数据的逻辑。
  • 错误处理: 如果 Promise reject,use 会抛出错误。这个错误会沿着组件树冒泡,直到被最近的 <ErrorBoundary> 捕获。
  • 缓存: 在实际应用中,直接在组件内部调用 fetch 通常不是最佳实践,因为它可能导致重复请求。use 通常与一个缓存层(如示例中的 fetchData 函数所模拟的,或者像 Relay、React Query、SWR 等库提供的缓存)结合使用。缓存层负责管理 Promise 的状态(pending, resolved, rejected)并确保同一请求只发起一次。use 只是读取这个缓存层提供的 Promise 或数据。
  • 对比 await: await 只能在 async 函数中使用,而 React 函数组件不能是 async 的(因为它们需要同步返回 JSX 或 null)。use 解决了在同步函数(组件渲染)中等待异步操作的问题,通过与 Suspense 集成。

3.2 use with Context

use 也可以用来读取 React Context 的值,同样可以在条件和循环中使用。

jsx 复制代码
import React, { createContext, useContext, use, useState } from 'react';

// 1. 创建 Context 对象
const ThemeContext = createContext(null); // 初始值设为 null 或默认值

// 传统的 Context 读取方式
function OldToolbar() {
  // useContext 必须在组件顶层调用
  const theme = useContext(ThemeContext);

  if (!theme) {
    // 需要处理 context 未提供的情况
    return <div>Loading theme... (Old method)</div>;
  }

  return (
    <div style={{ background: theme.background, color: theme.foreground, padding: '10px', margin: '5px 0' }}>
      Old Toolbar - Theme: {theme.name}
    </div>
  );
}

// 使用 use 读取 Context
function NewToolbar() {
  // 使用 use 读取 Context,可以在条件语句之后
  console.log("Rendering NewToolbar");

  // 假设我们有一些条件逻辑
  const showAdvanced = true; // 示例条件

  if (showAdvanced) {
    // use 可以在条件内部调用
    const theme = use(ThemeContext); // <--- 使用 use 读取 Context
    console.log("NewToolbar read theme:", theme);

    // 如果 Context 没有 Provider,或者值为 null/undefined,use 会表现得像 useContext(Context)
    // 如果 Context.Provider 的 value 是一个 Promise,use 会触发 Suspense (虽然不常见)

    if (!theme) {
      // 同样需要处理未提供的情况,或者确保 Provider 总存在
      // 或者在 createContext 时提供一个有意义的默认值
      return <div>Waiting for theme provider... (New method)</div>;
    }

    return (
      <div style={{ background: theme.background, color: theme.foreground, padding: '10px', border: '2px solid blue', margin: '5px 0' }}>
        New Toolbar (Advanced View) - Theme: {theme.name}
        {/* 甚至可以在 map 内部使用 (如果 Context 值本身包含列表或函数) */}
        {/* {theme.features.map(feature => <Feature key={feature.id} config={use(feature.configContext)} />)} */}
      </div>
    );
  } else {
     // 在其他分支也可以使用 use
     const theme = use(ThemeContext);
     if (!theme) return <div>Waiting for theme...</div>;
     return (
        <div style={{ background: theme.background, color: theme.foreground, padding: '10px', border: '1px solid gray', margin: '5px 0' }}>
           New Toolbar (Simple View) - Theme: {theme.name}
        </div>
     );
  }
}

// 主应用,提供 Context
export default function App() {
  const themes = {
    light: { name: 'Light', background: '#eee', foreground: '#333' },
    dark: { name: 'Dark', background: '#333', foreground: '#eee' },
  };
  const [currentTheme, setCurrentTheme] = useState('light');

  const toggleTheme = () => {
    setCurrentTheme(prev => (prev === 'light' ? 'dark' : 'light'));
  };

  return (
    <div>
      <h2>Using `use` Hook with Context</h2>
      <button onClick={toggleTheme}>Toggle Theme</button>
      <p>Current theme: {themes[currentTheme].name}</p>

      {/* 2. 使用 Context Provider 包裹需要访问 Context 的子组件 */}
      <ThemeContext.Provider value={themes[currentTheme]}>
        <div style={{ marginTop: '15px', padding: '10px', border: '1px solid green' }}>
          <p>Inside Theme Provider:</p>
          <OldToolbar />
          <NewToolbar />
          {/* 你甚至可以嵌套 Provider */}
          {/* <ThemeContext.Provider value={themes.dark}> */}
          {/*   <NewToolbar /> */}
          {/* </ThemeContext.Provider> */}
        </div>
      </ThemeContext.Provider>

       {/* Provider 外部,Context 值为 createContext 的初始值 (null) */}
       {/* <div style={{ marginTop: '15px', padding: '10px', border: '1px solid red' }}>
         <p>Outside Theme Provider:</p>
         <OldToolbar />
         <NewToolbar />
       </div> */}
    </div>
  );
}

讲解:

  • use(Context) : 将 Context 对象(由 createContext 创建)传递给 use
  • 灵活性: 最大的优势是可以在条件语句和循环中读取 Context,这在 useContext 中是不允许的,因为它必须遵守 Hooks 的规则(只能在顶层调用)。这使得根据不同条件应用不同 Context 值或在循环中为每个项读取特定 Context 变得更加直接。
  • 行为: use(Context) 的基本行为与 useContext(Context) 类似,它会查找组件树中最近的 <Context.Provider> 并返回其 value。如果没有找到 Provider,则返回 createContext 时定义的默认值。
  • 与 Suspense 的潜在交互: 虽然不常见,但如果 Provider 的 value 碰巧是一个 Promise,use(Context) 也会像处理 Promise 一样触发 Suspense。

4. Asset Loading & Suspense

React 19 增强了 Suspense 的能力,使其能够原生协调 样式表 (stylesheets)、字体 (fonts) 和脚本 (scripts) 的加载。

目标: 避免内容布局闪烁 (FOUC - Flash of Unstyled Content) 和因资源未加载完成而导致的 UI 不一致。确保在显示依赖某些资源(如 CSS 或字体)的组件之前,这些资源已经加载完毕。

工作原理:

React 现在能够检测渲染所需的资源(通过分析 JSX 中的 <link>, <style>, <script> 标签,或通过新的资源加载 API)。当组件挂起(例如,因为 use(promise) 或代码分割)时,React 会同时检查其渲染所需的资源是否已加载。如果资源未加载,React 会优先加载这些资源,并在资源加载完成后再继续渲染。

html 复制代码
<!-- public/index.html (或服务器端渲染输出) -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>React Asset Loading</title>

  <!-- 1. Preloading Assets -->
  <!-- React 19 可以利用 preload link 来了解需要哪些资源 -->
  <!-- 样式表 -->
  <link rel="preload" href="/styles/heavy-component.css" as="style" />
  <!-- 字体 (示例) -->
  <!-- <link rel="preload" href="/fonts/my-custom-font.woff2" as="font" type="font/woff2" crossorigin> -->
  <!-- 脚本 (示例) -->
  <!-- <link rel="preload" href="/scripts/some-library.js" as="script"> -->

</head>
<body>
  <div id="root"></div>
  <!-- Bundled JS 会在这里加载 -->
</body>
</html>
jsx 复制代码
// src/App.js
import React, { Suspense, lazy, useState } from 'react';

// 假设这是一个需要特定 CSS 文件的组件,并且是懒加载的
// heavy-component.css 定义了 .heavy-style
const HeavyComponent = lazy(() => import('./HeavyComponent')); // 使用 React.lazy 进行代码分割

// 假设这是 heavy-component.css 的内容:
/*
.heavy-style {
  border: 5px solid purple;
  padding: 20px;
  margin: 10px 0;
  background-color: lightgoldenrodyellow;
  font-size: 1.2em;
  // 假设使用了需要加载的字体
  // font-family: 'My Custom Font', sans-serif;
}
*/

// 另一个可能需要脚本的组件 (概念示例)
// const ComponentNeedingScript = lazy(() => import('./ComponentNeedingScript'));

function App() {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <div>
      <h1>React 19 Asset Loading with Suspense</h1>
      <button onClick={() => setShowHeavy(s => !s)}>Toggle Heavy Component</button>

      {/* 使用 Suspense 包裹可能需要加载资源和代码的组件 */}
      <Suspense fallback={<div>⏳ Loading Component and its Assets...</div>}>
        {showHeavy && (
          <div>
            {/*
              React 19 会确保在 HeavyComponent 渲染之前,
              它所依赖的 CSS (heavy-component.css,通过某种方式关联,
              例如在 HeavyComponent.js 中 import './heavy-component.css'
              或者通过 preload link) 已经加载完成。
              如果 CSS 尚未加载,Suspense 会保持显示 fallback。
            */}
            <HeavyComponent />

            {/*
              类似地,如果组件依赖特定字体或脚本,
              并且 React 能够检测到这种依赖(通过 preload 或新 API),
              Suspense 会等待这些资源加载。
            */}
            {/* <ComponentNeedingScript /> */}
          </div>
        )}
      </Suspense>

      <p>Some content outside Suspense.</p>
    </div>
  );
}

export default App;

// src/HeavyComponent.js (示例)
import React from 'react';
// 假设 Webpack 或类似工具配置为处理 CSS 导入并注入 <link> 或 <style>
import './heavy-component.css';
// 假设字体也通过 CSS 或 link 标签加载

export default function HeavyComponent() {
  console.log("HeavyComponent rendered");
  return (
    <div className="heavy-style">
      This is the Heavy Component. Its styles and potentially fonts
      should be loaded before you see this, thanks to Suspense coordination.
    </div>
  );
}

讲解:

  • <link rel="preload"> : 提示浏览器提前开始加载资源。React 19 可以利用这些提示来了解渲染所需的资源。
  • Suspense 协调:<HeavyComponent> 因为代码分割 (React.lazy) 而挂起时,Suspense 会显示 fallback。React 19 在此基础上增加了资源检查:如果 HeavyComponent 的渲染还依赖 heavy-component.css(或其他预加载的资源),即使组件代码已经加载完成,Suspense 也会继续显示 fallback,直到关联的 CSS(或其他资源)也加载完毕。
  • 消除 FOUC: 这确保了用户不会先看到没有样式的组件内容,然后样式才突然应用,从而提供了更平滑的加载体验。
  • 自动检测 vs. 手动提示: React 会尝试自动检测资源依赖(例如,通过分析 CSS-in-JS 库的输出或处理 import './style.css')。同时,也提供了新的 API(如 React.preload)让开发者可以更明确地告诉 React 何时以及哪些资源是需要的。
  • 字体和脚本: 同样的机制也适用于字体和脚本的加载,确保在使用自定义字体的文本或依赖特定 JS 库的组件渲染之前,相应的资源已经就绪。

5. Ref Prop 改进

在 React 19 之前,将 ref 传递给 函数组件 需要使用 React.forwardRef HOC (Higher-Order Component)。React 19 简化了这一点,允许将 ref 像普通 prop 一样直接传递给函数组件。

代码对比:

React 18 及之前 (使用 forwardRef):

jsx 复制代码
import React, { useRef, forwardRef, useImperativeHandle } from 'react';

// 子组件必须用 forwardRef 包裹才能接收 ref
const FancyInputOld = forwardRef((props, ref) => {
  const inputRef = useRef();

  // (可选) 使用 useImperativeHandle 暴露特定的方法给父组件
  useImperativeHandle(ref, () => ({
    focusInput: () => {
      inputRef.current.focus();
    },
    getValue: () => {
      return inputRef.current.value;
    }
  }));

  return (
    <div>
      <label>{props.label}: </label>
      <input ref={inputRef} type="text" style={{ border: '1px solid red' }} />
    </div>
  );
});

function AppOld() {
  const fancyInputRef = useRef();

  const handleFocusClick = () => {
    // 通过 ref 调用子组件暴露的方法
    if (fancyInputRef.current) {
      fancyInputRef.current.focusInput();
    }
  };

  const handleGetValueClick = () => {
    if (fancyInputRef.current) {
      alert(`Input value: ${fancyInputRef.current.getValue()}`);
    }
  };

  return (
    <div>
      <h3>Ref Handling (React 18 - forwardRef)</h3>
      {/* 将 ref 传递给 forwardRef 包裹的组件 */}
      <FancyInputOld ref={fancyInputRef} label="My Input (Old)" />
      <button onClick={handleFocusClick}>Focus Input</button>
      <button onClick={handleGetValueClick}>Get Value</button>
    </div>
  );
}

React 19 (直接传递 ref prop):

jsx 复制代码
import React, { useRef, useImperativeHandle } from 'react'; // 不再需要 forwardRef

// 函数组件直接在其 props 中接收 ref
// ref 现在是 props 的一部分,不再是 forwardRef 的第二个参数
function FancyInputNew({ label, ref }) { // <--- ref 作为 prop 接收
  const inputRef = useRef();

  // useImperativeHandle 仍然可以正常工作
  useImperativeHandle(ref, () => ({
    focusInput: () => {
      inputRef.current.focus();
    },
    getValue: () => {
      return inputRef.current.value;
    }
    // 可以添加更多需要暴露的方法
  }), [inputRef]); // 建议添加依赖项

  return (
    <div>
      <label>{label}: </label>
      {/* 将内部 ref 关联到 DOM 元素 */}
      <input ref={inputRef} type="text" style={{ border: '1px solid green' }} />
    </div>
  );
}

function AppNew() {
  const fancyInputRef = useRef();

  const handleFocusClick = () => {
    if (fancyInputRef.current) {
      fancyInputRef.current.focusInput();
    }
  };

   const handleGetValueClick = () => {
    if (fancyInputRef.current) {
      alert(`Input value: ${fancyInputRef.current.getValue()}`);
    }
  };

  return (
    <div>
      <h3 style={{marginTop: '20px'}}>Ref Handling (React 19 - Direct Prop)</h3>
      {/* ref 像普通 prop 一样传递 */}
      <FancyInputNew ref={fancyInputRef} label="My Input (New)" />
      <button onClick={handleFocusClick}>Focus Input</button>
      <button onClick={handleGetValueClick}>Get Value</button>
    </div>
  );
}

export default function App() {
  return (
    <>
      <AppOld />
      <AppNew />
    </>
  )
}

讲解:

  • 简化: 不再需要 forwardRef 包装器,代码更简洁、更直观。
  • ref 作为 Prop: ref 现在被视为一个普通的 prop,可以在函数组件的 props 参数中直接解构或访问 (props.ref)。
  • 向后兼容: 使用 forwardRef 的旧代码在 React 19 中仍然可以工作,提供了平滑的过渡。
  • 类组件: 对于类组件,ref 的行为保持不变(仍然可以直接访问 this.refs 或通过 React.createRef() / useRef() 创建的 ref 附加到类组件实例)。
  • useImperativeHandle: 仍然用于自定义暴露给父组件的 ref 值,用法不变,只是 ref 参数现在来自 props。

6. Context Provider 简化

在 React 19 之前,提供 Context 值需要使用 <MyContext.Provider> 组件。React 19 允许直接将 Context 对象本身作为组件使用来提供值。

代码对比:

React 18 及之前:

jsx 复制代码
import React, { createContext, useContext } from 'react';

const MyDataCtx = createContext('Default Value');

function ConsumerOld() {
  const data = useContext(MyDataCtx);
  return <p>Consumed (Old): {data}</p>;
}

function AppOld() {
  return (
    <div>
      <h4>Context Provider (React 18)</h4>
      {/* 必须使用 .Provider */}
      <MyDataCtx.Provider value="Value from Old Provider">
        <ConsumerOld />
      </MyDataCtx.Provider>
      {/* Provider 外部 */}
      {/* <ConsumerOld /> */}
    </div>
  );
}

React 19:

jsx 复制代码
import React, { createContext, useContext } from 'react'; // React 19

const MyDataCtxNew = createContext('Default Value New'); // 使用新名称避免冲突

function ConsumerNew() {
  const data = useContext(MyDataCtxNew); // 消费方式不变
  // 或者使用 use(MyDataCtxNew)
  return <p>Consumed (New): {data}</p>;
}

function AppNew() {
  return (
    <div>
      <h4 style={{marginTop: '20px'}}>Context Provider (React 19)</h4>
      {/* 直接使用 Context 对象作为 Provider 组件 */}
      <MyDataCtxNew value="Value from New Provider"> {/* <--- 简化写法 */}
        <ConsumerNew />
        {/* 仍然可以嵌套 */}
        <MyDataCtxNew value="Inner Value">
           <ConsumerNew />
        </MyDataCtxNew>
      </MyDataCtxNew>
       {/* Provider 外部 */}
       {/* <ConsumerNew /> */}
    </div>
  );
}

export default function App() {
  return (
    <>
      <AppOld />
      <AppNew />
    </>
  )
}

讲解:

  • <MyContext value={...}> : 你现在可以直接使用由 createContext() 返回的 Context 对象作为一个组件,并通过 value prop 提供值。
  • 语法糖: 这本质上是 <MyContext.Provider value={...}> 的语法糖,使得代码更简洁。
  • 消费方式不变: 消费 Context 的方式(使用 useContext(MyContext) 或新的 use(MyContext))保持不变。
  • 向后兼容: 旧的 <MyContext.Provider> 语法仍然有效。

7. 改进的 Web Components / Custom Elements 支持

React 19 改进了与 Web Components (Custom Elements) 的集成。在之前的版本中,将 React props 传递给 Custom Elements 的属性 (properties) 可能比较困难,尤其是对于复杂数据类型(对象、数组)。

React 19 会更智能地处理 props:

  • 属性 vs. 特性 (Attribute): 它能更好地区分何时应该设置 DOM attribute(通常是字符串)和何时应该设置 JavaScript property(可以是任何类型)。
  • 类型转换: 对于常见的布尔值和数字属性,React 会尝试进行正确的转换。
  • 事件处理: 对 Custom Element 抛出的自定义事件的处理也可能得到改善。

代码示例 (概念性):

假设有一个 Custom Element <my-chart>,它有一个 data property (期望接收数组) 和一个 options property (期望接收对象)。

jsx 复制代码
// 假设已经注册了一个名为 'my-chart' 的 Custom Element
// customElements.define('my-chart', MyChartElement);
// class MyChartElement extends HTMLElement {
//   set data(value) { /* ... handle array ... */ }
//   set options(value) { /* ... handle object ... */ }
//   // ...
// }

import React, { useState } from 'react';

function ChartWrapper() {
  const [chartData, setChartData] = useState([10, 20, 30]);
  const [chartOptions, setChartOptions] = useState({
    color: 'blue',
    animated: true,
    type: 'line'
  });

  const updateData = () => {
    setChartData(prev => [...prev, Math.random() * 50 + 10]);
  };

  return (
    <div>
      <h3>Web Component Integration (React 19 Improvements)</h3>
      {/*
        在 React 19 中,传递对象和数组作为 props 给 Custom Elements
        应该能更可靠地设置对应的 JavaScript property,而不是尝试
        将它们字符串化为 attribute。
        React 会尝试直接设置 .data 和 .options 属性。
      */}
      <my-chart
        data={chartData}       // 传递数组
        options={chartOptions}   // 传递对象
        // 普通的 attribute 仍然可以传递
        class="chart-container"
        // 事件监听 (语法可能保持类似,但内部处理可能更健壮)
        // onChartRendered={handleChartRendered}
      ></my-chart>

      <button onClick={updateData} style={{marginTop: '10px'}}>Add Data Point</button>
    </div>
  );
}

export default ChartWrapper;

讲解:

  • 更可靠的集成: 开发者可以更放心地在 React 组件中使用第三方或自己编写的 Web Components,传递复杂数据结构作为 props,期望它们能被正确地设置为 Custom Element 的 JavaScript properties。
  • 减少手动处理: 之前可能需要使用 refuseEffect 来手动设置 property (ref.current.data = chartData),React 19 旨在减少这种手动操作。

8. 错误报告改进

React 19 改进了错误报告,特别是对于常见的 Hydration 错误

  • 更清晰的差异: 当服务器渲染的 HTML 与客户端首次渲染的结果不匹配时,React 19 会提供更详细的差异信息,指出具体哪个属性或文本内容不匹配,而不是给出泛泛的错误。
  • 重复 Hydration 错误: 对于同一个不匹配问题,React 19 会减少重复报告,只报告一次。
  • 组件堆栈: 错误信息中的组件堆栈跟踪也得到了改进,更容易定位到出错的组件。

讲解: 这主要是开发者体验的改进,代码层面没有直接变化,但在调试 SSR/SSG 应用中的 Hydration 问题时会非常有帮助。


总结

我们详细探讨了 React 19 的主要新特性:

  1. React Compiler: 自动优化,减少手动 memoization。
  2. Actions: 统一数据处理,包含 Client Actions, useActionState, useFormStatus, useOptimistic
  3. use Hook: 在渲染中解包 Promise 和 Context。
  4. Asset Loading & Suspense: 协调 CSS, 字体, 脚本加载。
  5. Ref Prop 改进: 无需 forwardRef
  6. Context Provider 简化: 直接使用 Context 作为 Provider。
  7. Web Components 支持改进: 更好处理属性传递。
  8. 错误报告改进: 更清晰的 Hydration 错误。

React 19 是一个重要的版本,它在保留 React 核心概念的同时,通过引入 React Compiler、Actions 和 use Hook 等新特性,显著提升了开发体验和应用潜力。虽然目前仍处于早期阶段,但这些特性预示着 React 未来的发展方向。在实际项目中应用时,请务必关注官方文档和社区的最佳实践。

相关推荐
magic 2455 分钟前
深入解析Promise:从基础原理到async/await实战
开发语言·前端·javascript
海盗强6 分钟前
babel和loader的关系
前端·javascript
顾洋洋11 分钟前
WASM与OPFS组合技系列三(魔改写操作)
前端·javascript·webassembly
清粥油条可乐炸鸡17 分钟前
el-transfer穿梭框数据量过大的解决方案
前端·javascript
天天扭码18 分钟前
Trae 04.22 版本:前端学习者的智能成长助手
前端·trae
snakeshe101020 分钟前
深入解析React Hooks:useCallback与useMemo的原理与区别
前端
听风吹等浪起22 分钟前
html5:从零构建经典游戏-扫雷游戏
前端·html·html5
独立开阀者_FwtCoder22 分钟前
TypeScript 是怎么工作的?一文带你深入编译器内部流程
前端·javascript·面试
独立开阀者_FwtCoder27 分钟前
前端自适应方案全面解析:打造多端适配的现代网页
前端·javascript·面试
万事胜意50737 分钟前
前端切换Tab数据缓存实践
前端