先说明一下,React 19在本文写作时尚未正式发布(预计2024年下半年发布),目前只有RC版本和开发版。本文基于React 19的已公布新特性和RC版本来写,一些API可能会在正式版中调整。如果你在生产环境使用,建议等正式版发布后再升级。

React 19是React的一个大版本更新,带来了很多令人期待的新特性,比如Actions、use() Hook、useOptimistic、useActionState、文档元数据API、资源加载API等。这些新特性能让我们写出更简洁、更优雅、更易维护的代码。

很多老项目里的React代码,因为历史原因,写得比较啰嗦、复杂、难以维护。React 19的新特性,给了我们一个重构的好机会。这篇文章就来聊聊,如何用React 19的新特性,把烂代码重构成优雅代码。

React 19的核心新特性

在讲重构之前,先快速了解一下React 19的核心新特性。

Actions

Actions是React 19最重要的新特性之一。它是一种处理异步状态更新的新方式,特别适合处理表单提交、数据更新等异步操作。

在React 19之前,处理异步操作需要手动管理loading状态、error状态、success状态,代码很啰嗦。用了Actions之后,React会自动管理这些状态,代码大大简化。

Actions的基本用法:

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

  const handleSubmit = () => {
    startTransition(async () => {
      const error = await updateName(name);
      if (error) {
        setError(error);
        return;
      }
      redirect("/path");
    });
  };

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

用了Actions之后(配合useActionState),代码可以简化为:

function UpdateName() {
  const [name, setName] = useState("");
  const [error, submitAction, isPending] = useActionState(
    async (previousState, formData) => {
      const error = await updateName(formData.get("name"));
      if (error) {
        return error;
      }
      redirect("/path");
      return null;
    },
    null
  );

  return (
    <form action={submitAction}>
      <input type="text" name="name" />
      <button type="submit" disabled={isPending}>更新</button>
      {error && <p>{error}</p>}
    </form>
  );
}

可以看到,代码简洁了很多,不需要手动管理isPending和error状态,React自动帮你管理。

use() Hook

use()是React 19新增的一个Hook,它可以在渲染过程中读取Promise和Context的值。

最强大的地方是,use()可以在条件语句和循环中调用,这是其他Hook做不到的(其他Hook必须在组件顶层调用,不能在条件和循环里)。

use()读取Promise的用法:

function Comments({ commentsPromise }) {
  // use()会暂停渲染,直到Promise resolve
  const comments = use(commentsPromise);
  return comments.map(c => <Comment key={c.id} comment={c} />);
}

use()读取Context的用法:

function Heading() {
  // 可以在条件语句中调用use()
  if (use(ThemeContext) === "dark") {
    return <DarkHeading />;
  }
  return <LightHeading />;
}

use()让我们能更灵活地处理异步数据和Context,代码也更简洁。

useOptimistic

useOptimistic是React 19新增的Hook,用来实现乐观更新。乐观更新就是在异步操作完成之前,先假设操作成功,更新UI,让用户感觉响应很快。如果操作失败了,再回滚。

在React 19之前,实现乐观更新需要手动管理状态,代码很复杂。用了useOptimistic之后,代码大大简化。

用法:

function LikeButton({ messageId, initialLikes }) {
  const [likes, addLike] = useOptimistic(
    initialLikes,
    (state, newLike) => state + newLike
  );

  return (
    <button
      onClick={async () => {
        addLike(1); // 乐观更新,立即+1
        await addLikeToServer(messageId); // 异步请求
      }}
    >
      {likes} 个赞
    </button>
  );
}

文档元数据API

React 19新增了内置的文档元数据支持,可以在组件里直接渲染<title>、<meta>、<link>等标签,React会自动把它们提升到document.head里。

在React 19之前,需要用react-helmet等第三方库来管理文档元数据。现在不需要了,React内置支持。

用法:

function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={post.url} />
      <meta property="og:title" content={post.title} />
      <meta property="og:description" content={post.excerpt} />
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

React会自动把这些标签移到head里,不需要额外的库。

资源加载API

React 19新增了资源加载的生命周期钩子,可以在组件里预加载脚本、样式、字体等资源,提升性能。

比如preload一个脚本:

import { preload } from "react-dom";

function App() {
  preload("https://example.com/analytics.js", { as: "script" });
  // ...
}

还有preinit、preloadDNS等API,可以更精细地控制资源加载。

ref作为prop

React 19之前,函数组件不能直接接收ref prop,需要用forwardRef。React 19之后,函数组件可以直接接收ref作为prop,不需要forwardRef了。

// React 19之前
const MyInput = forwardRef(function MyInput(props, ref) {
  return <input ref={ref} {...props} />;
});

// React 19之后
function MyInput({ ref, ...props }) {
  return <input ref={ref} {...props} />;
}

这个变化让代码更简洁,也更容易理解。

重构案例一:表单提交

来看第一个重构案例:表单提交。

这是React 19之前常见的表单提交代码:

function UserProfileForm({ user }) {
  const [name, setName] = useState(user.name);
  const [email, setEmail] = useState(user.email);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [error, setError] = useState(null);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    setError(null);
    setSuccess(false);

    try {
      const res = await fetch("/api/user", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, email }),
      });
      if (!res.ok) throw new Error("更新失败");
      setSuccess(true);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label>姓名</label>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          disabled={isSubmitting}
        />
      </div>
      <div>
        <label>邮箱</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          disabled={isSubmitting}
        />
      </div>
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? "提交中..." : "保存"}
      </button>
      {error && <p className="error">{error}</p>}
      {success && <p className="success">保存成功!</p>}
    </form>
  );
}

这段代码的问题:

  1. 状态太多,name、email、isSubmitting、error、success,五个状态,管理起来很麻烦。
  2. 手动管理提交状态,isSubmitting、error、success都要手动设置,容易出错。
  3. 用受控组件,每个input都要写onChange,代码啰嗦。

用React 19的Actions和useActionState重构:

function UserProfileForm({ user }) {
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      try {
        const res = await fetch("/api/user", {
          method: "PUT",
          body: formData, // 直接用FormData
        });
        if (!res.ok) throw new Error("更新失败");
        return { success: true, error: null };
      } catch (err) {
        return { success: false, error: err.message };
      }
    },
    { success: false, error: null }
  );

  return (
    <form action={formAction}>
      <div>
        <label>姓名</label>
        <input
          type="text"
          name="name"
          defaultValue={user.name}
          disabled={isPending}
        />
      </div>
      <div>
        <label>邮箱</label>
        <input
          type="email"
          name="email"
          defaultValue={user.email}
          disabled={isPending}
        />
      </div>
      <button type="submit" disabled={isPending}>
        {isPending ? "提交中..." : "保存"}
      </button>
      {state.error && <p className="error">{state.error}</p>}
      {state.success && <p className="success">保存成功!</p>}
    </form>
  );
}

重构后的改进:

  1. 状态大大减少,不需要手动管理name、email、isSubmitting、error、success,React自动管理提交状态。
  2. 用非受控组件(defaultValue),不需要写onChange,代码更简洁。
  3. 直接用FormData提交,不需要手动构造JSON。
  4. 代码量减少了近一半,逻辑更清晰,更不容易出错。

重构案例二:异步数据加载

第二个重构案例:异步数据加载。

React 19之前常见的异步数据加载代码:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    setError(null);

    fetch(`/api/users/${userId}`)
      .then((res) => {
        if (!res.ok) throw new Error("加载失败");
        return res.json();
      })
      .then((data) => {
        if (!cancelled) setUser(data);
      })
      .catch((err) => {
        if (!cancelled) setError(err.message);
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });

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

  if (loading) return <div>加载中...</div>;
  if (error) return <div className="error">{error}</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{user.bio}</p>
    </div>
  );
}

这段代码的问题:

  1. 状态太多,user、loading、error三个状态。
  2. useEffect里的异步逻辑很复杂,要处理cancelled标志,防止组件卸载后setState。
  3. 错误处理和加载状态的判断分散在各处。

用React 19的use()和Suspense重构:

首先,把数据获取逻辑抽成一个Promise:

// 数据获取函数,返回Promise
function fetchUser(userId) {
  return fetch(`/api/users/${userId}`).then((res) => {
    if (!res.ok) throw new Error("加载失败");
    return res.json();
  });
}

然后,用use()读取Promise:

function UserProfile({ userId }) {
  const userPromise = fetchUser(userId); // 注意:实际项目中要缓存这个Promise

  return (
    <Suspense fallback={<div>加载中...</div>}>
      <UserDetails userPromise={userPromise} />
    </Suspense>
  );
}

function UserDetails({ userPromise }) {
  const user = use(userPromise); // use()会等待Promise resolve

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{user.bio}</p>
    </div>
  );
}

错误处理用ErrorBoundary:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

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

  render() {
    if (this.state.hasError) {
      return <div className="error">{this.state.error.message}</div>;
    }
    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>加载中...</div>}>
        <UserProfile userId={1} />
      </Suspense>
    </ErrorBoundary>
  );
}

重构后的改进:

  1. 组件里不需要手动管理loading和error状态,Suspense和ErrorBoundary自动处理。
  2. 组件逻辑更纯粹,只关心渲染数据,不关心数据加载的状态。
  3. 错误处理统一在ErrorBoundary里,不需要每个组件都写一遍。
  4. 代码更简洁,更符合React的声明式编程风格。

注意:实际项目中,fetchUser返回的Promise需要缓存,否则每次渲染都会重新发起请求。可以用React的cache()函数,或者用SWR、React Query等库来管理。

重构案例三:乐观更新

第三个重构案例:点赞功能的乐观更新。

React 19之前的乐观更新代码:

function LikeButton({ postId, initialLikes, initialLiked }) {
  const [likes, setLikes] = useState(initialLikes);
  const [liked, setLiked] = useState(initialLiked);
  const [isPending, setIsPending] = useState(false);

  const handleClick = async () => {
    if (isPending) return;

    // 乐观更新
    const prevLikes = likes;
    const prevLiked = liked;
    setLikes(liked ? likes - 1 : likes + 1);
    setLiked(!liked);
    setIsPending(true);

    try {
      const res = await fetch(`/api/posts/${postId}/like`, {
        method: liked ? "DELETE" : "POST",
      });
      if (!res.ok) throw new Error("操作失败");
    } catch (err) {
      // 失败回滚
      setLikes(prevLikes);
      setLiked(prevLiked);
      alert(err.message);
    } finally {
      setIsPending(false);
    }
  };

  return (
    <button onClick={handleClick} disabled={isPending}>
      {liked ? "取消点赞" : "点赞"} ({likes})
    </button>
  );
}

这段代码的问题:

  1. 手动管理乐观更新和回滚,逻辑复杂,容易出错。
  2. 状态多,likes、liked、isPending三个状态。
  3. 要手动保存之前的状态,失败了再回滚,代码啰嗦。

用React 19的useOptimistic重构:

function LikeButton({ postId, initialLikes, initialLiked }) {
  const [liked, setLiked] = useState(initialLiked);
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, delta) => state + delta
  );
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    const delta = liked ? -1 : 1;
    setLiked(!liked);

    startTransition(async () => {
      addOptimisticLike(delta); // 乐观更新
      try {
        const res = await fetch(`/api/posts/${postId}/like`, {
          method: liked ? "DELETE" : "POST",
        });
        if (!res.ok) throw new Error("操作失败");
      } catch (err) {
        // useOptimistic会自动回滚乐观更新
        setLiked(liked); // 回滚liked状态
        alert(err.message);
      }
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending}>
      {liked ? "取消点赞" : "点赞"} ({optimisticLikes})
    </button>
  );
}

重构后的改进:

  1. 乐观更新由useOptimistic管理,不需要手动保存和回滚likes状态。
  2. 代码更简洁,逻辑更清晰。
  3. useOptimistic会在异步操作失败后自动回滚乐观状态,不需要手动处理。

重构案例四:文档标题和元数据

第四个重构案例:页面标题和元数据管理。

React 19之前,需要用react-helmet等第三方库来管理文档标题和元数据:

import { Helmet } from "react-helmet";

function BlogPost({ post }) {
  return (
    <article>
      <Helmet>
        <title>{post.title} - 我的博客</title>
        <meta name="description" content={post.excerpt} />
        <link rel="canonical" href={`https://myblog.com/posts/${post.id}`} />
        <meta property="og:title" content={post.title} />
        <meta property="og:description" content={post.excerpt} />
        <meta property="og:image" content={post.cover} />
      </Helmet>
      <h1>{post.title}</h1>
      <p className="meta">{post.date} · {post.author}</p>
      <div className="content">{post.content}</div>
    </article>
  );
}

问题:需要依赖第三方库react-helmet,增加了项目的依赖和体积。

用React 19的文档元数据API重构:

function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title} - 我的博客</title>
      <meta name="description" content={post.excerpt} />
      <link rel="canonical" href={`https://myblog.com/posts/${post.id}`} />
      <meta property="og:title" content={post.title} />
      <meta property="og:description" content={post.excerpt} />
      <meta property="og:image" content={post.cover} />
      <h1>{post.title}</h1>
      <p className="meta">{post.date} · {post.author}</p>
      <div className="content">{post.content}</div>
    </article>
  );
}

重构后的改进:

  1. 不需要第三方库,React内置支持,减少了项目依赖。
  2. 代码更简洁,直接在组件里写title、meta、link标签,React自动提升到head。
  3. 性能更好,不需要额外的库运行时开销。

重构案例五:Context消费

第五个重构案例:Context的消费。

React 19之前,消费Context要用useContext Hook,而且必须在组件顶层调用,不能在条件语句里:

function UserPanel() {
  const user = useContext(UserContext);
  const theme = useContext(ThemeContext);

  if (!user) {
    return <LoginPrompt />;
  }

  return (
    <div className={theme === "dark" ? "dark-panel" : "light-panel"}>
      <h2>欢迎,{user.name}</h2>
      <p>邮箱:{user.email}</p>
      <UserSettings user={user} theme={theme} />
    </div>
  );
}

问题:即使user为null,不需要theme,也要先调用useContext(ThemeContext),因为Hook不能在条件语句里调用。

用React 19的use()重构:

function UserPanel() {
  const user = use(UserContext);

  if (!user) {
    return <LoginPrompt />;
  }

  // 可以在条件语句之后调用use()
  const theme = use(ThemeContext);

  return (
    <div className={theme === "dark" ? "dark-panel" : "light-panel"}>
      <h2>欢迎,{user.name}</h2>
      <p>邮箱:{user.email}</p>
      <UserSettings user={user} theme={theme} />
    </div>
  );
}

重构后的改进:

  1. use()可以在条件语句之后调用,只有在需要的时候才消费Context。
  2. 代码逻辑更清晰,不需要在组件顶层声明所有的Context。
  3. 性能更好,不需要的Context不会订阅,减少不必要的重渲染。

重构的注意事项

用React 19新特性重构代码,有一些注意事项。

第一,渐进式重构,不要一次性全改。React 19的新特性和旧代码是兼容的,可以一个组件一个组件地重构,先重构最复杂、最需要改进的组件,其他的保持原样。

第二,充分测试。重构可能会引入bug,特别是异步逻辑和状态管理的部分。重构之后要充分测试,确保功能正常。建议有单元测试和集成测试的保障。

第三,关注性能。React 19的新特性能提升性能,但如果用得不好,也可能导致性能问题。比如use()如果没有正确缓存Promise,会导致重复请求。useOptimistic如果用得太频繁,也可能导致状态混乱。要在实际环境中测试性能。

第四,考虑兼容性。如果你的项目需要支持旧版本的React,或者有第三方库依赖旧的API,升级React 19之前要检查兼容性。特别是一些依赖forwardRef、useEffect等API的库,可能需要升级。

第五,团队学习成本。React 19的新特性需要团队成员学习和适应。重构之前,要确保团队成员理解新特性的用法和最佳实践,避免写出不规范的代码。

写在最后

React 19的新特性,给了我们一个重构老旧代码的好机会。Actions、use()、useOptimistic、文档元数据API等新特性,能让我们写出更简洁、更优雅、更易维护的代码。

但重构不是目的,提升代码质量和可维护性才是目的。不要为了用新特性而用新特性,要根据实际情况,选择合适的特性来重构。有些旧代码虽然写得老,但如果运行稳定、易于维护,也不一定需要重构。

重构的时候,要遵循渐进式、小步快跑的原则,一个组件一个组件地改,每改一个都要测试,确保不引入新的bug。不要一次性大改,那样风险太大。

React在不断进化,新的特性和最佳实践也在不断出现。作为开发者,要保持学习,跟上技术的发展,用最新的、最好的方式来写代码。但也不要盲目追新,要根据项目的实际情况,选择合适的技术和方案。

希望这篇文章能帮你更好地理解React 19的新特性,并用它们来提升你的代码质量。有什么问题欢迎交流。