-
-
+ {details.title}
+{details.description}
+ > + ); +} +``` + +```js src/Home.js hidden +import { useId, useState, use, useDeferredValue, unstable_ViewTransition as ViewTransition } from "react";import { Video } from "./Videos";import Layout from "./Layout";import { fetchVideos } from "./data";import { IconSearch } from "./Icons"; + +function SearchList({searchText, videos}) { + // Активировать с помощью useDeferredValue ("when") + const deferredSearchText = useDeferredValue(searchText); + const filteredVideos = filterVideos(videos, deferredSearchText); + return ( +{details.title}
+{details.description}
+ > + ); +} +``` + +```js src/Home.js hidden +import { useId, useState, use, useDeferredValue, unstable_ViewTransition as 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 ( +
+ {details.title}
-{details.description}
- > - ); -} - -function VideoInfoFallback() { - return ( - <> - - - > - ); -} - -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); - - return ( -{details.title}
-{details.description}
- > - ); -} - -function VideoInfoFallback() { - return ( - <> - - - > - ); -} - -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); - - return ( -{details.title}
-{details.description}
- > - ); -} - -function VideoInfoFallback() { - return ( - <> - - - > - ); -} - -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); - - return ( -{details.title}
-{details.description}
- > - ); -} - -function VideoInfoFallback() { - return ( - <> - - - > - ); -} - -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); - - return ( -{details.title}
-{details.description}
-{details.title}
-{details.description}
- > - ); -} - -function VideoInfoFallback() { - return ( - <> - - - > - ); -} - -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); - - return ( -{details.title}
-{details.description}
- > - ); -} -``` - -```js src/Home.js -import { useId, useState, use, useDeferredValue, unstable_ViewTransition as 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 ( -{details.title}
-{details.description}
- > - ); -} - -function VideoInfoFallback() { - return ( - <> - - - > - ); -} - -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); - - return ( -{video.title}
-{details.title}
-{details.description}
- > - ); -} - -function VideoInfoFallback() { - return ( - <> - - - > - ); -} - -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); - - return ( -{details.title}
-{details.description}
- > - ); -} -``` - -```js src/Home.js hidden -import { useId, useState, use, useDeferredValue, unstable_ViewTransition as 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 ( -{details.title}
+{details.description}
+ > ); } function VideoInfoFallback() { return ( -{details.title}
-{details.description}
-{details.title}
-{details.description}
- > - ); -} ``` ```js src/Home.js hidden @@ -14658,7 +3834,7 @@ function SearchInput({ value, onChange }) { return ({details.title}
-{details.description}
- > - ); + +```js src/data.js hidden +const videos = [ + { + id: '1', + title: 'Первое видео', + description: 'Описание видео', + image: 'blue', + }, + { + id: '2', + title: 'Второе видео', + description: 'Описание видео', + image: 'red', + }, + { + id: '3', + title: 'Третье видео', + description: 'Описание видео', + image: 'green', + }, + { + id: '4', + title: 'Четвертое видео', + description: 'Описание видео', + image: 'purple', + }, + { + id: '5', + title: 'Пятое видео', + description: 'Описание видео', + image: 'yellow', + }, + { + id: '6', + title: 'Шестое видео', + description: 'Описание видео', + image: 'gray', + }, +]; + +let videosCache = new Map(); +let videoCache = new Map(); +let videoDetailsCache = new Map(); +const VIDEO_DELAY = 1; +const VIDEO_DETAILS_DELAY = 1000; +export function fetchVideos() { + if (videosCache.has(0)) { + return videosCache.get(0); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos); + }, VIDEO_DELAY); + }); + videosCache.set(0, promise); + return promise; +} + +export function fetchVideo(id) { + if (videoCache.has(id)) { + return videoCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DELAY); + }); + videoCache.set(id, promise); + return promise; +} + +export function fetchVideoDetails(id) { + if (videoDetailsCache.has(id)) { + return videoDetailsCache.get(id); + } + const promise = new Promise((resolve) => { + setTimeout(() => { + resolve(videos.find((video) => video.id === id)); + }, VIDEO_DETAILS_DELAY); + }); + videoDetailsCache.set(id, promise); + return promise; } ``` -```js src/Home.js -import { useId, useState, use, useDeferredValue, unstable_ViewTransition as ViewTransition } from "react";import { Video } from "./Videos";import Layout from "./Layout";import { fetchVideos } from "./data";import { IconSearch } from "./Icons"; +```js src/router.js hidden +import {useState, createContext, use, useTransition, useLayoutEffect, useEffect, unstable_addTransitionType as addTransitionType} from "react"; + +export function Router({ children }) { + const [isPending, startTransition] = useTransition(); + const [routerState, setRouterState] = useState({pendingNav: () => {}, url: document.location.pathname}); + function navigate(url) { + startTransition(() => { + // Тип перехода для причины "переход вперед" + addTransitionType('nav-forward'); + go(url); + }); + } + function navigateBack(url) { + startTransition(() => { + // Тип перехода для причины "переход назад" + addTransitionType('nav-back'); + go(url); + }); + } + + function go(url) { + setRouterState({ + url, + pendingNav() { + window.history.pushState({}, "", url); + }, + }); + } + + useEffect(() => { + function handlePopState() { + // Это не должно анимироваться, так как восстановление должно быть синхронным. + // Даже если это переход. + startTransition(() => { + setRouterState({ + url: document.location.pathname + document.location.search, + pendingNav() { + // Нет действия. URL уже обновлен. + }, + }); + }); + } + window.addEventListener("popstate", handlePopState); + return () => { + window.removeEventListener("popstate", handlePopState); + }; + }, []); + const pendingNav = routerState.pendingNav; + useLayoutEffect(() => { + pendingNav(); + }, [pendingNav]); -function SearchList({searchText, videos}) { - // Activate with useDeferredValue ("when") - const deferredSearchText = useDeferredValue(searchText); - const filteredVideos = filterVideos(videos, deferredSearchText); return ( -{details.title}
+{details.description}
+ > + ); +} + +function VideoInfoFallback() { + return ( + <> + + + > + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( +{details.title}
+{details.description}
+ > + ); +} + +function VideoInfoFallback() { + return ( + <> + + + > + ); +} + +export default function Details() { + const { url, navigateBack } = useRouter(); + const videoId = url.split("/").pop(); + const video = use(fetchVideo(videoId)); + + return ( +{details.title}
-{details.description}
- > - ); + background-color: #ffffff; + background-size: cover; + user-select: none; } -function VideoInfoFallback() { - return ( - <> - - - > - ); +.video-details-title { + margin-top: 8px; } -export default function Details() { - const { url, navigateBack } = useRouter(); - const videoId = url.split("/").pop(); - const video = use(fetchVideo(videoId)); +.video-details-speaker { + display: flex; + gap: 8px; + margin-top: 10px +} - return ( -{details.title}
-{details.description}
- > +{details.title}
+{details.description}
+{details.title}
-{details.description}
-{details.title}
+{details.description}
+ > + ); } +``` +```js src/Home.js hidden +import { Video } from "./Videos"; +import Layout from "./Layout"; +import { fetchVideos } from "./data"; +import { useId, useState, use } from "react"; +import { IconSearch } from "./Icons"; -```css {1, 6} -::view-transition-old(.slide-down) { - /* Сдвинуть резервный вариант вниз */ - animation: ...; +function SearchInput({ value, onChange }) { + const id = useId(); + return ( +