likes
comments
collection
share

「揭秘React 19」 即将推出最实用的新Hooks

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

与大家想的不一样,React 核心团队没有完全专注于 React 服务器组件和 Next.js。在下一个主要版本的 React,即 React 19 中,将推出新的客户端 Hooks。这些新 Hooks 主要关注 React 中的两个痛点:数据获取表单处理。这些 Hooks 将提升所有 React 开发者的生产力,包括那些致力于单页面应用程序开发的开发者。

言归正传,让我们来看看这些新的 Hooks 吧!

注意:这些钩子只适用于 React 的 Canary 和实验频道。它们应该是即将推出的 React 19 的一部分,但在最终发布之前,API 可能会发生变化。

use

use 是一个实验性 React Hook,可让您读取 Promise 或 Context 等资源的值。

const value = use(resource);

与所有其他 React Hooks 不同use 可以在循环和条件语句(如 if)中调用。与其他 React Hooks 一样,调用 use 的函数必须是 Component 或 Hook。

我们来分别看一下use(Promise)use(Context)

use(Promise)

use(Promise)这个新的 Hook 是客户端上“挂起”的官方 API。您可以向其传递一个 Promise,React 将在其解决之前挂起。这种基本语法取自 React 的使用文档。如下:

import { use } from 'react';

function MessageComponent({ messagePromise }) {
    const message = use(messagePromise);
    // ...
}

比较不错的是,这个 hook 可以用于数据获取。下面是一个具体的例子,展示了在挂载时和点击按钮时进行数据获取。这段代码没有使用到 useEffect:

import * as React from 'react';
import { useState, use, Suspense } from 'react';
import { faker } from '@faker-js/faker';

export const App = () => {
  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(4)].map(() => faker.lorem.sentence());

const fetchNews = () =>
  new Promise<string[]>((resolve) =>
    // 使用 setTimeout 模拟数据获取过程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<string[]>(newsPromise);
  return (
    <ul>
      {news.map((title, index) => (
        <li key={index}>{title}</li>
      ))}
    </ul>
  );
};

上面的代码在 fetchNews 函数中,通过模拟 setTimeout@faker-js/faker 库来生成假数据,每次调用都会在数组的开头添加一个新闻标题。返回一个 Promise,Promise 被解析后,将返回带有更新后的新闻标题的数组。

当使用 Promise 调用 use 钩子时,它会与 Suspense 和错误边界集成。调用 use 的组件会在传递给 use 的 Promise 处于 pending 状态时挂起。如果调用 use 的组件被包装在 Suspense 边界中,则会显示 fallback。一旦 Promise 被解析,Suspense fallback 将被渲染为使用 use 钩子返回的数据的组件所替代。如果传递给 use 的 Promise 被拒绝,则将显示最近错误边界的 fallback

注意:

<Suspense> 文档中提到的警告已经不再适用于 React 19。

因为上面提到的,这个新的 use 钩子有一个隐藏的功能:与所有其他 React Hooks 不同,use 可以在循环和条件语句中(如 if 语句)被调用。

这是否意味着我们不再需要使用像 TanStack Query 这样的第三方库在客户端上获取数据?

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;
}

这将简化某些用例的组件层次结构,因为在循环或条件语句中读取上下文的唯一方式是将组件拆分为两个部分。

从性能方面来看,这也是一个巨大的进步,因为现在可以有条件地跳过组件的重新渲染,即使上下文已经发生了变化。

在 React 文档中了解更多关于 use(Context) hook 的信息。

Form Actions

这个新功能使您能够将函数传递给 <form> 的 action 属性。当表单提交时,React 会调用这个函数:

<form action={handleSubmit} />

请注意,在 React 18 中,如果您添加了一个带有 action 属性的<form> 标签,您将收到以下警告:

警告:在 <form> 标签上的 action 属性的值无效。要么将其从元素中删除,要么传递一个字符串或数字值以使其保留在 DOM 中。

然而,在 React 19 中,您可以像这样编写一个表单:

import { useState } from 'react';

const AddToCartForm = ({ id, title, addToCart }) => {
  const formAction = async (formData) => {
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">加入购物车</button>
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      购物车内容:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    // 模拟一个 AJAX 请求
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
      <AddToCartForm
        id="2"
        title="商品"
        addToCart={addToCart}
      />
  );
};

运行效果如下:

「揭秘React 19」 即将推出最实用的新Hooks

addToCart 函数不是一个服务器端操作。它在客户端被调用,可以是一个异步函数。

这将极大地简化在 React 中处理 AJAX 表单的方式,例如在搜索表单中。但是,这可能并不足以摆脱第三方库,比如 React Hook Form,它不仅仅处理表单提交(还包括验证、副作用等)。

提示:您可能会发现上面的示例中存在一些可用性问题(提交按钮在提交时未禁用,缺少确认消息,购物车更新较晚)。幸运的是,更多的钩子即将推出,以帮助处理这种情况。继续阅读!

您可以在 React 文档中了解更多关于 <form action> 属性的信息。

useFormState

这个新的hook旨在帮助处理上述描述的异步表单操作功能。调用 useFormState 来访问上次表单提交时的操作返回值。

import { useFormState } from 'react-dom';
import { action } from './action';

function MyComponent() {
    const [state, formAction] = useFormState(action, null);
    // ...
    return <form action={formAction}>{/* ... */}</form>;
}

例如,这可以让您显示表单操作返回的确认或错误信息。

import { useState } from 'react';
import { useFormState } from 'react-dom';

const AddToCartForm = ({ id, title, addToCart }) => {
  const addToCartAction = async (prevState, formData) => {
    try {
      await addToCart(formData, title);
      return 'Added to cart';
    } catch (e) {
      return '无法添加到购物车:该商品已售罄。';
    }
  };

  const [message, formAction] = useFormState(addToCartAction, null);

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">加入购物车</button>&nbsp;
      {message}
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    await new Promise((resolve) => setTimeout(resolve, 1000));
    if (id === '1') {
      setCart((cart: Item[]) => [...cart, { id, title }]);
    } else {
      throw new Error('Unavailable');
    }

    return { id };
  };

  return (
      <AddToCartForm id="2" title="商品" addToCart={addToCart} />
  );
};

运行效果如图:

「揭秘React 19」 即将推出最实用的新Hooks

注意:useFormState必须从react-dom导入,而不是react。

useFormStatus

useFormStatus 可让您了解父级 <form> 当前是否正在提交或已成功提交。它可以被表单的子组件调用,并返回一个包含以下属性的对象。

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

您可以使用 data 属性来显示用户提交的数据。您也可以显示一个等待状态,例如以下示例中,当表单正在提交时,按钮被禁用。

import { useState } from 'react';
import { useFormStatus } from 'react-dom';

const AddToCartForm = ({ id, title, addToCart }) => {
  const formAction = async (formData) => {
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  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>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      购物车:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={cart} />
      <AddToCartForm id="2" title="商品" addToCart={addToCart} />
    </>
  );
};

运行效果:

「揭秘React 19」 即将推出最实用的新Hooks

注意:useFormState必须从react-dom导入,而不是react。此外,它仅在父表单使用上述 action 属性时才有效。

与 useFormState 一起使用,这个钩子将提升客户端表单的用户体验,而不会使您的组件混杂着无用的contexteffects

useOptimistic

useOptimistic hook 使得在执行类似提交表单这样的操作,等待服务器响应时,能够让UI更美观,增强用户体验。

在上面的购物车示例中,我们可以使用此 hook 在 AJAX 调用完成之前显示添加了新商品的购物车:

import { useState, useOptimistic } from 'react';

const AddToCartForm = ({ id, title, addToCart, optimisticAddToCart }) => {
  const formAction = async (formData) => {
    optimisticAddToCart({ id, title });
    try {
      await addToCart(formData, title);
    } catch (e) {
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={id} />
      <button type="submit">加入购物车</button>
    </form>
  );
};

type Item = {
  id: string;
  title: string;
};

const Cart = ({ cart }: { cart: Item[] }) => {
  if (cart.length == 0) {
    return null;
  }
  return (
    <>
      购物车:
      <ul>
        {cart.map((item, index) => (
          <li key={index}>{item.title}</li>
        ))}
      </ul>
      <hr />
    </>
  );
};

export const App = () => {
  const [cart, setCart] = useState<Item[]>([]);

  const [optimisticCart, optimisticAddToCart] = useOptimistic<Item[], Item>(
    cart,
    (state, item) => [...state, item]
  );

  const addToCart = async (formData: FormData, title) => {
    const id = String(formData.get('itemID'));
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setCart((cart: Item[]) => [...cart, { id, title }]);

    return { id };
  };

  return (
    <>
      <Cart cart={optimisticCart} />
      <AddToCartForm
        id="2"
        title="商品"
        addToCart={addToCart}
        optimisticAddToCart={optimisticAddToCart}
      />
    </>
  );
};

运行效果:

「揭秘React 19」 即将推出最实用的新Hooks

Async Transitions

React 的 Transition API 允许您在不阻塞用户界面的情况下更新状态。例如,它允许您在用户改变主意时取消先前的状态更改。

这个想法是将状态更改包装在一个 startTransition 调用中。

以下示例展示了使用此 Transitions API 的选项卡导航。单击“Posts”,然后立即单击“contact”。请注意,这会中断“Posts”的缓慢渲染。 “联系人”选项卡立即显示。由于此状态更新被标记为转换,因此缓慢的重新渲染不会冻结用户界面。

import { useState, useTransition } from 'react';
import TabButton from './TabButton';
import AboutTab from './AboutTab';
import PostsTab from './PostsTab';
import ContactTab from './ContactTab';

export function App() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <>
      <TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>
        About
      </TabButton>
      <TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>
        Posts (slow)
      </TabButton>
      <TabButton
        isActive={tab === 'contact'}
        onClick={() => selectTab('contact')}
      >
        Contact
      </TabButton>
      <hr />
      {tab === 'about' && <AboutTab />}
      {tab === 'posts' && <PostsTab />}
      {tab === 'contact' && <ContactTab />}
    </>
  );
}

运行效果:

「揭秘React 19」 即将推出最实用的新Hooks

useTransition 钩子函数已经在 React 18.2 中可用。React 19 中的新功能是,现在您可以将异步函数传递给 startTransition,并由 React 等待以开始过渡。

这对于通过 AJAX 调用提交数据并在过渡中呈现结果非常有用。过渡挂起状态始于异步数据提交。它已经用于上面描述的表单操作功能。这意味着 React 调用用 startTransition 包装的 <form action> 处理程序,因此不会阻塞当前页面。

这个功能还没有在 React 文档中记录,但您可以在 pull request 中了解更多信息。

总结

有了这些功能,数据获取及表单在 React 中变得更加容易实现。然而,要创建出色的用户体验涉及集成所有这些 Hooks,这可能会变得复杂。或者,您可以使用像 react-admin 这样的框架,其中内置了带有乐观更新的用户友好表单。

为什么这些功能会出现在 React 19 而不是 React 18.3?

这似乎意味着不会发布 18.3 版本,因为这些功能包括一些微小的破坏性更改。

React 19 何时发布?目前还没有确定的发布日期,但本文提到的所有功能已经可以使用。尽管如此,我不建议立即开始使用它们-在生产环境中使用 canary 发行版并不是一个好主意(即使 Next.js 这样做)。

欢迎关注公众号《前端界》,文章会同步更新,也可快速加入前端交流群!

参考文章:marmelab.com/blog/2024/0…

转载自:https://juejin.cn/post/7337580941668171787
评论
请登录