diff --git a/src/components/Layout/Toc.tsx b/src/components/Layout/Toc.tsx index e2e2169fd53..5f9bd6a5ede 100644 --- a/src/components/Layout/Toc.tsx +++ b/src/components/Layout/Toc.tsx @@ -11,6 +11,7 @@ import cx from 'classnames'; import {useTocHighlight} from './useTocHighlight'; +import {IsInTocContext} from '../MDX/TocContext'; import type {Toc} from '../MDX/TocContext'; export function Toc({headings}: {headings: Toc}) { @@ -32,37 +33,39 @@ export function Toc({headings}: {headings: Toc}) { overscrollBehavior: 'contain', }}> diff --git a/src/components/MDX/MDXComponents.tsx b/src/components/MDX/MDXComponents.tsx index 334e72f3482..5dc177a04cd 100644 --- a/src/components/MDX/MDXComponents.tsx +++ b/src/components/MDX/MDXComponents.tsx @@ -36,7 +36,7 @@ import YouWillLearnCard from './YouWillLearnCard'; import {Challenges, Hint, Solution} from './Challenges'; import {IconNavArrow} from '../Icon/IconNavArrow'; import ButtonLink from 'components/ButtonLink'; -import {TocContext} from './TocContext'; +import {TocContext, IsInTocContext} from './TocContext'; import type {Toc, TocItem} from './TocContext'; import {TeamMember} from './TeamMember'; import {LanguagesContext} from './LanguagesContext'; @@ -122,33 +122,57 @@ const RSC = ({children}: {children: React.ReactNode}) => ( {children} ); -const CanaryBadge = ({title}: {title: string}) => ( - - - Canary only - -); +const CanaryBadge = ({title}: {title: string}) => { + const isInToc = useContext(IsInTocContext); + if (isInToc) { + return ( + + ); + } + return ( + + + Canary only + + ); +}; -const ExperimentalBadge = ({title}: {title: string}) => ( - - - Experimental only - -); +const ExperimentalBadge = ({title}: {title: string}) => { + const isInToc = useContext(IsInTocContext); + if (isInToc) { + return ( + + ); + } + return ( + + + Experimental only + + ); +}; const NextMajorBadge = ({title}: {title: string}) => ( ; + return ( + + + + ); } function InlineTocItem({items}: {items: Array}) { diff --git a/src/components/MDX/TocContext.tsx b/src/components/MDX/TocContext.tsx index 924e6e09eed..80489536937 100644 --- a/src/components/MDX/TocContext.tsx +++ b/src/components/MDX/TocContext.tsx @@ -20,3 +20,6 @@ export type TocItem = { export type Toc = Array; export const TocContext = createContext([]); + +// Lets badge components render compactly when inside the table of contents. +export const IsInTocContext = createContext(false); diff --git a/src/content/reference/react/Suspense.md b/src/content/reference/react/Suspense.md index 5dd1acbcde0..1b6cf37ec13 100644 --- a/src/content/reference/react/Suspense.md +++ b/src/content/reference/react/Suspense.md @@ -2500,9 +2500,9 @@ hr { --- -### Animating from Suspense content {/*animating-from-suspense-content*/} +### Animating from Suspense content {/*animating-from-suspense-content*/} - Suspense composes with [``](/reference/react/ViewTransition) to animate the swap from the fallback to the content. Wrap the boundary in a ``, and React treats the swap as an update, cross-fading between the fallback and the content by default: +Suspense composes with [``](/reference/react/ViewTransition) to animate the swap from the fallback to the content. Wrap the boundary in a ``, and React treats the swap as an update, cross-fading between the fallback and the content by default: @@ -2738,9 +2738,9 @@ Where you place the `` relative to the boundary determines wheth --- -### Waiting for a font to load {/*waiting-for-a-font-to-load*/} +### Waiting for a font to load {/*waiting-for-a-font-to-load*/} - When a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React also waits for new fonts the content introduces, up to a timeout, so the text doesn't flash with a fallback font. This only happens during a `` update. +When a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React also waits for new fonts the content introduces, up to a timeout, so the text doesn't flash with a fallback font. This only happens during a `` update. In the example below, the Suspense boundary is wrapped in a ``, and the `Quote` component suspends while its data loads. Rendering the quote starts its font download. React keeps the fallback visible until the font has loaded, so the quote appears already in its font. @@ -2888,11 +2888,11 @@ hr { --- -### Waiting for an image to load {/*waiting-for-an-image-to-load*/} +### Waiting for an image to load {/*waiting-for-an-image-to-load*/} - Images work the same way: when a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits for visible images to load, up to a timeout, so the animation doesn't start with a half-loaded image. This only happens during a `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``. +Images work the same way: when a [``](/reference/react/ViewTransition) animates a Suspense boundary's reveal, React waits for visible images to load, up to a timeout, so the animation doesn't start with a half-loaded image. This only happens during a `` update. Adding an `onLoad` handler opts a specific image out, even inside a ``. -In the example below, the Suspense boundary is wrapped in a `` and shows its fallback until the portrait has loaded. +In the example below, the Suspense boundary is wrapped in a `` and shows a profile skeleton until the portrait has loaded. For comparison, the second button performs the same update with plain DOM, without React. Nothing waits for the image, so the card appears immediately and the image pops in when it loads: @@ -2912,6 +2912,15 @@ function Profile({ src }) { ); } +function ProfilePlaceholder() { + return ( +
+
+

 

+
+ ); +} + export default function App() { const [src, setSrc] = useState(null); return ( @@ -2926,7 +2935,7 @@ export default function App() { {src && ( - ⌛ Loading image...

}> + }>
@@ -2975,15 +2984,244 @@ export function freshImageUrl() { margin-top: 1em; } .card img { + display: block; border-radius: 50%; background: #dfe3e9; } .card p { font-weight: bold; } +.avatar-placeholder { + width: 100px; + height: 100px; + border-radius: 50%; + background: #dfe3e9; +} +.name-placeholder { + width: 90px; + border-radius: 4px; + background: #dfe3e9; +} +hr { + margin: 16px 0; +} +``` + +```json package.json hidden +{ + "dependencies": { + "react": "canary", + "react-dom": "canary", + "react-scripts": "latest" + } +} +``` + + + +--- + +### Coordinating fonts, images, and stylesheets {/*coordinating-fonts-images-and-stylesheets*/} + +All of these waits work together. In the example below, the `ProfileCard` component suspends while its data loads, and renders a stylesheet with `precedence`, text in a new font, and a portrait. React keeps the skeleton visible while the data and the stylesheet load. The `` reveal then waits for the font and the image, so the card appears complete. + +For comparison, the plain DOM version loads the same data and shows every resource arriving on its own schedule: + + + +```js +import { ViewTransition, Suspense, use, useState, startTransition } from 'react'; +import { fetchQuote } from './data.js'; +import { freshStylesheetUrl, freshImageUrl } from './resources.js'; +import VanillaProfileCard from './VanillaProfileCard.js'; + +function ProfileCard({ resources }) { + const quote = use(resources.quotePromise); + return ( + <> + +
+ Jack Pope +
+

Jack Pope

+

{quote}

+
+
+ + ); +} + +function ProfileCardPlaceholder() { + return ( +
+
+
+

 

+

 

+
+
+ ); +} + +export default function App() { + const [resources, setResources] = useState(null); + return ( + <> + + {resources && ( + + }> + + + + )} +
+ + + ); +} +``` + +```js src/VanillaProfileCard.js +import { useRef } from 'react'; +import { fetchQuote } from './data.js'; +import { freshStylesheetUrl, freshImageUrl } from './resources.js'; + +export default function VanillaProfileCard() { + const ref = useRef(null); + async function show() { + const quote = await fetchQuote(); + const doc = ref.current.contentWindow.document; + doc.open(); + doc.write(` + +
+ Jack Pope +
+

Jack Pope

+

${quote}

+
+
+ + `); + doc.close(); + } + return ( + <> + +