likes
comments
collection
share

React19前瞻

作者站长头像
站长
· 阅读数 6

React19前瞻

近日,React 团队成员在社交平台表示,团队正在开发 React v19 版本,并且没有计划发布 v18.3 版本。React 团队已经确定,下一个版本将是大版本号,即版本号会是 19.0.0。

现在,让我们根据 React 团队最新发布的消息,来抢先学习一下 v19 版本可能正式发布的新特性。

  • React Compiler 自动记忆化编译器

    React 的核心思想是开发人员将其 UI 定义为当前状态的函数。可以使用普通的 JavaScript 值,如数字、字符串、数组、对象,并使用标准的 JavaScript 语法,如 if/else、for 等,来描述组件逻辑。其思维模型是,当应用程序状态发生更改时,React 将重新渲染。

    问题在于,React 有时会过于 reactive:它可能会重新渲染太多次。例如,在 JavaScript 中,我们没有简单的方法来比较两个对象或数组是否相等(具有相同的键和值),因此在每次渲染时创建一个新对象或数组都可能会导致 React 执行更多的工作。

    在使用新编译器以前,我们使用 useMemouseCallbackmemo 来手动缓存状态,以减少不必要的重新渲染,这种实现方式虽然可行,但 React 团队认为这并不是他们认为理想的方式,他们一直寻找让 React 在状态变化时自动且只重新渲染必要部分的方案。

    React团队表示,新的编译器,目前已经在 instagram 的生产环境中应用,React 团队计划在 Meta 的更多平台中应用,并在后续分享更多细节,相信这对开发者来说是又一次开发范式的改变。

  • 新的Hooks

    React 19 预计将推出 4 个全新 Hooks,这些 Hooks 主要关注 React 中的两个痛点:数据获取表单。 这些 Hooks 目前在 React 预览版本中作为实验性 API 提供,预计会成为 React 19 的一部分,但是最终发布之前,API 可能会有所变化。

    新的 Hooks 包括:

    • use
    • useOptimistic
    • useFormState
    • useFormStatus
  • use

    use 是一个实验性 React Hook,它可以让读取类似于 Promisecontext 的资源的值。

    官方文档:​https://zh-hans.react.dev/reference/react/use​

    use 是一个 React Hook,它可以让你读取类似于 ​Promise​​context​ 的资源的值。

    const value = use(resource);
    

    use(Promise)

    在组件中调用 use 以读取类似于 Promise 或 context 的资源的值。 可以将一个 promise 传递给它,React 将会在该 promise 解决之前进行 “挂起” 。它的基本语法如下:

    import { use } from 'react';
    
    function MessageComponent({ messagePromise }) {
      const message = use(messagePromise);
      const theme = use(ThemeContext);
      // ...
    

    挂起:挂起:出自React中文文档,原英文文档中词为 ”suspends“ ”pending“,直译为“待定”,“暂停”。“挂起”概念在Next.js中被广泛使用,Next.js是一个服务器端渲染(SSR)框架,它需要在服务器端等待数据加载完成,然后再将页面渲染到客户端。挂起是指组件在等待某些操作完成时的状态。这些操作可能包括数据加载、网络请求、异步计算等。

  • Suspense (v16.6)

    官方文档:​https://zh-hans.react.dev/reference/react/Suspense​

    <Suspense> 中译名“悬念”,允许在子组件完成加载前展示后备方案。

    fallback:备用UI,用于被挂起时渲染。

    children:真正的 UI 渲染内容,当 children 中的组件数据被挂起时,渲染 fallback

      <Suspense fallback={<Loading />}>
        <SomeComponent />
      </Suspense>
    

    下面来看一个简单的例子:

    import * as React from 'react';
    import { useState, use, Suspense } from 'react';
    import { faker } from '@faker-js/faker';
    
    const usePormise = () => {
      const [newsPromise, setNewsPromise] = useState(() => fetchNews());
    
      const handleUpdate = () => {
        fetchNews().then((news) => {
          setNewsPromise(Promise.resolve(news));
        });
      };
    
      return (
        <>
          <h3>
            新闻列表
                    <button onClick={handleUpdate}>刷新</button>
          </h3>
          <NewsContainer newsPromise={newsPromise} />
        </>
      );
    };
    
    let news = [...new Array(2)].map(() => faker.lorem.sentence());
    
    const fetchNews = () =>
      new Promise((resolve) =>
        // 使用 setTimeout 模拟数据获取
        setTimeout(() => {
          // 每次刷新时添加一个标题
          news.unshift(faker.lorem.sentence());
          resolve(news);
        }, 1000)
      );
    
    const NewsContainer = ({ newsPromise }) => (
      <Suspense fallback={<p>请求中...</p>}>
        <News newsPromise={newsPromise} />
      </Suspense>
    );
    
    const News = ({ newsPromise }) => {
      const news = use(newsPromise);
      return (
        <ul>
          {news.map((title, index) => (
            <li key={index}>{title}</li>
          ))}
        </ul>
      );
    };
    
    export default usePormise;
    

    React19前瞻

    在上面的例子中,每次刷新时,都会先显示“请求中…”,请求到数据后进行展示。

    官方文档中,关于 有一个警告:

    目前尚不支持在不使用固定框架的情况下进行启用 Suspense 的数据获取。实现支持 Suspense 数据源的要求是不稳定的,也没有文档。React 将在未来的版本中发布官方 API,用于与 Suspense 集成数据源。

    在新版本中,use 可能就是用于与 Suspense 集成数据源的官方 API。

    这个全新的 use hook 与其他 Hooks 不同,它可以在循环和条件语句中像 if 一样被调用。

    use(Context)

    use(Context) 可以用于读取 React Context。它与 useContext 完全相同,只是可以在循环和条件语句(如 if)中调用。

      import { use } from 'react';
    
      function HorizontalRule({ show }) {
          if (show) {
              const theme = use(ThemeContext);
              return <hr className={theme} />;
          }
          return false;
      }
    

    这将简化某些场景下的组件层级结构,因为在循环或条件语句中读取 context,之前唯一的方法就是将组件一分为二。

  • Form Actions

    官方文档:​https://zh-hans.react.dev/reference/react-dom/components/form​

    React扩展了内置浏览器组件 <form> 的属性 action,这个新功能使您能够将函数传递给 <form> 的 action 属性。当表单提交时,React 会调用这个函数。

    action: URL 或函数。当 URL 传递到 action 表单时,其行为将类似于 HTML 表单组件。当一个函数被传递给 action 该函数时,该函数将处理表单提交。传递给的函数 action 可以是异步的,并且将使用包含所提交表单的 ​​formData 的单个参数来调用。

    <form action={search}>
        <input name="query" />
        <button type="submit">Search</button>
    </form>
    

    例如在 React 19 中,可以像这样编写一个表单:

    import { useState } from "react";
    
    const AddToCartForm = ({ id, title, addToCart }) => {
      const formAction = async (formData) => {
        try {
          await addToCart(formData, title);
        } catch (e) {
        }
      };
    
      return (
        <form action={formAction}>
          <h2>{title}</h2>
          <input type="hidden" name="itemID" value={id} />
          <button type="submit">加入购物车</button>
        </form>
      );
    };
    
    export const Cart = ({ cart }) => {
      if (cart.length === 0) {
        return null;
      }
    
      return (
        <>
          购物车内容:
          <ul>
            {cart.map((item, index) => (
              <li key={index}>{item.title}</li>
            ))}
          </ul>
          <hr />
        </>
      );
    };
    
    export const FormActions = () => {
      const [cart, setCart] = useState([]);
    
      const addToCart = async (formData, title) => {
        const id = String(formData.get("itemID"));
        // 模拟一个 AJAX 请求
        await new Promise((resolve) => setTimeout(resolve, 100));
        setCart((cart) => [...cart, { id, title }]);
    
        return { id };
      };
    
      return (
        <>
          <Cart cart={cart}></Cart>
          <AddToCartForm id="2" title="JavaScript权威指南" addToCart={addToCart} />
          <AddToCartForm id="3" title="JavaScript高级程序设计" addToCart={addToCart} />
        </>
      );
    };
    
    

    React19前瞻

    action 函数可以同步或异步操作。使用 action 时,React 将为开发者管理数据提交的生命周期,我们可以通过 useFormStatususeFormState 这两个 hook 来访问表单操作的当前状态和响应。下面就来看看新的Hook:useFormStateuseFormStatus

    useFormState

    useFormState 是一个可以根据某个表单动作的结果更新 state 的 Hook。

    官方文档:​https://zh-hans.react.dev/reference/react-dom/hooks/useFormState​

    const [state, formAction] = useFormState(fn, initialState);
    

    参数:

    • fn:当按钮被按下或者表单被提交时触发的函数。当函数被调用时,该函数会接收到表单的上一个 state(初始值为传入的 initialState 参数,否则为上一次执行完该函数的结果)作为函数的第一个参数,余下参数为普通表单动作接到的参数。
    • initialState:state 的初始值。任何可序列化的值都可接收。当 action 被调用一次后该参数会被忽略。

    返回值:

    useFormState 返回一个包含两个值的数组:

    1. 当前的 state。第一次渲染期间,该值为传入的 initialState 参数值。在 action 被调用后该值会变为 action 的返回值。
    1. 一个新的 action 函数用于在你的 form 组件的 action 参数或表单中任意一个 button 组件的 formAction 参数中传递。

    例如,这可以用来显示由表单操作返回的确认消息或错误消息:

    import { useFormState } from "react-dom";
    
    export async function addToCart(prevState, queryData) {
        console.log('prevState', prevState);
        const itemID = queryData.get('itemID');
        if (itemID === "1") {
          return "已加入购物车";
        } else {
          return "无法加入购物车:商品已售罄";
        }
      }
    
    
    function AddToCartForm({itemID, itemTitle}) {
      const [message, formAction] = useFormState(addToCart, null);
      return (
        <form action={formAction}>
          <h2>{itemTitle}</h2>
          <input type="hidden" name="itemID" value={itemID} />
          <button type="submit">加入购物车</button>
          {message}
        </form>
      );
    }
    
    export default function UseFormState() {
      return (
        <>
          <AddToCartForm itemID="1" itemTitle="JavaScript:权威指南" />
          <AddToCartForm itemID="2" itemTitle="JavaScript:优点荟萃" />
        </>
      )
    }
    

    React19前瞻

    表单被提交后,传入的 action 函数会被执行。返回值将会作为该表单的新的 当前 state。

    与直接通过表单动作调用的函数不同,传入 useFormState 的函数被调用时,会多传入一个代表 state 的上一个值或初始值的参数作为该函数的第一个参数。

    useFormStatus

    useFormStatus 是一个提供上次表单提交状态信息的 Hook。

    官方文档:​https://zh-hans.react.dev/reference/react-dom/hooks/useFormStatus​

    const { pending, data, method, action } = useFormStatus();
    

    它不接收任何参数,会返回一个包含以下属性的 status 对象:

    • pending:布尔值。如果为 true,则表示父级 <form> 正在等待提交;否则为 false。
    • data:包含父级 <form> 正在提交的数据;如果没有进行提交或没有父级 <form>,它将为 null
    • method:字符串,可以是 'get' 或 'post'。表示父级 <form> 使用 GET 或 POST HTTP 方法 进行提交。默认情况下,<form> 将使用 GET 方法,并可以通过 method 属性指定。
    • action:一个传递给父级 <form>action 属性的函数引用。如果没有父级 <form>,则该属性为 null。如果在 action 属性上提供了 URI 值,或者未指定 action 属性,status.action 将为 null

    还是上面的购物车例子,利用 useFormStatus 将商品添加到购物车成功前,禁用添加按钮:

    import { useState } from "react";
    import { Cart } from "./FormActions";
    import { useFormStatus } from 'react-dom';
    
    const AddToCartForm = ({ id, title, addToCart }) => {
      const formAction = async (formData) => {
        try {
          await addToCart(formData, title);
        } catch (e) {}
      };
    
      return (
        <form action={formAction}>
          <h2>{title}</h2>
          <input type="hidden" name="itemID" value={id} />
          <SubmitButton />
        </form>
      );
    };
    
    const SubmitButton = () => {
        const { pending } = useFormStatus();
        return (
          <button disabled={pending} type="submit">
            添加到购物车
          </button>
        );
      };
    
    const UseFormStatus = () => {
      const [cart, setCart] = useState([]);
    
      const addToCart = async (formData, title) => {
        const id = String(formData.get("itemID"));
        // 模拟一个 AJAX 请求
        await new Promise((resolve) => setTimeout(resolve, 2000));
        setCart((cart) => [...cart, { id, title }]);
    
        return { id };
      };
    
      return (
        <>
          <Cart cart={cart} />
          <AddToCartForm id="2" title="JavaScript权威指南" addToCart={addToCart} />
          <AddToCartForm
            id="3"
            title="JavaScript高级程序设计"
            addToCart={addToCart}
          />
        </>
      );
    };
    
    export default UseFormStatus;
    
    

    React19前瞻

    注意:

    • useFormStatus Hook 必须从在 <form> 内渲染的组件中调用,此外,它仅在父级表单使用 action 属性时才有效。
    • useFormStatus 仅会返回父级 <form> 的状态信息。它不会返回同一组件或子组件中渲染的任何 <form> 的状态信息。
  • useOptimistic 乐观更新

    乐观更新:是一种在前端开发中常用的处理异步操作反馈的策略。它基于一种“乐观”的假设:即假设无论我们向服务器发送什么请求,这些操作都将成功执行,因此在得到服务器响应之前,我们就提前在用户界面上渲染这些改变。  
    

    使用场景:点赞、收藏、评论、即时添加,编辑等。

    useOptimistic(state, updateFn) 是一个新的 hook,允许你在异步操作(如网络请求)进行时,乐观地更新 UI。它通过接受当前状态和一个更新函数作为参数,返回一个在异步操作期间可能会有所不同的状态副本。你需要提供一个函数,这个函数接收当前状态和操作的输入,并返回在操作等待期间使用的乐观状态。

    官方文档:​https://zh-hans.react.dev/reference/react/useOptimistic

    该Hook 允许在进行提交动作的同时,能够乐观地更新用户界面,提升用户体验。其语法如下:

    import { useOptimistic } from 'react';
    
    function AppContainer() {
        const [optimisticState, addOptimistic] = useOptimistic(
            state,
            // 更新函数
            (currentState, optimisticValue) => {
                // 合并并返回带有乐观值的新状态  
            },
        );
    }
    

    参数

    • state: 初始状态值,以及在没有操作进行时返回的值。
    • updateFn(currentState, optimisticValue) : 一个函数,接收当前状态和传递给 addOptimistic 的乐观值,返回结果乐观状态。updateFn 接收两个参数:currentStateoptimisticValue。返回值将是 currentStateoptimisticValue 的合并值。

    返回值

    • optimisticState: 产生的乐观状态。当有操作正在进行,它等于 updateFn 返回的值,没有操作正在进行,它等于 state
    • addOptimistic: 这是在进行乐观更新时调用的调度函数。它接受一个参数 optimisticValue(任意类型),并调用带有 stateoptimisticValueupdateFn

    更详细的用法如下:

    import { useOptimistic, useState, useRef } from "react";
    
    function List({ messages, sendMessage }) {
      const formRef = useRef();
    
      async function formAction(formData) {
        addOptimisticMessage(formData.get("message"));
        formRef.current.reset();
        await sendMessage(formData);
      }
    
      const [optimisticMessages, addOptimisticMessage] = useOptimistic(
        messages,
        (state, newMessage) => [
          ...state,
          { text: newMessage, sending: true }
        ]
      );
    
      return (
        <>
          {optimisticMessages.map((message, index) => (
            <div key={index}>
              {message.text}
              {!!message.sending && <small> (Sending...)</small>}
            </div>
          ))}
          <form action={formAction} ref={formRef}>
            <input type="text" name="message" placeholder="Hello!" />
            <button type="submit">Send</button>
          </form>
        </>
      );
    }
    
    function UseOptimisitc() {
      const [messages, setMessages] = useState([
        { text: "Hello!", sending: false, key: 1 }
      ]);
    
      async function sendMessage(formData) {
        const sentMessage = await deliverMessage(formData.get("message"));
        setMessages((messages) => [...messages, { text: sentMessage }]);
      }
    
      async function deliverMessage(message) {
        await new Promise((res) => setTimeout(res, 1000));
        return message;
      }
    
      return <List messages={messages} sendMessage={sendMessage} />;
    }
    
    export default UseOptimisitc;
    
    

    React19前瞻

    在上面的例子中,当用户在表单中输入消息并点击“发送”按钮时,useOptimistic Hook 允许消息立即出现在列表中,并带有“发送中……”标签,即使消息实际上还没有发送到服务器。这种“乐观”方法给人一种快速和响应灵敏的印象。然后,表单在后台尝试真正发送消息。一旦服务器确认消息已收到,“发送中……”标签就会被移除。

  • 更多更新

    在此之上,React 团队的成员 Andrew Clark 也提出了2024年将会有如下的一些改变:

    • forwardRef 改为 ref is a prop:这一改变将简化子组件内部元素或组件的引用方式,让 ref 可以像一个普通的 prop` 一样传递
    • React.lazy 变为 RSC, promise-as-child:这一改变将提升代码分割和懒加载的能力
    • <Context.Provider> 变为 <Context>:这一改变将使上下文提供者的使用变得更加简单

    React 的目标十分宏大,他们期望能够打破前端和后端的界限,在保持自身客户端优势的同时,也能为社区的全栈框架提供基础设施。React 19 将会是继引入 hooks 之后的又一次里程碑式的版本,让我们一起期待。