Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 19 总览 #66

Open
brickspert opened this issue Jun 7, 2024 · 1 comment
Open

React 19 总览 #66

brickspert opened this issue Jun 7, 2024 · 1 comment

Comments

@brickspert
Copy link
Owner

如果你对 React 18 还不熟悉,欢迎阅读之前的文章《React 18 全览

最近 React 发布了 V19 RC 版本,按照惯例,我们对 React 19 的新特性进行一次深度的体验学习,以便尽快上手新特性。

这篇文章,我会通过丰富的示例,演示 React 19 的新特性,以及相较于老版本的差异。同时会附上自己对部分新特性的评价,如有不对,烦请指正。

本文所有示例代码可以在这里查看:https://codesandbox.io/p/sandbox/react19-demo-lmygpv

React 19 的最重要改动,是新增了几个 Hook,均是针对 form 和异步网络请求通用能力的封装。有点类似 react-query 的 useQuery,或者 ahooks 的 useRequest

在 React Hooks 中,最基本的网络请求我们可能会这样写:

function BasicDemo() {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, setIsPending] = useState(false);

  const handleSubmit = async () => {
    setIsPending(true);
    try {
      await updateName(name);
      console.log("Name updated successfully");
    } catch (e) {
      setError(e.message);
    }
    setIsPending(false);
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

上面是一个最简单的网络请求示例,点击按钮后,请求 updateName 接口,同时维护了 isPending 和 error 两个请求相关的状态。

useTransition 支持异步函数

useTransition 是 React 18 新增的一个 Hook,主要用来标记低优先级更新,低优先级更新是可以被中断的。在 React 18 中,useTransition 返回的 isPending 代表这次低优先级的更新正在等待中。

const [isPending, startTransition] = useTransition();

在 18 中,useTransition 返回的 startTransition 只支持传递同步函数,而在 19 中,增加了对异步函数的支持。通过这个特性,我们可以用来自动维护异步请求的 isPending 状态。代码如下:

export default function BasicDemo() {
  const [name, setName] = useState("");
  const [error, setError] = useState(null);
  const [isPending, startTransition] = useTransition();

  const handleSubmit = () => {
    startTransition(async () => {
      try {
        await updateName(name);
        console.log("Name updated successfully");
      } catch (e) {
        setError(e.message);
      }
    });
  };

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button onClick={handleSubmit} disabled={isPending}>
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

上述写法有两个好处:

  1. 自动维护了 isPending 状态
  2. 标记 updateName 触发的更新为低优先更新,不会阻塞 UI 渲染

useActionState 管理异步函数状态

useActionState 是 React 19 新增的一个 Hook,用来管理异步函数,自动维护了 data、action、pending 等状态。
经过 useActionState 改造的代码如下:

export default function ActionStateDemo() {
  const [name, setName] = useState("");

  // 接受一个异步请求函数,返回 [data、action、pending]
  const [error, handleSubmit, isPending] = useActionState(
    async (previousState, name) => {
      try {
        await updateName(name);
        console.log("Name updated successfully");
        return null;
      } catch (e) {
        console.log("error");
        return e.message;
      }
    }
  );

  return (
    <div>
      <input value={name} onChange={(event) => setName(event.target.value)} />
      <button
        onClick={() => {
          startTransition(() => {
            handleSubmit(name);
          });
        }}
        disabled={isPending}
      >
        Update
      </button>
      {error && <p>{error}</p>}
    </div>
  );
}

上面代码乍一看,感觉很熟悉,好像和大家经常用的 useRequest、useQuery、useSWR 等都差不多。
我们通过 API 来仔细了解一下 useActionState

const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);

返回参数含义:

  • state:代表 fn 函数返回的内容,fn 未执行时,等于 initialState
  • formAction:用来触发 fn 函数执行,可以直接调用,也可以传递给 form 的 action 属性
  • isPending:fn 函数是否正在执行中

传入参数含义:

  • fn:一个异步函数,接受两个参数 previousStateformData
    • previousState: 代表上一次执行 fn 返回的内容,首次调用等于 initialState
    • formData:代表调用 formAction 时传递的参数
  • initialState:fn 没执行时,默认的 state
  • permalink:一个 URL 字符串,通常和服务端组件有关系。(表示暂时没看懂干啥的)

你可以能注意到了上面 demo 中使用了 startTransition 来包裹调用 handleSubmit。因为不用 startTransition 来包裹,useActionState 就没用。

官方提供的 demo 是通过 form action 触发的 handleSubmit,其内置了 startTransition ,所以不需要手动设置。

<form action={handleSubmit}></form>

有没有觉得很难用?我是这么觉得的。

useOptimistic 乐观更新

乐观更新是一种常见的体验优化手段,在发送异步请求之前,我们默认请求是成功的,让用户立即看到成功后的状态。

先来看看官方提供的例子:提交表单更新 name,可以立即将新的 name 更新到 UI 中。请求成功则 UI 不变,请求失败则 UI 回滚。

function ChangeName() {
  const [name, setName] = useState("");

  // 定义乐观更新的状态
  const [optimisticName, setOptimisticName] = useOptimistic(name);

  const submitAction = async (formData) => {
    const newName = formData.get("name");
    // 请求之前,先把状态更新到 optimisticLike
    setOptimisticName(newName);
    try {
      await updateName(newName);
      // 成功之后,更新最终状态
      setName(newName);
    } catch (e) {
      console.error(e);
    }
  };

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change Name:</label>
        <input type="text" name="name" disabled={name !== optimisticName} />
      </p>
    </form>
  );
}

useOptimistic 用来维护临时状态,保证 UI 的乐观更新。

const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);

返回参数含义:

  • optimisticState:乐观更新的状态,UI 上应该始终消费这个状态。默认等于真正的 state。
  • addOptimistic:更新 optimisticState,可以通过 updateFn 指定更新逻辑

传入参数含义:

  • state:真正的状态
  • updateFn(currentState, optimisticValue) => newOptimisticState,调用 addOptimistic 的时候会通过这个函数生成新的 optimisticState

这个 Hook API 看起来还是挺简单的。

但是关于上面的 Demo 示例,我有个困惑:如果请求失败了,是怎么让状态回滚呢?

经过测试,上面的代码确实有失败状态回滚的能力。

其奥秘就是异步函数执行结束后,无论是成功还是失败,optimisticName 都会重置成和最新 state 一样。

也就是我们调用了 setName(newName),那 optimisticName 就变成新的状态。如果没调用,则变成之前的状态。

关于乐观更新,我在日常开发中,经常会用到。经典的场景是点赞场景,用户点赞后,立即更新 UI 为点赞成功,如果请求失败后,再回滚 UI。

使用 useOptimistic 后,其代码如下

function LikeDemo() {
  const [like, setLike] = useState(false);

  // 定义乐观更新的状态
  const [optimisticLike, setOptimisticLike] = useOptimistic(like);

  const handleLike = async () => {
    const targetLike = !like;
    try {
      // 请求之前,先把状态更新到 optimisticLike
      setOptimisticLike(targetLike);
      await updateLike(like);
      // 成功之后,更新最终状态
      setLike(targetLike);
    } catch (e) {
      console.error(e);
    }
  };

  return (
    <div>
      <div onClick={handleLike}>{optimisticLike ? "收藏" : "未收藏"}</div>
    </div>
  );
}

上面代码看起来很简单,但是没用,会报错

Warning: An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.

意思是 optimistic state 的更新,必须包裹在 startTransition 里面。

根据告警再优化下代码

function LikeDemo() {
  const [like, setLike] = useState(false);
  const [pending, startTransition] = useTransition();
  const [optimisticLike, setOptimisticLike] = useOptimistic(like);

  const handleLike = () => {
    const targetLike = !like;
    startTransition(async () => {
      try {
        setOptimisticLike(targetLike);
        await updateLike(like);
        setLike(targetLike);
      } catch (e) {
        console.error(e);
      }
    });
  };

  return (
    <div>
      <div onClick={handleLike}>{optimisticLike ? "收藏" : "未收藏"}</div>
    </div>
  );
}

增加了 startTransition 后,功能可以正常使用。

为什么官方的示例代码不用加 startTransition 呢?因为官方示例是通过 form 的 action 调用的,其默认内置了 startTransition。

体验下来,我觉得这个 Hook,确实是没啥用,我普通代码实现个乐观更新,更简单。

  const [like, setLike] = useState(false);

  const handleLike = async () => {
    try {
      setLike((s) => !s);
      await updateLike(like);
    } catch (e) {
      setLike((s) => !s);
    }
  };

上面我们介绍了 React 19 新增的几个 Hook,不知道大家看下来什么感受?说说我个人的感受。

React 19 之前的 Hook,基本都是原子级别的,必要的,比如 useStateuseEffectuseTransition等,没有它就有些功能实现不了。

但 React 19 新增的几个 Hook 明显不是这样的,而是更上层的封装,并且和 form 耦合很严重。

我觉得在实际业务开发中,几乎不会用到上述 Hook。

useFormStatus 获取表单状态

useFormStatus 是 React 19 新增的一个 Hook,主要用来快捷读取到最近的父级 form 表单的数据,其实就是类似 Context 的封装。

import { useFormStatus } from "react-dom";
import action from './actions';

function Submit() {
  const status = useFormStatus();
  return <button disabled={status.pending}>Submit</button>
}

export default function App() {
  return (
    <form action={action}>
      <Submit />
    </form>
  );
}
const { pending, data, method, action } = useFormStatus();

useFormStatus 能拿到父级最近的 form 的状态:

  • pending:是否正在提交中
  • data:表单正在提交的数据,如果 form 没有被提交,则为 null
  • method:form 的 method 属性,getpost
  • action:form 的 action 属性,如果 action 不是函数,则为 null

useFormStatus 使用场景较窄,绝大部分开发者不会用到。

use

use 是 React 19 新增的一个特性,支持处理 Promise 和 Context。

假如我们要实现这样一个需求:请求接口数据,请求过程中,显示 loading,请求成功,展示数据。

以前我们可能会这样写代码

function ReactUseDemo() {
  const [data, setData] = useState("");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    getList()
      .then((res) => {
        setData(res);
        setLoading(false);
      })
      .catch(() => {
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;

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

通过 use 我们可以把代码改造成下面这样

export default function ReactUseDemo() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ChildCompont />
    </Suspense>
  );
}

function ChildCompont() {
  const data = use(getData());
  return <div>{data}</div>;
}

use 接收一个 Promise,会阻塞 render 继续渲染,通常需要配套 Suspense 处理 loading 状态,需要配套 ErrorBoundary 来处理异常状态。

另外 use 也支持接收 Context,类似之前的 useContext,但比 useContext 更灵活,可以在条件语句和循环中使用。

function MyPage() {
  return (
    <ThemeContext.Provider value="dark">
      <Form />
    </ThemeContext.Provider>
  );
}

function Form() {
  const theme = use(ThemeContext);
  ......
}

use 的使用有一些注意事项

  • 需要在组件或 Hook 内部使用
  • use 可以在条件语句(比如 if)或者循环(比如 for)里面调用

ref

在之前,父组件传递 ref 给子组件,子组件如果要消费,则必须通过 forwardRef 来消费。

function RefDemo() {
  const inputRef = useRef(null);
  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <Input ref={inputRef} />
      <button onClick={focusInput}>Focus</button>
    </div>
  );
}

const Input = forwardRef((props, ref) => {
  return <input ref={ref} />;
});

React 19 开始,不需要使用 forwardRef 了,ref 可以作为一个普通的 props 了。

export const Input = ({ ref }) => {
  return <input ref={ref} />;
};

未来在某个版本会删除掉 forwardRef。

Context

在 React 19 之前,我们需要使用 Context.Provider,比如

import React, { createContext } from 'react';
 
const ThemeContext = createContext('');
 
function App({ children }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  );
}

在 React 19 中,我们可以使用 Context来代替 Context.Provider

function App({ children }) {
  return (
    <ThemeContext value="dark">
      {children}
    </ThemeContext>
  );
}

未来在某个版本会删除掉 Context.Provider

ref 支持返回 cleanup 函数

ref 支持返回一个 cleanup 函数,在组件卸载时会调用该函数。

<input
  ref={(ref) => {
    // ref created

    // NEW: return a cleanup function to reset
    // the ref when element is removed from DOM.
    return () => {
      // ref cleanup
    };
  }}
/>

useDeferredValue 增加了 initialValue 参数

useDeferredValue 现在增加了第二个参数 initialValue,指定初始化值。

const value = useDeferredValue(deferredValue, initialValue);

支持 Document Metadata

在之前,如果我们希望动态的在组件中指定 metatitlelink等文档属性,我们可能会这样做:

  1. 在 useEffect 中,通过 JS 手动创建
  2. 使用 react-helmet 这类三方库

在 React 19 中,原生支持了这三个文档属性,支持在组件中设置。

在渲染过程中,React 发现这三种标签,会自动提升到 上。

function BlogPost({post}) {
  return (
    <div>
      <meta name="author" content="Josh" />
      <link rel="author" href="https://twitter.com/joshcstory/" />
      <meta name="keywords" content={post.keywords} />
    </div>
  );
}

其它更多特性

  • Server Components 和 Server Actions 将成为稳定特性
  • 更多外联样式表能力支持:比如支持通过 precedence指定样式表的优先级,同样优先级的样式表会被放到一起
  • 更多 script 标签能力支持
  • 支持预加载资源

images?url=https%3A%2F%2Fintranetproxy alipay com%2Fskylark%2Flark%2F0%2F2021%2Fpng%2F112013%2F1640597326837-3ff62a59-0406-4505-9f30-69ca7e4ce587 png sign=9879b951034975fc72f598b112e81678b7ac62298fb0e5c2223271a978c34555

@SteveZhang08
Copy link

佩服,我从未见过如此新奇的博客方式,赞

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants