diff --git a/beta/src/content/apis/react/index.md b/beta/src/content/apis/react/index.md index 5d462887..db095406 100644 --- a/beta/src/content/apis/react/index.md +++ b/beta/src/content/apis/react/index.md @@ -111,11 +111,11 @@ function TodoList({ todos, tab, theme }) { [See the `useMemo` page for more examples.](/apis/react/useMemo) -Sometimes, you can't skip re-rendering because the screen actually needs to update. In that case, you can improve performance by separating urgent updates that must be synchronous (like typing into an input) from non-urgent updates which don't need to block the user interface (like updating a chart). +Sometimes, you can't skip re-rendering because the screen actually needs to update. In that case, you can improve performance by separating blocking updates that must be synchronous (like typing into an input) from non-blocking updates which don't need to block the user interface (like updating a chart). To prioritize rendering, use one of these Hooks: -- [`useTransition`](/apis/react/useTransition) lets you mark a state transition as non-urgent and allow other updates to interrupt it. +- [`useTransition`](/apis/react/useTransition) lets you mark a state transition as non-blocking and allow other updates to interrupt it. - [`useDeferredValue`](/apis/react/useDeferredValue) lets you defer updating a non-critical part of the UI and let other parts update first. --- diff --git a/beta/src/content/apis/react/useDeferredValue.md b/beta/src/content/apis/react/useDeferredValue.md index 77bd1003..faa44591 100644 --- a/beta/src/content/apis/react/useDeferredValue.md +++ b/beta/src/content/apis/react/useDeferredValue.md @@ -953,7 +953,7 @@ During the initial render, the returned deferred value will be the same as the v - `useDeferredValue` does not by itself prevent extra network requests. -- There is no fixed delay caused by `useDeferredValue` itself. As soon as there are no urgent updates to handle, React will immediately start working on the background re-render with the new deferred value. However, any updates caused by events (like typing) will interrupt the background re-render and get prioritized over it. +- There is no fixed delay caused by `useDeferredValue` itself. As soon as React finishes the original re-render, React will immediately start working on the background re-render with the new deferred value. However, any updates caused by events (like typing) will interrupt the background re-render and get prioritized over it. - The background re-render caused by `useDeferredValue` does not fire Effects until it's committed to the screen. If the background re-render suspends, its Effects will run after the data loads and the UI updates. diff --git a/beta/src/content/apis/react/useState.md b/beta/src/content/apis/react/useState.md index d24063f4..8a590376 100644 --- a/beta/src/content/apis/react/useState.md +++ b/beta/src/content/apis/react/useState.md @@ -31,7 +31,7 @@ function MyComponent() { // ... ``` - The convention is to name state variables like `[something, setSomething]` using [array destructuring.](https://javascript.info/destructuring-assignment) +The convention is to name state variables like `[something, setSomething]` using [array destructuring.](https://javascript.info/destructuring-assignment) `useState` returns an array with exactly two items: diff --git a/beta/src/content/apis/react/useTransition.md b/beta/src/content/apis/react/useTransition.md index 9f90e8fe..b518a468 100644 --- a/beta/src/content/apis/react/useTransition.md +++ b/beta/src/content/apis/react/useTransition.md @@ -2,19 +2,1625 @@ title: useTransition --- - + -This section is incomplete, please see the old docs for [useTransition.](https://reactjs.org/docs/hooks-reference.html#usetransition) +`useTransition` is a React Hook that lets you update the state without blocking the UI. - +```js +const [isPending, startTransition] = useTransition() +``` + - + + +--- + +## Usage {/*usage*/} + +### Marking a state update as a non-blocking transition {/*marking-a-state-update-as-a-non-blocking-transition*/} + +Call `useTransition` at the top level of your component to mark some state updates as non-blocking *transitions*. + +```js [[1, 4, "isPending"], [2, 4, "startTransition"]] +import { useState, useTransition } from 'react'; + +function TabContainer() { + const [isPending, startTransition] = useTransition(); + // ... +} +``` + +`useTransition` returns an array with exactly two items: + +1. The `isPending` flag that tells you whether there is a pending transition. +2. The `startTransition` function that lets you mark a state update as a transition. + +You can then mark a state update as a transition like this: + +```js {6,8} +function TabContainer() { + const [isPending, startTransition] = useTransition(); + const [tab, setTab] = useState('about'); + + function selectTab(nextTab) { + startTransition(() => { + setTab(nextTab); + }); + } + // ... +} +``` + +Transitions let you keep the user interface updates responsive even on slow devices. + +With a transition, your UI stays responsive in the middle of a re-render. For example, if the user clicks a tab but then change their mind and click another tab, they can do that without waiting for the first re-render to finish. + + + +#### Updating the current tab in a transition {/*updating-the-current-tab-in-a-transition*/} + +In this example, the "Posts" tab is **artificially slowed down** so that it takes at least a second to render. + +Click "Posts" and then immediately click "Contact". Notice that this interrupts the slow render of "Posts". The "Contact" tab shows immediately. Because this state update is marked as a transition, a slow re-render did not freeze the user interface. + + ```js -const [isPending, startTransition] = useTransition(); +import { useState, useTransition } from 'react'; +import TabButton from './TabButton.js'; +import AboutTab from './AboutTab.js'; +import PostsTab from './PostsTab.js'; +import ContactTab from './ContactTab.js'; + +export default function TabContainer() { + const [isPending, startTransition] = useTransition(); + const [tab, setTab] = useState('about'); + + function selectTab(nextTab) { + startTransition(() => { + setTab(nextTab); + }); + } + + return ( + <> + selectTab('about')} + > + About + + selectTab('posts')} + > + Posts (slow) + + selectTab('contact')} + > + Contact + +
+ {tab === 'about' && } + {tab === 'posts' && } + {tab === 'contact' && } + + ); +} ``` -
+```js TabButton.js +import { useTransition } from 'react'; - +export default function TabButton({ children, isActive, onClick }) { + if (isActive) { + return {children} + } + return ( + + ) +} + +``` + +```js AboutTab.js +export default function AboutTab() { + return ( +

Welcome to my profile!

+ ); +} +``` + +```js PostsTab.js +import { memo } from 'react'; + +const PostsTab = memo(function PostsTab() { + // Log once. The actual slowdown is inside SlowPost. + console.log('[ARTIFICIALLY SLOW] Rendering 500 '); + + let items = []; + for (let i = 0; i < 500; i++) { + items.push(); + } + return ( +
    + {items} +
+ ); +}); + +function SlowPost({ index }) { + let startTime = performance.now(); + while (performance.now() - startTime < 1) { + // Do nothing for 1 ms per item to emulate extremely slow code + } + + return ( +
  • + Post #{index + 1} +
  • + ); +} + +export default PostsTab; +``` + +```js ContactTab.js +export default function ContactTab() { + return ( + <> +

    + You can find me online here: +

    +
      +
    • admin@mysite.com
    • +
    • +123456789
    • +
    + + ); +} +``` + +```css +button { margin-right: 10px } +b { display: inline-block; margin-right: 10px; } +``` + + + + + +#### Updating the current tab without a transition {/*updating-the-current-tab-without-a-transition*/} + +In this example, the "Posts" tab is also **artificially slowed down** so that it takes at least a second to render. Unlike in the previous example, this state update is **not a transition.** + +Click "Posts" and then immediately click "Contact". Notice that the app freezes while rendering the slowed down tab, and the UI becomes unresponsive. This state update is not a transition, so a slow re-render freezed the user interface. + + + +```js +import { useState } from 'react'; +import TabButton from './TabButton.js'; +import AboutTab from './AboutTab.js'; +import PostsTab from './PostsTab.js'; +import ContactTab from './ContactTab.js'; + +export default function TabContainer() { + const [tab, setTab] = useState('about'); + + function selectTab(nextTab) { + setTab(nextTab); + } + + return ( + <> + selectTab('about')} + > + About + + selectTab('posts')} + > + Posts (slow) + + selectTab('contact')} + > + Contact + +
    + {tab === 'about' && } + {tab === 'posts' && } + {tab === 'contact' && } + + ); +} +``` + +```js TabButton.js +import { useTransition } from 'react'; + +export default function TabButton({ children, isActive, onClick }) { + if (isActive) { + return {children} + } + return ( + + ) +} + +``` + +```js AboutTab.js +export default function AboutTab() { + return ( +

    Welcome to my profile!

    + ); +} +``` + +```js PostsTab.js +import { memo } from 'react'; + +const PostsTab = memo(function PostsTab() { + // Log once. The actual slowdown is inside SlowPost. + console.log('[ARTIFICIALLY SLOW] Rendering 500 '); + + let items = []; + for (let i = 0; i < 500; i++) { + items.push(); + } + return ( +
      + {items} +
    + ); +}); + +function SlowPost({ index }) { + let startTime = performance.now(); + while (performance.now() - startTime < 1) { + // Do nothing for 1 ms per item to emulate extremely slow code + } + + return ( +
  • + Post #{index + 1} +
  • + ); +} + +export default PostsTab; +``` + +```js ContactTab.js +export default function ContactTab() { + return ( + <> +

    + You can find me online here: +

    +
      +
    • admin@mysite.com
    • +
    • +123456789
    • +
    + + ); +} +``` + +```css +button { margin-right: 10px } +b { display: inline-block; margin-right: 10px; } +``` + +
    + + + + + +--- + +### Updating the parent component in a transition {/*updating-the-parent-component-in-a-transition*/} + +The `useTransition` call does not have to be in the same component whose state you're updating. You can also move it into a child component. For example, this `TabButton` component wraps its `onClick` logic in a transition: + +```js {8-10} +export default function TabButton({ children, isActive, onClick }) { + const [isPending, startTransition] = useTransition(); + if (isActive) { + return {children} + } + return ( + + ); +} +``` + +Because the parent component updates its state inside the `onClick` event handler, that state update gets marked as a transition. This is why, like in the earlier example, you can click on "Posts" and then immediately click "Contact". Updating the selected tab is marked as a transition, so it does not block further user interactions. + + + +```js +import { useState } from 'react'; +import TabButton from './TabButton.js'; +import AboutTab from './AboutTab.js'; +import PostsTab from './PostsTab.js'; +import ContactTab from './ContactTab.js'; + +export default function TabContainer() { + const [tab, setTab] = useState('about'); + return ( + <> + setTab('about')} + > + About + + setTab('posts')} + > + Posts (slow) + + setTab('contact')} + > + Contact + +
    + {tab === 'about' && } + {tab === 'posts' && } + {tab === 'contact' && } + + ); +} +``` + +```js TabButton.js active +import { useTransition } from 'react'; + +export default function TabButton({ children, isActive, onClick }) { + const [isPending, startTransition] = useTransition(); + if (isActive) { + return {children} + } + return ( + + ); +} +``` + +```js AboutTab.js +export default function AboutTab() { + return ( +

    Welcome to my profile!

    + ); +} +``` + +```js PostsTab.js +import { memo } from 'react'; + +const PostsTab = memo(function PostsTab() { + // Log once. The actual slowdown is inside SlowPost. + console.log('[ARTIFICIALLY SLOW] Rendering 500 '); + + let items = []; + for (let i = 0; i < 500; i++) { + items.push(); + } + return ( +
      + {items} +
    + ); +}); + +function SlowPost({ index }) { + let startTime = performance.now(); + while (performance.now() - startTime < 1) { + // Do nothing for 1 ms per item to emulate extremely slow code + } + + return ( +
  • + Post #{index + 1} +
  • + ); +} + +export default PostsTab; +``` + +```js ContactTab.js +export default function ContactTab() { + return ( + <> +

    + You can find me online here: +

    +
      +
    • admin@mysite.com
    • +
    • +123456789
    • +
    + + ); +} +``` + +```css +button { margin-right: 10px } +b { display: inline-block; margin-right: 10px; } +``` + +
    + +--- + +### Displaying a pending visual state during the transition {/*displaying-a-pending-visual-state-during-the-transition*/} + +You can use the `isPending` boolean value returned by `useTransition` to indicate to the user that a transition is in progress. For example, the tab button can have a special "pending" visual state: + +```js {4-6} +function TabButton({ children, isActive, onClick }) { + const [isPending, startTransition] = useTransition(); + // ... + if (isPending) { + return {children}; + } + // ... +``` + +Notice how clicking "Posts" now feels more responsive because the tab button itself updates right away: + + + +```js +import { useState } from 'react'; +import TabButton from './TabButton.js'; +import AboutTab from './AboutTab.js'; +import PostsTab from './PostsTab.js'; +import ContactTab from './ContactTab.js'; + +export default function TabContainer() { + const [tab, setTab] = useState('about'); + return ( + <> + setTab('about')} + > + About + + setTab('posts')} + > + Posts (slow) + + setTab('contact')} + > + Contact + +
    + {tab === 'about' && } + {tab === 'posts' && } + {tab === 'contact' && } + + ); +} +``` + +```js TabButton.js active +import { useTransition } from 'react'; + +export default function TabButton({ children, isActive, onClick }) { + const [isPending, startTransition] = useTransition(); + if (isActive) { + return {children} + } + if (isPending) { + return {children}; + } + return ( + + ); +} +``` + +```js AboutTab.js +export default function AboutTab() { + return ( +

    Welcome to my profile!

    + ); +} +``` + +```js PostsTab.js +import { memo } from 'react'; + +const PostsTab = memo(function PostsTab() { + // Log once. The actual slowdown is inside SlowPost. + console.log('[ARTIFICIALLY SLOW] Rendering 500 '); + + let items = []; + for (let i = 0; i < 500; i++) { + items.push(); + } + return ( +
      + {items} +
    + ); +}); + +function SlowPost({ index }) { + let startTime = performance.now(); + while (performance.now() - startTime < 1) { + // Do nothing for 1 ms per item to emulate extremely slow code + } + + return ( +
  • + Post #{index + 1} +
  • + ); +} + +export default PostsTab; +``` + +```js ContactTab.js +export default function ContactTab() { + return ( + <> +

    + You can find me online here: +

    +
      +
    • admin@mysite.com
    • +
    • +123456789
    • +
    + + ); +} +``` + +```css +button { margin-right: 10px } +b { display: inline-block; margin-right: 10px; } +.pending { color: #777; } +``` + +
    + +--- + +### Preventing unwanted loading indicators {/*preventing-unwanted-loading-indicators*/} + +In this example, the `PostsTab` component fetches some data using a [Suspense-enabled](/apis/react/Suspense) data source. When you click the "Posts" tab, the `PostsTab` component *suspends*, causing the closest loading fallback to be displayed: + + + +```js +import { Suspense, useState } from 'react'; +import TabButton from './TabButton.js'; +import AboutTab from './AboutTab.js'; +import PostsTab from './PostsTab.js'; +import ContactTab from './ContactTab.js'; + +export default function TabContainer() { + const [tab, setTab] = useState('about'); + return ( + 🌀 Loading...}> + setTab('about')} + > + About + + setTab('posts')} + > + Posts + + setTab('contact')} + > + Contact + +
    + {tab === 'about' && } + {tab === 'posts' && } + {tab === 'contact' && } +
    + ); +} +``` + +```js TabButton.js +export default function TabButton({ children, isActive, onClick }) { + if (isActive) { + return {children} + } + return ( + + ); +} +``` + +```js AboutTab.js hidden +export default function AboutTab() { + return ( +

    Welcome to my profile!

    + ); +} +``` + +```js PostsTab.js hidden +import { fetchData } from './data.js'; + +// Note: this component is written using an experimental API +// that's not yet available in stable versions of React. + +// For a realistic example you can follow today, try a framework +// that's integrated with Suspense, like Relay or Next.js. + +function PostsTab() { + const posts = use(fetchData('/posts')); + return ( +
      + {posts.map(post => + + )} +
    + ); +} + +function Post({ title }) { + return ( +
  • + {title} +
  • + ); +} + +export default PostsTab; + +// This is a workaround for a bug to get the demo running. +// TODO: replace with real implementation when the bug is fixed. +function use(promise) { + if (promise.status === 'fulfilled') { + return promise.value; + } else if (promise.status === 'rejected') { + throw promise.reason; + } else if (promise.status === 'pending') { + throw promise; + } else { + promise.status = 'pending'; + promise.then( + result => { + promise.status = 'fulfilled'; + promise.value = result; + }, + reason => { + promise.status = 'rejected'; + promise.reason = reason; + }, + ); + throw promise; + } +} +``` + +```js ContactTab.js hidden +export default function ContactTab() { + return ( + <> +

    + You can find me online here: +

    +
      +
    • admin@mysite.com
    • +
    • +123456789
    • +
    + + ); +} +``` + + +```js data.js hidden +// Note: the way you would do data fething depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. + +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); + } + return cache.get(url); +} + +async function getData(url) { + if (url.startsWith('/posts')) { + return await getPosts(); + } else { + throw Error('Not implemented'); + } +} + +async function getPosts() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + let posts = []; + for (let i = 0; i < 500; i++) { + posts.push({ + id: i, + title: 'Post #' + (i + 1) + }); + } + return posts; +} +``` + +```css +button { margin-right: 10px } +b { display: inline-block; margin-right: 10px; } +.pending { color: #777; } +``` + +
    + +Hiding the entire tab container to show a loading indicator leads to a jarring user experience. If you add `useTransition` to `TabButton`, you can instead indicate display the pending state in the tab button instead. + +Notice that clicking "Posts" no longer replaces the entire tab container with a spinner: + + + +```js +import { Suspense, useState } from 'react'; +import TabButton from './TabButton.js'; +import AboutTab from './AboutTab.js'; +import PostsTab from './PostsTab.js'; +import ContactTab from './ContactTab.js'; + +export default function TabContainer() { + const [tab, setTab] = useState('about'); + return ( + 🌀 Loading...}> + setTab('about')} + > + About + + setTab('posts')} + > + Posts + + setTab('contact')} + > + Contact + +
    + {tab === 'about' && } + {tab === 'posts' && } + {tab === 'contact' && } +
    + ); +} +``` + +```js TabButton.js active +import { useTransition } from 'react'; + +export default function TabButton({ children, isActive, onClick }) { + const [isPending, startTransition] = useTransition(); + if (isActive) { + return {children} + } + if (isPending) { + return {children}; + } + return ( + + ); +} +``` + +```js AboutTab.js hidden +export default function AboutTab() { + return ( +

    Welcome to my profile!

    + ); +} +``` + +```js PostsTab.js hidden +import { fetchData } from './data.js'; + +// Note: this component is written using an experimental API +// that's not yet available in stable versions of React. + +// For a realistic example you can follow today, try a framework +// that's integrated with Suspense, like Relay or Next.js. + +function PostsTab() { + const posts = use(fetchData('/posts')); + return ( +
      + {posts.map(post => + + )} +
    + ); +} + +function Post({ title }) { + return ( +
  • + {title} +
  • + ); +} + +export default PostsTab; + +// This is a workaround for a bug to get the demo running. +// TODO: replace with real implementation when the bug is fixed. +function use(promise) { + if (promise.status === 'fulfilled') { + return promise.value; + } else if (promise.status === 'rejected') { + throw promise.reason; + } else if (promise.status === 'pending') { + throw promise; + } else { + promise.status = 'pending'; + promise.then( + result => { + promise.status = 'fulfilled'; + promise.value = result; + }, + reason => { + promise.status = 'rejected'; + promise.reason = reason; + }, + ); + throw promise; + } +} +``` + +```js ContactTab.js hidden +export default function ContactTab() { + return ( + <> +

    + You can find me online here: +

    +
      +
    • admin@mysite.com
    • +
    • +123456789
    • +
    + + ); +} +``` + + +```js data.js hidden +// Note: the way you would do data fething depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. + +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); + } + return cache.get(url); +} + +async function getData(url) { + if (url.startsWith('/posts')) { + return await getPosts(); + } else { + throw Error('Not implemented'); + } +} + +async function getPosts() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + let posts = []; + for (let i = 0; i < 500; i++) { + posts.push({ + id: i, + title: 'Post #' + (i + 1) + }); + } + return posts; +} +``` + +```css +button { margin-right: 10px } +b { display: inline-block; margin-right: 10px; } +.pending { color: #777; } +``` + +
    + +[Read more about using transitions with Suspense.](/apis/react/Suspense#preventing-already-revealed-content-from-hiding) + + + +Transitions will only "wait" long enough to avoid hiding *already revealed* content (like the tab container). For example, if the Posts tab had a [nested `` boundary,](/apis/react/Suspense#revealing-nested-content-as-it-loads) the transition would not "wait" for it. + + + +--- + +### Building a Suspense-enabled router {/*building-a-suspense-enabled-router*/} + +If you're building your own React framework or a router, we recommend to mark page navigations as transitions. + +```js {3,6,8} +function Router() { + const [page, setPage] = useState('/'); + const [isPending, startTransition] = useTransition(); + + function navigate(url) { + startTransition(() => { + setPage(url); + }); + } + // ... +``` + +This is recommended for two reasons: + +- [Transitions are interruptible,](#marking-a-state-update-as-a-non-blocking-transition) which lets the user click away without waiting for the re-render to complete. +- [Transitions prevent unwanted loading indicators,](#preventing-unwanted-loading-indicators) which lets the user avoid jarring jumps on navigation. + +Here is a tiny simplified router example using transitions for navigations. + + + +```json package.json hidden +{ + "dependencies": { + "react": "experimental", + "react-dom": "experimental" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} +``` + +```js App.js +import { Suspense, useState, useTransition } from 'react'; +import IndexPage from './IndexPage.js'; +import ArtistPage from './ArtistPage.js'; +import Layout from './Layout.js'; + +export default function App() { + return ( + }> + + + ); +} + +function Router() { + const [page, setPage] = useState('/'); + const [isPending, startTransition] = useTransition(); + + function navigate(url) { + startTransition(() => { + setPage(url); + }); + } + + let content; + if (page === '/') { + content = ( + + ); + } else if (page === '/the-beatles') { + content = ( + + ); + } + return ( + + {content} + + ); +} + +function BigSpinner() { + return

    🌀 Loading...

    ; +} +``` + +```js Layout.js +export default function Layout({ children, isPending }) { + return ( +
    +
    + Music Browser +
    +
    + {children} +
    +
    + ); +} +``` + +```js IndexPage.js +export default function IndexPage({ navigate }) { + return ( + + ); +} +``` + +```js ArtistPage.js +import { Suspense } from 'react'; +import Albums from './Albums.js'; +import Biography from './Biography.js'; +import Panel from './Panel.js'; + +export default function ArtistPage({ artist }) { + return ( + <> +

    {artist.name}

    + + }> + + + + + + ); +} + +function AlbumsGlimmer() { + return ( +
    +
    +
    +
    +
    + ); +} +``` + +```js Albums.js hidden +import { fetchData } from './data.js'; + +// Note: this component is written using an experimental API +// that's not yet available in stable versions of React. + +// For a realistic example you can follow today, try a framework +// that's integrated with Suspense, like Relay or Next.js. + +export default function Albums({ artistId }) { + const albums = use(fetchData(`/${artistId}/albums`)); + return ( +
      + {albums.map(album => ( +
    • + {album.title} ({album.year}) +
    • + ))} +
    + ); +} + +// This is a workaround for a bug to get the demo running. +// TODO: replace with real implementation when the bug is fixed. +function use(promise) { + if (promise.status === 'fulfilled') { + return promise.value; + } else if (promise.status === 'rejected') { + throw promise.reason; + } else if (promise.status === 'pending') { + throw promise; + } else { + promise.status = 'pending'; + promise.then( + result => { + promise.status = 'fulfilled'; + promise.value = result; + }, + reason => { + promise.status = 'rejected'; + promise.reason = reason; + }, + ); + throw promise; + } +} +``` + +```js Biography.js hidden +import { fetchData } from './data.js'; + +// Note: this component is written using an experimental API +// that's not yet available in stable versions of React. + +// For a realistic example you can follow today, try a framework +// that's integrated with Suspense, like Relay or Next.js. + +export default function Biography({ artistId }) { + const bio = use(fetchData(`/${artistId}/bio`)); + return ( +
    +

    {bio}

    +
    + ); +} + +// This is a workaround for a bug to get the demo running. +// TODO: replace with real implementation when the bug is fixed. +function use(promise) { + if (promise.status === 'fulfilled') { + return promise.value; + } else if (promise.status === 'rejected') { + throw promise.reason; + } else if (promise.status === 'pending') { + throw promise; + } else { + promise.status = 'pending'; + promise.then( + result => { + promise.status = 'fulfilled'; + promise.value = result; + }, + reason => { + promise.status = 'rejected'; + promise.reason = reason; + }, + ); + throw promise; + } +} +``` + +```js Panel.js hidden +export default function Panel({ children }) { + return ( +
    + {children} +
    + ); +} +``` + +```js data.js hidden +// Note: the way you would do data fething depends on +// the framework that you use together with Suspense. +// Normally, the caching logic would be inside a framework. + +let cache = new Map(); + +export function fetchData(url) { + if (!cache.has(url)) { + cache.set(url, getData(url)); + } + return cache.get(url); +} + +async function getData(url) { + if (url === '/the-beatles/albums') { + return await getAlbums(); + } else if (url === '/the-beatles/bio') { + return await getBio(); + } else { + throw Error('Not implemented'); + } +} + +async function getBio() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 500); + }); + + return `The Beatles were an English rock band, + formed in Liverpool in 1960, that comprised + John Lennon, Paul McCartney, George Harrison + and Ringo Starr.`; +} + +async function getAlbums() { + // Add a fake delay to make waiting noticeable. + await new Promise(resolve => { + setTimeout(resolve, 3000); + }); + + return [{ + id: 13, + title: 'Let It Be', + year: 1970 + }, { + id: 12, + title: 'Abbey Road', + year: 1969 + }, { + id: 11, + title: 'Yellow Submarine', + year: 1969 + }, { + id: 10, + title: 'The Beatles', + year: 1968 + }, { + id: 9, + title: 'Magical Mystery Tour', + year: 1967 + }, { + id: 8, + title: 'Sgt. Pepper\'s Lonely Hearts Club Band', + year: 1967 + }, { + id: 7, + title: 'Revolver', + year: 1966 + }, { + id: 6, + title: 'Rubber Soul', + year: 1965 + }, { + id: 5, + title: 'Help!', + year: 1965 + }, { + id: 4, + title: 'Beatles For Sale', + year: 1964 + }, { + id: 3, + title: 'A Hard Day\'s Night', + year: 1964 + }, { + id: 2, + title: 'With The Beatles', + year: 1963 + }, { + id: 1, + title: 'Please Please Me', + year: 1963 + }]; +} +``` + +```css +main { + min-height: 200px; + padding: 10px; +} + +.layout { + border: 1px solid black; +} + +.header { + background: #222; + padding: 10px; + text-align: center; + color: white; +} + +.bio { font-style: italic; } + +.panel { + border: 1px solid #aaa; + border-radius: 6px; + margin-top: 20px; + padding: 10px; +} + +.glimmer-panel { + border: 1px dashed #aaa; + background: linear-gradient(90deg, rgba(221,221,221,1) 0%, rgba(255,255,255,1) 100%); + border-radius: 6px; + margin-top: 20px; + padding: 10px; +} + +.glimmer-line { + display: block; + width: 60%; + height: 20px; + margin: 10px; + border-radius: 4px; + background: #f0f0f0; +} +``` + + + + + +[Suspense-enabled](/apis/react/Suspense) routers are expected to wrap the navigation updates into transitions by default. + + + +--- + +## Reference {/*reference*/} + +### `useTransition()` {/*usetransition*/} + +Call `useTransition` at the top level of your component to mark some state updates as transitions. + +```js +import { useTransition } from 'react'; + +function TabContainer() { + const [isPending, startTransition] = useTransition(); + // ... +} +``` + +[See more examples above.](/#usage) + +#### Parameters {/*parameters*/} + +`useTransition` does not take any parameters. + +#### Returns {/*returns*/} + +`useTransition` returns an array with exactly two items: + +1. The `isPending` flag that tells you whether there is a pending transition. +2. The [`startTransition` function](#starttransition) that lets you mark a state update as a transition. + +--- + +### `startTransition` function {/*starttransition*/} + +The `startTransition` function returned by `useTransition` lets you mark a state update as a transition. + +```js {6,8} +function TabContainer() { + const [isPending, startTransition] = useTransition(); + const [tab, setTab] = useState('about'); + + function selectTab(nextTab) { + startTransition(() => { + setTab(nextTab); + }); + } + // ... +} +``` + +#### Parameters {/*starttransition-parameters*/} + +* `fn`: A function that updates some state by calling one or more [`set` functions.](/apis/react/useState#setstate) React will immediately call `fn` with no parameters and keep track of which state updates have been scheduled. It will treat all of the updates scheduled by `fn` as transitions: they will be [non-blocking](#marking-a-state-update-as-a-non-blocking-transition) and [will not display unwanted loading indicators.](#preventing-unwanted-loading-indicators) + +#### Returns {/*starttransition-returns*/} + +`startTransition` does not return anything. + +#### Caveats {/*starttransition-caveats*/} + +* `useTransition` is a Hook, so it can only be called inside components or custom Hooks. If you need to start a transition somewhere else (for example, from a data library), call the standalone [`startTransition`](/apis/react/startTransition) instead. + +* You can wrap an update into a transition only if you have access to the `set` function of that state. If you want to start a transition in response to some prop or a custom Hook return value, try [`useDeferredValue`](/apis/react/usedeferredvalue) instead. + +* The function you pass to `startTransition` must be synchronous. React immediately executes this function, marking all state updates that happen while it executes as transitions. If you try to perform more state updates later (for example, in a timeout), they won't be marked as transitions. + +* A state update marked as a transition will be interrupted by other state updates. For example, if you update a chart component inside a transition, but then start typing into an input while the chart is in the middle of a re-render, React will restart the rendering work on the chart component after handling the input state update. + +* Transition updates can't be used to control text inputs. + +* If there are multiple ongoing transitions, React currently batches them together. This is a limitation that will likely be removed in a future release. + +--- + +## Troubleshooting {/*troubleshooting*/} + +### Updating an input in a transition doesn't work {/*updating-an-input-in-a-transition-doesnt-work*/} + +You can't use a transition for a state variable that controls an input: + +```js {4,10} +const [text, setText] = useState(''); +// ... +function handleChange(e) { + // ❌ Can't use transitions for controlled input state + startTransition(() => { + setText(e.target.value); + }); +} +// ... +return ; +``` + +This is because transitions are non-blocking, but updating an input in response to the change event should happen synchronously. If you want to run a transition in response to typing, you have two options: + +1. You can declare two separate state variables: one for the input state (which always updates synchronously), and one that you will update in a transition. This lets you control the input using the synchronous state, and pass the transition state variable (which will "lag behind" the input) to the rest of your rendering logic. +2. Alternatively, you can have one state variable, and add [`useDeferredValue`](/apis/react/useDeferredValue) which will "lag behind" the real value. It will trigger non-blocking re-renders to "catch up" with the new value automatically. + +--- + +### React doesn't treat my state update as a transition {/*react-doesnt-treat-my-state-update-as-a-transition*/} + +When you wrap a state update in a transition, make sure that it happens *during* the `startTransition` call: + +```js +startTransition(() => { + // ✅ Setting state *during* startTransition call + setPage('/about'); +}); +``` + +The function you pass to `startTransition` must be synchronous. + +You can't mark an update as a transition like this: + +```js +startTransition(() => { + // ❌ Setting state *after* startTransition call + setTimeout(() => { + setPage('/about'); + }, 1000); +}); +``` + +Instead, you could do this: + +```js +setTimeout(() => { + startTransition(() => { + // ✅ Setting state *during* startTransition call + setPage('/about'); + }); +}, 1000); +``` + +Similarly, you can't mark an update as a transition like this: + +```js +startTransition(async () => { + await someAsyncFunction(); + // ❌ Setting state *after* startTransition call + setPage('/about'); +}); +``` + +However, this works instead: + +```js +await someAsyncFunction(); +startTransition(() => { + // ✅ Setting state *during* startTransition call + setPage('/about'); +}); +``` + +--- + +### I want to call `useTransition` from outside a component {/*i-want-to-call-usetransition-from-outside-a-component*/} + +You can't call `useTransition` outside a component because it's a Hook. In this case, use the standalone [`startTransition`](/apis/react/startTransition) method instead. It works the same way, but it doesn't provide the `isPending` indicator. + +--- + +### The function I pass to `startTransition` executes immediately {/*the-function-i-pass-to-starttransition-executes-immediately*/} + +If you run this code, it will print 1, 2, 3: + +```js {1,3,6} +console.log(1); +startTransition(() => { + console.log(2); + setPage('/about'); +}); +console.log(3); +``` + +**It is expected to print 1, 2, 3.** The function you pass to `startTransition` does not get delayed. Unlike with the browser `setTimeout`, it does not run the callback later. React executes your function immediately, but any state updates scheduled *while it is running* will get marked as transitions. You can imagine that it works like this: + +```js +// A simplified version of how React works + +let isInsideTransition = false; + +function startTransition(fn) { + isInsideTransition = true; + fn(); + isInsideTransition = false; +} + +function setState() { + if (isInsideTransition) { + // ... schedule a transition state update ... + } else { + // ... schedule an urgent state update ... + } +} +``` diff --git a/beta/src/sidebarAPIs.json b/beta/src/sidebarAPIs.json index 972b5814..788e1f1d 100644 --- a/beta/src/sidebarAPIs.json +++ b/beta/src/sidebarAPIs.json @@ -72,8 +72,7 @@ }, { "title": "useTransition", - "path": "/apis/react/useTransition", - "wip": true + "path": "/apis/react/useTransition" } ] },