React 实验室:查看过渡、活动等

2025年4月23日,作者 Ricky Hanlon

🌐 April 23, 2025 by Ricky Hanlon


在 React Labs 的文章中,我们会撰写关于正在进行研究和开发的项目。在这篇文章中,我们将分享两个可以立即尝试的新实验性功能,以及我们目前正在进行的其他字段的更新。

🌐 In React Labs posts, we write about projects in active research and development. In this post, we’re sharing two new experimental features that are ready to try today, and updates on other areas we’re working on now.

今天,我们很高兴发布两个新的实验性功能的文档,这些功能已准备好进行测试:

🌐 Today, we’re excited to release documentation for two new experimental features that are ready for testing:

我们还在分享当前正在开发的新功能的更新:

🌐 We’re also sharing updates on new features currently in development:


新实验功能

🌐 New Experimental Features

注意

<Activity /> 已在 react@19.2 发货。

<ViewTransition />addTransitionType 现在可以在 react@canary 获得。

视图过渡和活动现已可以在 react@experimental 中进行测试。这些功能已在生产环境中进行测试并且稳定,但随着我们采纳反馈,最终的 API 可能仍会发生变化。

🌐 View Transitions and Activity are now ready for testing in react@experimental. These features have been tested in production and are stable, but the final API may still change as we incorporate feedback.

你可以通过将 React 包升级到最新的实验版本来尝试它们:

🌐 You can try them by upgrading React packages to the most recent experimental version:

  • react@experimental
  • react-dom@experimental

继续阅读以了解如何在你的应用中使用这些功能,或查看新发布的文档:

🌐 Read on to learn how to use these features in your app, or check out the newly published docs:

视图过渡

🌐 View Transitions

React 视图过渡是一个新的实验性功能,它使在应用的 UI 过渡中添加动画更加容易。在底层,这些动画使用大多数现代浏览器中可用的新 startViewTransition API。

🌐 React View Transitions are a new experimental feature that makes it easier to add animations to UI transitions in your app. Under-the-hood, these animations use the new startViewTransition API available in most modern browsers.

要选择为元素添加动画,请将其封装在新的 <ViewTransition> 组件中:

🌐 To opt-in to animating an element, wrap it in the new <ViewTransition> component:

// "what" to animate.
<ViewTransition>
<div>animate me</div>
</ViewTransition>

这个新组件让你以声明式方式定义在动画被激活时要“动画化”的内容。

🌐 This new component lets you declaratively define “what” to animate when an animation is activated.

你可以通过使用以下三种触发器之一来定义 View 转换的动画“时间”:

🌐 You can define “when” to animate by using one of these three triggers for a View Transition:

// "when" to animate.

// Transitions
startTransition(() => setState(...));

// Deferred Values
const deferred = useDeferredValue(value);

// Suspense
<Suspense fallback={<Fallback />}>
<div>Loading...</div>
</Suspense>

默认情况下,这些动画使用应用的视图过渡默认 CSS 动画(通常是平滑的交叉淡入淡出)。你可以使用视图过渡伪选择器来定义动画的“运行方式”。例如,你可以使用 * 来更改所有过渡的默认动画:

🌐 By default, these animations use the default CSS animations for View Transitions applied (typically a smooth cross-fade). You can use view transition pseudo-selectors to define “how” the animation runs. For example, you can use * to change the default animation for all transitions:

// "how" to animate.
::view-transition-old(*) {
animation: 300ms ease-out fade-out;
}
::view-transition-new(*) {
animation: 300ms ease-in fade-in;
}

当由于动画触发器(例如 startTransitionuseDeferredValueSuspense 回退切换到内容)而更新 DOM 时,React 将使用声明性启发式方法 自动确定应为动画激活哪些 <ViewTransition> 组件。然后,浏览器将运行 CSS 中定义的动画。

🌐 When the DOM updates due to an animation trigger—like startTransition, useDeferredValue, or a Suspense fallback switching to content—React will use declarative heuristics to automatically determine which <ViewTransition> components to activate for the animation. The browser will then run the animation that’s defined in CSS.

如果你熟悉浏览器的视图过渡(View Transition)API,并且想了解 React 如何支持它,请查看文档中的 <ViewTransition> 是如何工作的

🌐 If you’re familiar with the browser’s View Transition API and want to know how React supports it, check out How does <ViewTransition> Work in the docs.

在这篇文章中,让我们来看几个如何使用视图过渡的例子。

🌐 In this post, let’s take a look at a few examples of how to use View Transitions.

我们将从这个应用开始,它不会对以下任意交互进行动画处理:

🌐 We’ll start with this app, which doesn’t animate any of the following interactions:

  • 点击一个视频以查看详细信息。
  • 点击“返回”以回到动态页面。
  • 在列表中输入以筛选视频。
import TalkDetails from './Details'; import Home from './Home'; import {useRouter} from './router';

export default function App() {
  const {url} = useRouter();

  // 🚩This version doesn't include any animations yet
  return url === '/' ? <Home /> : <TalkDetails />;
}

注意

视图过渡并不能取代由 CSS 和 JS 驱动的动画

🌐 View Transitions do not replace CSS and JS driven animations

视图过渡旨在用于用户界面过渡,例如导航、展开、打开或重新排序。它们并不意味着要取代应用中的所有动画。

🌐 View Transitions are meant to be used for UI transitions such as navigation, expanding, opening, or re-ordering. They are not meant to replace all the animations in your app.

在我们上面的示例应用中,注意当你点击“喜欢”按钮以及在 Suspense 回退闪烁时已经有动画。这些是 CSS 动画的良好使用案例,因为它们正在对特定元素进行动画处理。

🌐 In our example app above, notice that there are already animations when you click the “like” button and in the Suspense fallback glimmer. These are good use cases for CSS animations because they are animating a specific element.

导航动画

🌐 Animating navigations

我们的应用包括一个启用了 Suspense 的路由,页面过渡已标记为过渡,这意味着导航是通过 startTransition 执行的:

🌐 Our app includes a Suspense-enabled router, with page transitions already marked as Transitions, which means navigations are performed with startTransition:

function navigate(url) {
startTransition(() => {
go(url);
});
}

startTransition 是一个视图过渡触发器,因此我们可以添加 <ViewTransition> 来实现页面之间的动画效果:

// "what" to animate
<ViewTransition key={url}>
{url === '/' ? <Home /> : <TalkDetails />}
</ViewTransition>

url 变化时,<ViewTransition> 和新路由会被渲染。由于 <ViewTransition>startTransition 内被更新,<ViewTransition> 被激活以进行动画。

🌐 When the url changes, the <ViewTransition> and new route are rendered. Since the <ViewTransition> was updated inside of startTransition, the <ViewTransition> is activated for an animation.

默认情况下,视图过渡包括浏览器的默认交叉淡入淡出动画。将其添加到我们的示例中后,每当我们在页面之间导航时,就会出现交叉淡入淡出效果:

🌐 By default, View Transitions include the browser default cross-fade animation. Adding this to our example, we now have a cross-fade whenever we navigate between pages:

import {ViewTransition} from 'react'; import Details from './Details';
import Home from './Home'; import {useRouter} from './router';

export default function App() {
  const {url} = useRouter();

  // Use ViewTransition to animate between pages.
  // No additional CSS needed by default.
  return (
    <ViewTransition>
      {url === '/' ? <Home /> : <Details />}
    </ViewTransition>
  );
}

由于我们的路由已经使用 startTransition 更新了路由,这一行将 <ViewTransition> 添加的更改会以默认的交叉淡入淡出动画激活。

🌐 Since our router already updates the route using startTransition, this one line change to add <ViewTransition> activates with the default cross-fade animation.

如果你想知道这是如何工作的,请参阅<ViewTransition> 是如何工作的?的文档

🌐 If you’re curious how this works, see the docs for How does <ViewTransition> work?

注意

选择退出 <ViewTransition> 动画

🌐 Opting out of <ViewTransition> animations

在这个例子中,我们为了简单起见将应用的根封装在 <ViewTransition> 中,但这意味着应用中的所有过渡都会被动画化,这可能导致意外的动画效果。

🌐 In this example, we’re wrapping the root of the app in <ViewTransition> for simplicity, but this means that all transitions in the app will be animated, which can lead to unexpected animations.

为了解决这个问题,我们将路由子组件用 "none" 封装,这样每个页面可以控制自己的动画:

🌐 To fix, we’re wrapping route children with "none" so each page can control its own animation:

// Layout.js
<ViewTransition default="none">
{children}
</ViewTransition>

在实践中,导航应该通过“enter”和“exit”属性进行,或者使用过渡类型。

🌐 In practice, navigations should be done via “enter” and “exit” props, or by using Transition Types.

自定义动画

🌐 Customizing animations

默认情况下,<ViewTransition> 包括来自浏览器的默认交叉淡入淡出效果。

🌐 By default, <ViewTransition> includes the default cross-fade from the browser.

要自定义动画,你可以向 <ViewTransition> 组件提供属性,以根据 <ViewTransition> 如何激活 来指定使用哪些动画。

🌐 To customize animations, you can provide props to the <ViewTransition> component to specify which animations to use, based on how the <ViewTransition> activates.

例如,我们可以减慢 default 交叉淡入动画:

🌐 For example, we can slow down the default cross fade animation:

<ViewTransition default="slow-fade">
<Home />
</ViewTransition>

并使用 视图过渡类 在 CSS 中定义 slow-fade

🌐 And define slow-fade in CSS using view transition classes:

::view-transition-old(.slow-fade) {
animation-duration: 500ms;
}

::view-transition-new(.slow-fade) {
animation-duration: 500ms;
}

现在,交叉淡入淡出更慢了:

🌐 Now, the cross fade is slower:

import { ViewTransition } from "react";
import Details from "./Details";
import Home from "./Home";
import { useRouter } from "./router";

export default function App() {
  const { url } = useRouter();

  // Define a default animation of .slow-fade.
  // See animations.css for the animation definition.
  return (
    <ViewTransition default="slow-fade">
      {url === '/' ? <Home /> : <Details />}
    </ViewTransition>
  );
}

有关 <ViewTransition> 样式的完整指南,请参阅 Styling View Transitions

🌐 See Styling View Transitions for a full guide on styling <ViewTransition>.

共享元素过渡

🌐 Shared Element Transitions

当两个页面包含相同的元素时,你通常希望将它从一个页面动画到下一个页面。

🌐 When two pages include the same element, often you want to animate it from one page to the next.

为此,你可以在 <ViewTransition> 中添加一个唯一的 name

🌐 To do this you can add a unique name to the <ViewTransition>:

<ViewTransition name={`video-${video.id}`}>
<Thumbnail video={video} />
</ViewTransition>

现在视频缩略图在两个页面之间动画显示:

🌐 Now the video thumbnail animates between the two pages:

import { useState, ViewTransition } from "react"; import LikeButton from "./LikeButton"; import { useRouter } from "./router"; import { PauseIcon, PlayIcon } from "./Icons"; import { startTransition } from "react";

export function Thumbnail({ video, children }) {
  // Add a name to animate with a shared element transition.
  // This uses the default animation, no additional css needed.
  return (
    <ViewTransition name={`video-${video.id}`}>
      <div
        aria-hidden="true"
        tabIndex={-1}
        className={`thumbnail ${video.image}`}
      >
        {children}
      </div>
    </ViewTransition>
  );
}

export function VideoControls() {
  const [isPlaying, setIsPlaying] = useState(false);

  return (
    <span
      className="controls"
      onClick={() =>
        startTransition(() => {
          setIsPlaying((p) => !p);
        })
      }
    >
      {isPlaying ? <PauseIcon /> : <PlayIcon />}
    </span>
  );
}

export function Video({ video }) {
  const { navigate } = useRouter();

  return (
    <div className="video">
      <div
        className="link"
        onClick={(e) => {
          e.preventDefault();
          navigate(`/video/${video.id}`);
        }}
      >
        <Thumbnail video={video}></Thumbnail>

        <div className="info">
          <div className="video-title">{video.title}</div>
          <div className="video-description">{video.description}</div>
        </div>
      </div>
      <LikeButton video={video} />
    </div>
  );
}

默认情况下,React 会自动为每个激活过渡的元素生成一个唯一的 name(参见 <ViewTransition> 是如何工作的)。当 React 看到一个过渡,其中一个带有 name<ViewTransition> 被移除,并且一个具有相同 name 的新 <ViewTransition> 被添加时,它将激活共享元素过渡。

🌐 By default, React automatically generates a unique name for each element activated for a transition (see How does <ViewTransition> work). When React sees a transition where a <ViewTransition> with a name is removed and a new <ViewTransition> with the same name is added, it will activate a shared element transition.

欲了解更多信息,请参阅 共享元素动画 的文档。

🌐 For more info, see the docs for Animating a Shared Element.

基于原因的动画

🌐 Animating based on cause

有时,你可能希望元素根据触发方式有不同的动画效果。针对这种情况,我们添加了一个新的 API,称为 addTransitionType,用于指定过渡的原因:

🌐 Sometimes, you may want elements to animate differently based on how it was triggered. For this use case, we’ve added a new API called addTransitionType to specify the cause of a transition:

function navigate(url) {
startTransition(() => {
// Transition type for the cause "nav forward"
addTransitionType('nav-forward');
go(url);
});
}
function navigateBack(url) {
startTransition(() => {
// Transition type for the cause "nav backward"
addTransitionType('nav-back');
go(url);
});
}

使用过渡类型,你可以通过 props 为 <ViewTransition> 提供自定义动画。让我们为“6 个视频”和“返回”的标题添加共享元素过渡:

🌐 With transition types, you can provide custom animations via props to <ViewTransition>. Let’s add a shared element transition to the header for “6 Videos” and “Back”:

<ViewTransition
name="nav"
share={{
'nav-forward': 'slide-forward',
'nav-back': 'slide-back',
}}>
{heading}
</ViewTransition>

在这里,我们传递一个 share 属性来定义如何根据过渡类型进行动画。当共享过渡从 nav-forward 激活时,会应用视图过渡类 slide-forward。当它从 nav-back 激活时,会启动 slide-back 动画。让我们在 CSS 中定义这些动画:

🌐 Here we pass a share prop to define how to animate based on the transition type. When the share transition activates from nav-forward, the view transition class slide-forward is applied. When it’s from nav-back, the slide-back animation is activated. Let’s define these animations in CSS:

::view-transition-old(.slide-forward) {
/* when sliding forward, the "old" page should slide out to left. */
animation: ...
}

::view-transition-new(.slide-forward) {
/* when sliding forward, the "new" page should slide in from right. */
animation: ...
}

::view-transition-old(.slide-back) {
/* when sliding back, the "old" page should slide out to right. */
animation: ...
}

::view-transition-new(.slide-back) {
/* when sliding back, the "new" page should slide in from left. */
animation: ...
}

现在我们可以根据导航类型对标题和缩略图进行动画处理:

🌐 Now we can animate the header along with thumbnail based on navigation type:

import {ViewTransition} from 'react'; import { useIsNavPending } from "./router";

export default function Page({ heading, children }) {
  const isPending = useIsNavPending();
  return (
    <div className="page">
      <div className="top">
        <div className="top-nav">
          
          <ViewTransition
            name="nav"
            share={{
              'nav-forward': 'slide-forward',
              'nav-back': 'slide-back',
            }}>
            {heading}
          </ViewTransition>
          {isPending && <span className="loader"></span>}
        </div>
      </div>
      
      
      <ViewTransition default="none">
        <div className="bottom">
          <div className="content">{children}</div>
        </div>
      </ViewTransition>
    </div>
  );
}

动画Suspense边界

🌐 Animating Suspense Boundaries

Suspense也将激活视图转换。

🌐 Suspense will also activate View Transitions.

为了使内容回退具有动画效果,我们可以用 <ViewTranstion> 封装 Suspense

🌐 To animate the fallback to content, we can wrap Suspense with <ViewTranstion>:

<ViewTransition>
<Suspense fallback={<VideoInfoFallback />}>
<VideoInfo />
</Suspense>
</ViewTransition>

通过添加这个,备用内容将会交叉淡入到实际内容中。点击一个视频,看看视频信息的动画效果:

🌐 By adding this, the fallback will cross-fade into the content. Click a video and see the video info animate in:

import { use, Suspense, ViewTransition } from "react"; import { fetchVideo, fetchVideoDetails } from "./data"; import { Thumbnail, VideoControls } from "./Videos"; import { useRouter } from "./router"; import Layout from "./Layout"; import { ChevronLeft } from "./Icons";

function VideoDetails({ id }) {
  // Cross-fade the fallback to content.
  return (
    <ViewTransition default="slow-fade">
      <Suspense fallback={<VideoInfoFallback />}>
          <VideoInfo id={id} />
      </Suspense>
    </ViewTransition>
  );
}

function VideoInfoFallback() {
  return (
    <div>
      <div className="fit fallback title"></div>
      <div className="fit fallback description"></div>
    </div>
  );
}

export default function Details() {
  const { url, navigateBack } = useRouter();
  const videoId = url.split("/").pop();
  const video = use(fetchVideo(videoId));

  return (
    <Layout
      heading={
        <div
          className="fit back"
          onClick={() => {
            navigateBack("/");
          }}
        >
          <ChevronLeft /> Back
        </div>
      }
    >
      <div className="details">
        <Thumbnail video={video} large>
          <VideoControls />
        </Thumbnail>
        <VideoDetails id={video.id} />
      </div>
    </Layout>
  );
}

function VideoInfo({ id }) {
  const details = use(fetchVideoDetails(id));
  return (
    <div>
      <p className="fit info-title">{details.title}</p>
      <p className="fit info-description">{details.description}</p>
    </div>
  );
}

我们还可以在备用动画上使用 exit,在内容上使用 enter 来提供自定义动画:

🌐 We can also provide custom animations using an exit on the fallback, and enter on the content:

<Suspense
fallback={
<ViewTransition exit="slide-down">
<VideoInfoFallback />
</ViewTransition>
}
>
<ViewTransition enter="slide-up">
<VideoInfo id={id} />
</ViewTransition>
</Suspense>

下面是我们将如何使用 CSS 定义 slide-downslide-up

🌐 Here’s how we’ll define slide-down and slide-up with CSS:

::view-transition-old(.slide-down) {
/* Slide the fallback down */
animation: ...;
}

::view-transition-new(.slide-up) {
/* Slide the content up */
animation: ...;
}

现在,Suspense 内容用滑动动画替换了回退内容:

🌐 Now, the Suspense content replaces the fallback with a sliding animation:

import { use, Suspense, ViewTransition } from "react"; import { fetchVideo, fetchVideoDetails } from "./data"; import { Thumbnail, VideoControls } from "./Videos"; import { useRouter } from "./router"; import Layout from "./Layout"; import { ChevronLeft } from "./Icons";

function VideoDetails({ id }) {
  return (
    <Suspense
      fallback={
        // Animate the fallback down.
        <ViewTransition exit="slide-down">
          <VideoInfoFallback />
        </ViewTransition>
      }
    >
      
      <ViewTransition enter="slide-up">
        <VideoInfo id={id} />
      </ViewTransition>
    </Suspense>
  );
}

function VideoInfoFallback() {
  return (
    <>
      <div className="fallback title"></div>
      <div className="fallback description"></div>
    </>
  );
}

export default function Details() {
  const { url, navigateBack } = useRouter();
  const videoId = url.split("/").pop();
  const video = use(fetchVideo(videoId));

  return (
    <Layout
      heading={
        <div
          className="fit back"
          onClick={() => {
            navigateBack("/");
          }}
        >
          <ChevronLeft /> Back
        </div>
      }
    >
      <div className="details">
        <Thumbnail video={video} large>
          <VideoControls />
        </Thumbnail>
        <VideoDetails id={video.id} />
      </div>
    </Layout>
  );
}

function VideoInfo({ id }) {
  const details = use(fetchVideoDetails(id));
  return (
    <>
      <p className="info-title">{details.title}</p>
      <p className="info-description">{details.description}</p>
    </>
  );
}

为列表添加动画

🌐 Animating Lists

你也可以使用 <ViewTransition> 来为项目列表在重新排序时添加动画,就像在可搜索的项目列表中一样:

🌐 You can also use <ViewTransition> to animate lists of items as they re-order, like in a searchable list of items:

<div className="videos">
{filteredVideos.map((video) => (
<ViewTransition key={video.id}>
<Video video={video} />
</ViewTransition>
))}
</div>

要激活 ViewTransition,我们可以使用 useDeferredValue

🌐 To activate the ViewTransition, we can use useDeferredValue:

const [searchText, setSearchText] = useState('');
const deferredSearchText = useDeferredValue(searchText);
const filteredVideos = filterVideos(videos, deferredSearchText);

现在,当你在搜索栏输入时,项目会动画显示:

🌐 Now the items animate as you type in the search bar:

import { useId, useState, use, useDeferredValue, ViewTransition } from "react";import { Video } from "./Videos";import Layout from "./Layout";import { fetchVideos } from "./data";import { IconSearch } from "./Icons";

function SearchList({searchText, videos}) {
  // Activate with useDeferredValue ("when")
  const deferredSearchText = useDeferredValue(searchText);
  const filteredVideos = filterVideos(videos, deferredSearchText);
  return (
    <div className="video-list">
      <div className="videos">
        {filteredVideos.map((video) => (
          // Animate each item in list ("what")
          <ViewTransition key={video.id}>
            <Video video={video} />
          </ViewTransition>
        ))}
      </div>
      {filteredVideos.length === 0 && (
        <div className="no-results">No results</div>
      )}
    </div>
  );
}

export default function Home() {
  const videos = use(fetchVideos());
  const count = videos.length;
  const [searchText, setSearchText] = useState('');

  return (
    <Layout heading={<div className="fit">{count} Videos</div>}>
      <SearchInput value={searchText} onChange={setSearchText} />
      <SearchList videos={videos} searchText={searchText} />
    </Layout>
  );
}

function SearchInput({ value, onChange }) {
  const id = useId();
  return (
    <form className="search" onSubmit={(e) => e.preventDefault()}>
      <label htmlFor={id} className="sr-only">
        Search
      </label>
      <div className="search-input">
        <div className="search-icon">
          <IconSearch />
        </div>
        <input
          type="text"
          id={id}
          placeholder="Search"
          value={value}
          onChange={(e) => onChange(e.target.value)}
        />
      </div>
    </form>
  );
}

function filterVideos(videos, query) {
  const keywords = query
    .toLowerCase()
    .split(" ")
    .filter((s) => s !== "");
  if (keywords.length === 0) {
    return videos;
  }
  return videos.filter((video) => {
    const words = (video.title + " " + video.description)
      .toLowerCase()
      .split(" ");
    return keywords.every((kw) => words.some((w) => w.includes(kw)));
  });
}

最终结果

🌐 Final result

通过添加几个 <ViewTransition> 组件和几行 CSS,我们能够将上面的所有动画添加到最终结果中。

🌐 By adding a few <ViewTransition> components and a few lines of CSS, we were able to add all the animations above into the final result.

我们对视图过渡感到兴奋,并认为它们将提升你能够构建的应用水平。它们已经可以在 React 发布的实验通道中开始尝试。

🌐 We’re excited about View Transitions and think they will level up the apps you’re able to build. They’re ready to start trying today in the experimental channel of React releases.

让我们去掉慢慢淡出的效果,来看看最终结果:

🌐 Let’s remove the slow fade, and take a look at the final result:

import {ViewTransition} from 'react'; import Details from './Details'; import Home from './Home'; import {useRouter} from './router';

export default function App() {
  const {url} = useRouter();

  // Animate with a cross fade between pages.
  return (
    <ViewTransition key={url}>
      {url === '/' ? <Home /> : <Details />}
    </ViewTransition>
  );
}

如果你想了解更多关于它们如何工作的内容,请查看文档中的 <ViewTransition> 是如何工作的

🌐 If you’re curious to know more about how they work, check out How Does <ViewTransition> Work in the docs.

有关我们如何构建视图过渡的更多背景信息,请参见:#31975#32105#32041#32734#32797#31999#32031#32050#32820#32029#32028#32038,作者 @sebmarkbage(感谢 Seb!)。

🌐 For more background on how we built View Transitions, see: #31975, #32105, #32041, #32734, #32797 #31999, #32031, #32050, #32820, #32029, #32028, and #32038 by @sebmarkbage (thanks Seb!).


活动

🌐 Activity

注意

<Activity /> 现在可以在 React 的 Canary 版本中使用。

在这里了解更多关于 React 发布通道的信息。

在过去的更新中,我们分享了正在研究一个API,以允许组件在视觉上被隐藏和降级优先级,同时保留UI状态,并相比卸载组件或使用CSS隐藏,降低性能成本。

🌐 In past updates, we shared that we were researching an API to allow components to be visually hidden and deprioritized, preserving UI state with reduced performance costs relative to unmounting or hiding with CSS.

我们现在已经准备好分享 API 及其工作方式,这样你就可以开始在实验性的 React 版本中进行测试。

🌐 We’re now ready to share the API and how it works, so you can start testing it in experimental React versions.

<Activity> 是一个用于隐藏和显示部分界面的新组件:

<Activity mode={isVisible ? 'visible' : 'hidden'}>
<Page />
</Activity>

当一个活动 可见 时,它会像平常一样渲染。当一个活动 隐藏 时,它会被卸载,但会保存其状态,并以低于屏幕上任何可见内容的优先级继续渲染。

你可以使用 Activity 来保存用户未使用的界面部分的状态,或者预渲染用户可能接下来要使用的部分。

🌐 You can use Activity to save state for parts of the UI the user isn’t using, or pre-render parts that a user is likely to use next.

让我们来看一些改进上述视图过渡示例的例子。

🌐 Let’s look at some examples improving the View Transition examples above.

注意

当一个活动被隐藏时,其效果不会积累。

当一个 <Activity>hidden 时,Effect 会被卸载。从概念上讲,组件被卸载,但 React 会保存状态以便以后使用。

🌐 When an <Activity> is hidden, Effects are unmounted. Conceptually, the component is unmounted, but React saves the state for later.

在实践中,如果你遵循了 You Might Not Need an Effect 指南,这通常会按预期工作。为了主动发现有问题的 Effects,我们建议添加 <StrictMode>,它会主动执行 Activity 的卸载和挂载,以捕获任何意外的副作用。

🌐 In practice, this works as expected if you have followed the You Might Not Need an Effect guide. To eagerly find problematic Effects, we recommend adding <StrictMode> which will eagerly perform Activity unmounts and mounts to catch any unexpected side effects.

使用 Activity 恢复状态

🌐 Restoring state with Activity

当用户离开一个页面时,通常会停止渲染旧页面:

🌐 When a user navigates away from a page, it’s common to stop rendering the old page:

function App() {
const { url } = useRouter();

return (
<>
{url === '/' && <Home />}
{url !== '/' && <Details />}
</>
);
}

然而,这意味着如果用户返回旧页面,之前的所有状态都会丢失。例如,如果 <Home /> 页面有一个 <input> 字段,当用户离开该页面时,<input> 会被卸载,他们输入的所有文本都会丢失。

🌐 However, this means if the user goes back to the old page, all of the previous state is lost. For example, if the <Home /> page has an <input> field, when the user leaves the page the <input> is unmounted, and all of the text they had typed is lost.

Activity 允许你在用户切换页面时保持状态,因此当他们返回时可以从上次离开的地方继续。这是通过将部分树封装在 <Activity> 中并切换 mode 来完成的:

🌐 Activity allows you to keep the state around as the user changes pages, so when they come back they can resume where they left off. This is done by wrapping part of the tree in <Activity> and toggling the mode:

function App() {
const { url } = useRouter();

return (
<>
<Activity mode={url === '/' ? 'visible' : 'hidden'}>
<Home />
</Activity>
{url !== '/' && <Details />}
</>
);
}

通过这个更改,我们可以改进上面的视图过渡示例。之前,当你搜索视频、选择一个视频然后返回时,你的搜索筛选条件会丢失。使用 Activity 后,你的搜索筛选条件会被恢复,你可以从上次离开的地方继续操作。

🌐 With this change, we can improve on our View Transitions example above. Before, when you searched for a video, selected one, and returned, your search filter was lost. With Activity, your search filter is restored and you can pick up where you left off.

尝试搜索一个视频,选择它,然后点击“返回”

🌐 Try searching for a video, selecting it, and clicking “back”:

import { Activity, ViewTransition } from "react"; import Details from "./Details"; import Home from "./Home"; import { useRouter } from "./router";

export default function App() {
  const { url } = useRouter();

  return (
    // View Transitions know about Activity
    <ViewTransition>
      
      <Activity mode={url === '/' ? 'visible' : 'hidden'}>
        <Home />
      </Activity>
      {url !== '/' && <Details />}
    </ViewTransition>
  );
}

使用活动进行预渲染

🌐 Pre-rendering with Activity

有时,你可能希望提前准备用户可能要使用的界面的下一部分,这样当他们准备使用时,它已经准备好了。如果下一个路由需要在其渲染所需的数据上暂停,这尤其有用,因为你可以帮助确保在用户导航之前数据已经被获取。

🌐 Sometimes, you may want to prepare the next part of the UI a user is likely to use ahead of time, so it’s ready by the time they are ready to use it. This is especially useful if the next route needs to suspend on data it needs to render, because you can help ensure the data is already fetched before the user navigates.

例如,我们的应用目前在你选择每个视频时需要暂停以加载数据。我们可以通过在用户导航之前在隐藏的 <Activity> 中渲染所有页面来改进这一点:

🌐 For example, our app currently needs to suspend to load the data for each video when you select one. We can improve this by rendering all of the pages in a hidden <Activity> until the user navigates:

<ViewTransition>
<Activity mode={url === '/' ? 'visible' : 'hidden'}>
<Home />
</Activity>
<Activity mode={url === '/details/1' ? 'visible' : 'hidden'}>
<Details id={id} />
</Activity>
<Activity mode={url === '/details/1' ? 'visible' : 'hidden'}>
<Details id={id} />
</Activity>
<ViewTransition>

通过此更新,如果下一页的内容有时间预渲染,它将在没有 Suspense 回退的情况下进行动画显示。点击一个视频,会注意到详情页上的视频标题和描述会立即渲染,而没有回退:

🌐 With this update, if the content on the next page has time to pre-render, it will animate in without the Suspense fallback. Click a video, and notice that the video title and description on the Details page render immediately, without a fallback:

import { Activity, ViewTransition, use } from "react"; import Details from "./Details"; import Home from "./Home"; import { useRouter } from "./router"; import {fetchVideos} from './data';

export default function App() {
  const { url } = useRouter();
  const videoId = url.split("/").pop();
  const videos = use(fetchVideos());

  return (
    <ViewTransition>
      
      {videos.map(({id}) => (
        <Activity key={id} mode={videoId === id ? 'visible' : 'hidden'}>
          <Details id={id}/>
        </Activity>
      ))}
      <Activity mode={url === '/' ? 'visible' : 'hidden'}>
        <Home />
      </Activity>
    </ViewTransition>
  );
}

带活动的服务器端渲染

🌐 Server-Side Rendering with Activity

在使用在服务器端渲染(SSR)的页面上的 Activity 时,还有额外的优化。

🌐 When using Activity on a page that uses server-side rendering (SSR), there are additional optimizations.

如果页面的一部分是使用 mode="hidden" 渲染的,那么它将不会包含在 SSR 响应中。相反,React 会为 Activity 内的内容安排客户端渲染,同时页面的其余部分进行水合,以优先显示屏幕上的可见内容。

🌐 If part of the page is rendered with mode="hidden", then it will not be included in the SSR response. Instead, React will schedule a client render for the content inside Activity while the rest of the page hydrates, prioritizing the visible content on screen.

对于使用 mode="visible" 渲染的 UI 部分,React 将降低对 Activity 内部内容的 hydration 优先级,这类似于 Suspense 内容以较低优先级进行 hydration 的方式。如果用户与页面进行交互,我们将在需要时优先对边界内的内容进行 hydration。

🌐 For parts of the UI rendered with mode="visible", React will de-prioritize hydration of content within Activity, similar to how Suspense content is hydrated at a lower priority. If the user interacts with the page, we’ll prioritize hydration within the boundary if needed.

这些是高级用例,但它们显示了使用 Activity 时考虑的额外好处。

🌐 These are advanced use cases, but they show the additional benefits considered with Activity.

活动的未来模式

🌐 Future modes for Activity

将来,我们可能会为活动添加更多模式。

🌐 In the future, we may add more modes to Activity.

例如,一个常见的用例是渲染模态窗口,其中之前的“非活动”页面在“活动”模态视图后面仍然可见。“隐藏”模式不适用于此用例,因为它不可见且未包含在服务器端渲染中。

🌐 For example, a common use case is rendering a modal, where the previous “inactive” page is visible behind the “active” modal view. The “hidden” mode does not work for this use case because it’s not visible and not included in SSR.

相反,我们正在考虑一种新模式,该模式可以保持内容可见——并包含在服务器端渲染(SSR)中——但保持不挂载状态并降低更新优先级。该模式可能还需要“暂停”DOM更新,因为在模态窗口打开时看到后台内容更新可能会分散注意力。

🌐 Instead, we’re considering a new mode that would keep the content visible—and included in SSR—but keep it unmounted and de-prioritize updates. This mode may also need to “pause” DOM updates, since it can be distracting to see backgrounded content updating while a modal is open.

我们正在考虑的另一个 Activity 模式是,如果使用的内存过多,能够自动销毁隐藏 Activity 的状态。由于组件已经被卸载,销毁应用中最近最少使用的隐藏部分的状态可能比消耗过多资源更可取。

🌐 Another mode we’re considering for Activity is the ability to automatically destroy state for hidden Activities if there is too much memory being used. Since the component is already unmounted, it may be preferable to destroy state for the least recently used hidden parts of the app rather than consume too many resources.

这些都是我们仍在探索的字段,随着进展我们会分享更多内容。有关今天 Activity 包含的内容的更多信息,请查看文档

🌐 These are areas we’re still exploring, and we’ll share more as we make progress. For more information on what Activity includes today, check out the docs.


开发中的功能

🌐 Features in development

我们也在开发功能来帮助解决以下常见问题。

🌐 We’re also developing features to help solve the common problems below.

在我们对可能的解决方案进行迭代时,你可能会看到我们根据正在合并的 PR 分享的一些正在测试的潜在 API。请记住,当我们尝试不同的想法时,通常会在试用后更改或删除不同的解决方案。

🌐 As we iterate on possible solutions, you may see some potential APIs we’re testing being shared based on the PRs we are landing. Please keep in mind that as we try different ideas, we often change or remove different solutions after trying them out.

当我们正在研究的解决方案过早地被共享时,可能会在社区中引起动荡和混乱。为了在保持透明度和限制混乱之间取得平衡,我们正在分享我们目前正在开发解决方案的问题,但不会分享我们心中具体的解决方案。

🌐 When the solutions we’re working on are shared too early, it can create churn and confusion in the community. To balance being transparent and limiting confusion, we’re sharing the problems we’re currently developing solutions for, without sharing a particular solution we have in mind.

随着这些功能的推进,我们将会在博客上宣布它们,并附上文档,以便你可以试用它们。

🌐 As these features progress, we’ll announce them on the blog with docs included so you can try them out.

React 性能追踪

🌐 React Performance Tracks

我们正在开发一组新的自定义轨道,以便向使用浏览器 API 的性能分析器添加自定义轨道,从而提供关于你 React 应用性能的更多信息。

🌐 We’re working on a new set of custom tracks to performance profilers using browser APIs that allow adding custom tracks to provide more information about the performance of your React app.

此功能仍在进行中,因此我们尚未准备好发布文档,以将其完全作为实验性功能发布。当使用 React 的实验版本时,你可以抢先预览,该版本会自动将性能跟踪添加到配置文件中:

🌐 This feature is still in progress, so we’re not ready to publish docs to fully release it as an experimental feature yet. You can get a sneak preview when using an experimental version of React, which will automatically add the performance tracks to profiles:

我们计划解决一些已知问题,例如性能问题,以及调度器跟踪并不总是将工作“连接”在挂起的树之间,所以它还不完全适合尝试。我们还在收集早期使用者的反馈,以改进跟踪的设计和可用性。

🌐 There are a few known issues we plan to address such as performance, and the scheduler track not always “connecting” work across Suspended trees, so it’s not quite ready to try. We’re also still collecting feedback from early adopters to improve the design and usability of the tracks.

一旦我们解决了这些问题,我们将发布实验性文档,并分享它已准备好尝试的信息。

🌐 Once we solve those issues, we’ll publish experimental docs and share that it’s ready to try.


自动效果依赖

🌐 Automatic Effect Dependencies

当我们发布 hooks 时,我们有三个动机:

🌐 When we released hooks, we had three motivations:

  • 在组件之间共享代码:hooks 取代了诸如渲染属性(render props)和高阶组件(higher-order components)之类的模式,使你能够在不改变组件层次结构的情况下重用有状态逻辑。
  • 以功能为导向,而不是生命周期:Hook 让你可以根据相关的部分(例如设置订阅或获取数据)将一个组件拆分成更小的函数,而不是强制根据生命周期方法进行拆分。
  • 支持提前编译:hooks 旨在支持提前编译,减少由于生命周期方法和类的局限性而导致的意外去优化的陷阱。

自从它们发布以来,Hooks 在在组件之间共享代码方面取得了成功。Hooks 现在是组件之间共享逻辑的首选方式,并且渲染属性和高阶组件的使用情况减少了。Hooks 还在支持诸如快速刷新(Fast Refresh)之类类组件无法实现的功能方面取得了成功。

🌐 Since their release, hooks have been successful at sharing code between components. Hooks are now the favored way to share logic between components, and there are less use cases for render props and higher order components. Hooks have also been successful at supporting features like Fast Refresh that were not possible with class components.

效果可能很难

🌐 Effects can be hard

不幸的是,有些 hooks 仍然很难从函数的角度而不是生命周期的角度去思考。尤其是 Effects,仍然很难理解,是我们从开发者那里听到的最常见的痛点。去年,我们花了大量时间研究 Effects 的使用方式,以及如何简化这些使用场景,使其更容易理解。

🌐 Unfortunately, some hooks are still hard to think in terms of function instead of lifecycles. Effects specifically are still hard to understand and are the most common pain point we hear from developers. Last year, we spent a significant amount of time researching how Effects were used, and how those use cases could be simplified and easier to understand.

我们发现,通常情况下,困惑来自于在不需要使用 Effect 时却使用了它。你可能不需要 Effect 指南涵盖了许多 Effect 并非正确解决方案的情况。然而,即使 Effect 是解决问题的正确选择,Effect 仍然可能比类组件的生命周期更难理解。

🌐 We found that often, the confusion is from using an Effect when you don’t need to. The You Might Not Need an Effect guide covers many cases for when Effects are not the right solution. However, even when an Effect is the right fit for a problem, Effects can still be harder to understand than class component lifecycles.

我们认为造成困惑的原因之一是开发者倾向于从组件的角度(比如生命周期)来看待 Effects,而不是从 Effects 的角度(Effect 做了什么)。

🌐 We believe one of the reasons for confusion is that developers to think of Effects from the component’s perspective (like a lifecycle), instead of the Effects point of view (what the Effect does).

让我们来看一个示例 来自文档:

🌐 Let’s look at an example from the docs:

useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);

许多用户会将这段代码理解为“在挂载时,连接到 roomId。每当 roomId 变化时,断开旧房间的连接并重新建立连接”。然而,这是从组件生命周期的角度来思考的,这意味着你需要考虑每一个组件的生命周期状态才能正确编写 Effect。这可能很困难,所以可以理解当从组件的角度来看时,Effect 似乎比类的生命周期更难。

🌐 Many users would read this code as “on mount, connect to the roomId. whenever roomId changes, disconnect to the old room and re-create the connection”. However, this is thinking from the component’s lifecycle perspective, which means you will need to think of every component lifecycle state to write the Effect correctly. This can be difficult, so it’s understandable that Effects seem harder than class lifecycles when using the component perspective.

无依赖的效果

🌐 Effects without dependencies

相反,最好从 Effect 的角度来思考。Effect 并不了解组件的生命周期。它只描述如何开始同步以及如何停止同步。当用户以这种方式思考 Effects 时,他们的 Effects 往往更容易编写,也更能适应按需多次启动和停止。

🌐 Instead, it’s better to think from the Effect’s perspective. The Effect doesn’t know about the component lifecycles. It only describes how to start synchronization and how to stop it. When users think of Effects in this way, their Effects tend to be easier to write, and more resilient to being started and stopped as many times as is needed.

我们花了一些时间研究为什么 Effects 会从组件的角度来考虑,我们认为其中一个原因是依赖数组。因为你必须写它,它就摆在那儿,直面提醒你正在“响应”的内容,并引导你进入“当这些值变化时执行此操作”的思维模式。

🌐 We spent some time researching why Effects are thought of from the component perspective, and we think one of the reasons is the dependency array. Since you have to write it, it’s right there and in your face reminding you of what you’re “reacting” to and baiting you into the mental model of ‘do this when these values change’.

当我们发布 hooks 时,我们知道可以通过提前编译让它们更易使用。使用 React 编译器,现在在大多数情况下你可以避免自己编写 useCallbackuseMemo。对于 Effects,编译器可以为你插入依赖:

🌐 When we released hooks, we knew we could make them easier to use with ahead-of-time compilation. With the React Compiler, you’re now able to avoid writing useCallback and useMemo yourself in most cases. For Effects, the compiler can insert the dependencies for you:

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}); // compiler inserted dependencies.

使用这段代码,React 编译器可以为你推断依赖并自动插入它们,因此你不需要查看或编写它们。通过像 IDE 扩展useEffectEvent 这样的功能,我们可以提供一个 CodeLens,向你显示编译器插入的内容,以便在需要调试时使用,或者通过删除某个依赖来进行优化。这有助于强化撰写 Effects 的正确思维模型,Effects 可以在任何时候运行,以将你的组件或 hook 的状态与其他内容同步。

🌐 With this code, the React Compiler can infer the dependencies for you and insert them automatically so you don’t need to see or write them. With features like the IDE extension and useEffectEvent, we can provide a CodeLens to show you what the Compiler inserted for times you need to debug, or to optimize by removing a dependency. This helps reinforce the correct mental model for writing Effects, which can run at any time to synchronize your component or hook’s state with something else.

我们的希望是,自动插入依赖不仅更容易编写,而且通过迫使你从效果的作用而不是组件生命周期的角度思考,也使它们更容易理解。

🌐 Our hope is that automatically inserting dependencies is not only easier to write, but that it also makes them easier to understand by forcing you to think in terms of what the Effect does, and not in component lifecycles.


编译器 IDE 扩展

🌐 Compiler IDE Extension

在2025年晚些时候,我们分享了 React Compiler 的第一个稳定版本,并且我们将继续投资以推出更多改进。

🌐 Later in 2025 we shared the first stable release of React Compiler, and we’re continuing to invest in shipping more improvements.

我们也开始探索使用 React 编译器提供信息的方法,以提高对代码的理解和调试能力。我们开始探索的一个想法是一个新的实验性基于 LSP 的 React IDE 扩展,由 React 编译器提供支持,类似于 Lauren Tan 的 React Conf 演讲 中使用的扩展。

🌐 We’ve also begun exploring ways to use the React Compiler to provide information that can improve understanding and debugging your code. One idea we’ve started exploring is a new experimental LSP-based React IDE extension powered by React Compiler, similar to the extension used in Lauren Tan’s React Conf talk.

我们的想法是,我们可以利用编译器的静态分析,在你的 IDE 中直接提供更多信息、建议和优化机会。例如,我们可以提供对违反 React 规则的代码进行诊断,悬停显示组件和 hooks 是否被编译器优化,或者提供 CodeLens 来查看 自动插入的 Effect 依赖

🌐 Our idea is that we can use the compiler’s static analysis to provide more information, suggestions, and optimization opportunities directly in your IDE. For example, we can provide diagnostics for code breaking the Rules of React, hovers to show if components and hooks were optimized by the compiler, or a CodeLens to see automatically inserted Effect dependencies.

IDE 扩展仍处于早期探索阶段,但我们将在未来的更新中分享我们的进展。

🌐 The IDE extension is still an early exploration, but we’ll share our progress in future updates.


片段引用

🌐 Fragment Refs

许多 DOM API,如用于事件管理、定位和焦点的 API,在使用 React 编写时很难组合。这通常导致开发者不得不使用 Effects,管理多个 Refs,并使用诸如 findDOMNode(在 React 19 中移除)的 API。

🌐 Many DOM APIs like those for event management, positioning, and focus are difficult to compose when writing with React. This often leads developers to reach for Effects, managing multiple Refs, by using APIs like findDOMNode (removed in React 19).

我们正在探索为 Fragments 添加 ref,这些 ref 将指向一组 DOM 元素,而不仅仅是单个元素。我们希望这将简化对多个子元素的管理,并在调用 DOM API 时更容易编写可组合的 React 代码。

🌐 We are exploring adding refs to Fragments that would point to a group of DOM elements, rather than just a single element. Our hope is that this will simplify managing multiple children and make it easier to write composable React code when calling DOM APIs.

片段引用仍在研究中。当我们更接近完成最终 API 时,我们会分享更多信息。

🌐 Fragment refs are still being researched. We’ll share more when we’re closer to having the final API finished.


手势动画

🌐 Gesture Animations

我们还在研究增强视图过渡的方法,以支持手势动画,例如滑动以打开菜单或滚动浏览照片轮播。

🌐 We’re also researching ways to enhance View Transitions to support gesture animations such as swiping to open a menu, or scroll through a photo carousel.

手势带来新的挑战有几个原因:

🌐 Gestures present new challenges for a few reasons:

  • 手势是连续的:当你滑动时,动画与手指停留的时间相关,而不是触发后运行至完成。
  • 手势未完成:当你释放手指时,手势动画可以继续播放直到完成,或者恢复到原始状态(就像你只部分打开菜单时一样),这取决于你的手势进行的程度。
  • 手势颠倒新旧:在你进行动画时,你希望动画的页面保持“活跃”并且可以交互。这颠倒了浏览器视图过渡模型,其中“旧”状态是快照,而“新”状态是实时 DOM。

我们相信我们已经找到了一种效果良好的方法,并可能会引入一个用于触发手势过渡的新 API。目前,我们专注于发布 <ViewTransition>,之后会重新考虑手势相关内容。

🌐 We believe we’ve found an approach that works well and may introduce a new API for triggering gesture transitions. For now, we’re focused on shipping <ViewTransition>, and will revisit gestures afterward.


并发存储

🌐 Concurrent Stores

当我们发布支持并发渲染的 React 18 时,我们也发布了 useSyncExternalStore,这样不使用 React 状态或上下文的外部存储库就可以通过在存储更新时强制同步渲染来支持并发渲染

🌐 When we released React 18 with concurrent rendering, we also released useSyncExternalStore so external store libraries that did not use React state or context could support concurrent rendering by forcing a synchronous render when the store is updated.

使用 useSyncExternalStore 是有代价的,因为它会强制从并发功能(如过渡)中退出,并强制现有内容显示 Suspense 回退内容。

🌐 Using useSyncExternalStore comes at a cost though, since it forces a bail out from concurrent features like transitions, and forces existing content to show Suspense fallbacks.

现在 React 19 已经发布,我们正在重新审视这个问题字段,以创建一个原语,以使用 use API 完全支持并发外部存储:

🌐 Now that React 19 has shipped, we’re revisiting this problem space to create a primitive to fully support concurrent external stores with the use API:

const value = use(store);

我们的目标是在渲染期间允许读取外部状态而不会造成撕裂,并且能够与 React 提供的所有并发特性无缝协作。

🌐 Our goal is to allow external state to be read during render without tearing, and to work seamlessly with all of the concurrent features React offers.

这项研究仍处于早期阶段。当我们有进一步进展时,我们会分享更多信息,以及新 API 的样子。

🌐 This research is still early. We’ll share more, and what the new APIs will look like, when we’re further along.


感谢 Aurora ScharffDan AbramovEli WhiteLauren TanLuna WeiMatt CarrollJack PopeJason BontaJordan BrownJordan Eldredge张墨非Sebastien LorberSebastian MarkbågeTim Yung 审阅此文章。

🌐 Thanks to Aurora Scharff, Dan Abramov, Eli White, Lauren Tan, Luna Wei, Matt Carroll, Jack Pope, Jason Bonta, Jordan Brown, Jordan Eldredge, Mofei Zhang, Sebastien Lorber, Sebastian Markbåge, and Tim Yung for reviewing this post.