Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «React.lazy Та suspense — ледачі компоненти в React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**React.lazy** завантажує компонент динамічно, не включаючи його до початкового бандлу. **Suspense** показує fallback під час завантаження. ```jsx const Chart = lazy(() => import('./Chart')); function App() { return ( <Suspense fallback={<p>Loading...</p>}> <Chart /> </Suspense> ); } ``` **Ключове:** lazy + Suspense = code splitting без додаткових налаштувань.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**React.lazy** - функція, яка завантажує React-компонент динамічно: не в початковому бандлі, а тоді, коли компонент вперше рендериться. Разом із **Suspense** вона показує заглушку під час завантаження і робить розділення коду (code splitting) майже без зусиль. ## Теорія ### TL;DR - `React.lazy()` обгортає динамічний `import()` і повертає компонент, який React рендерить звично - `Suspense` перехоплює стан завантаження і показує fallback UI (спінер, скелет, заглушка) - Webpack і Vite автоматично створюють окремі чанки для lazy-модулів - Працює лише з **default-експортами** - для named-експортів потрібен один додатковий крок - Найкраще підходить для сторінок за роутом, важких модалок і всього, що користувач бачить рідко ### Швидкий приклад ```jsx import React, { lazy, Suspense } from 'react'; // Vite/Webpack автоматично виносить це в окремий чанк const Dashboard = lazy(() => import('./pages/Dashboard')); function App() { return ( <Suspense fallback={<p>Loading...</p>}> <Dashboard /> </Suspense> ); } ``` Коли `Dashboard` рендериться вперше, React призупиняється, завантажує чанк, потім продовжує рендеринг. Prop `fallback` - це те, що бачить користувач в цей момент. ### Як відбувається розділення Бандлер бачить `import('./pages/Dashboard')` і створює окремий `.js` файл для цього модуля. `React.lazy` обгортає результат у Promise. При першому рендері компонента React "кидає" цей Promise всередині, Suspense його перехоплює, рендерить fallback і повторно рендерить компонент після того, як Promise виконається. Ти не торкаєшся цієї механіки. API - це `lazy + Suspense`, решта автоматично. ### Коли використовувати - **Розділення на рівні роутів**: кожна сторінка за роутом - хороший кандидат. Користувачі на `/home` не потребують бандла `/settings` одразу при завантаженні. - **Важкі UI-бібліотеки**: текстові редактори, чарти, PDF-в'юери - завантажуй їх лінево. - **Модалки і дровери**: компоненти, що відкриваються по кліку, завантажуються лише тоді, коли потрібні. - **Адмін-розділи**: частини додатку, куди більшість користувачів ніколи не заходить. Не треба лінево завантажувати дрібні компоненти або ті, що відображаються відразу при кожному рендері. Async-оверхед є, навіть якщо він малий. ### Named exports і lazy `React.lazy` очікує, що динамічний імпорт поверне модуль з **default-експортом**. Якщо компонент використовує named-експорт, обгорни так: ```jsx // UserCard використовує named-експорт const UserCard = lazy(() => import('./UserCard').then((mod) => ({ default: mod.UserCard })) ); ``` Це не баг - так спроектовано API. Я бачив, як ця деталь ставить в тупик цілі команди, які вважали, що lazy працює з будь-яким імпортом. ### Типові помилки **Забули Suspense:** ```jsx // Впаде в рантаймі const Chart = lazy(() => import('./Chart')); function Dashboard() { return <Chart />; // Вище немає Suspense - React видасть помилку } ``` Lazy-компоненти потребують Suspense-границі десь вище в дереві. Одна границя покриває кілька lazy-компонентів одночасно. **Оголошення lazy всередині компонента:** ```jsx // Погано: при кожному рендері створюється новий reference function Page() { const Chart = lazy(() => import('./Chart')); // неправильно return <Chart />; } ``` Оголошуй lazy-компоненти на рівні модуля, поза будь-якою функцією. **Нема ErrorBoundary в продакшені:** Мережа падає. Якщо завантаження чанку зафейлиться, React зруйнує весь дерево без `ErrorBoundary`. В продакшені завжди додавай обидва: ```jsx <ErrorBoundary fallback={<p>Не вдалося завантажити.</p>}> <Suspense fallback={<Spinner />}> <LazyPage /> </Suspense> </ErrorBoundary> ``` ### Де зустрічається - React Router-додатки: lazy для кожного роуту, один `Suspense` на рівні роутера - Next.js: використовує `next/dynamic`, який обгортає той самий патерн з додатковими опціями як `ssr: false` - Бібліотеки компонентів: важкі компоненти (DataGrid, RichEditor) шиплять окремими точками входу, щоб споживачі могли їх завантажувати лінево ### Follow-up питання **Q:** Чи може один Suspense обгортати кілька lazy-компонентів? **A:** Так. Suspense показує fallback, якщо хоча б один з його lazy-дітей ще завантажується. Після того як всі завантажились, рендеряться всі одразу. Зручно для розділення на рівні роуту. **Q:** Що станеться, якщо чанк не завантажиться? **A:** React кидає помилку вгору по дереву. Без ErrorBoundary увесь додаток розмонтується. З нею - тільки підтерево границі покаже fallback помилки. **Q:** Чи працює React.lazy з Server Components у React 18+? **A:** Не безпосередньо. На сервері динамічні імпорти працюють інакше. Next.js обробляє це через `next/dynamic` з опцією `{ ssr: false }`. Чистий `React.lazy` - тільки клієнт. **Q:** Яка різниця між React.lazy і next/dynamic? **A:** `next/dynamic` обгортає `React.lazy` і додає опції: `ssr: false` для пропуску серверного рендерингу та вбудований prop `loading` замість Suspense. Та сама концепція, інший шар API. ## Приклади ### Базовий lazy-компонент ```jsx import React, { lazy, Suspense } from 'react'; const HeavyChart = lazy(() => import('./HeavyChart')); function ReportPage() { return ( <div> <h1>Місячний звіт</h1> <Suspense fallback={<div>Завантаження графіка...</div>}> <HeavyChart data={reportData} /> </Suspense> </div> ); } ``` `HeavyChart` і його залежності завантажуються окремим файлом. Користувачі, які ніколи не відкривають `ReportPage`, не скачують цей код. ### Розділення на рівні роутів з React Router ```jsx import { BrowserRouter, Routes, Route } from 'react-router-dom'; import React, { lazy, Suspense } from 'react'; const Home = lazy(() => import('./pages/Home')); const Settings = lazy(() => import('./pages/Settings')); const AdminPanel = lazy(() => import('./pages/AdminPanel')); function App() { return ( <BrowserRouter> <Suspense fallback={<div>Завантаження сторінки...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/settings" element={<Settings />} /> <Route path="/admin" element={<AdminPanel />} /> </Routes> </Suspense> </BrowserRouter> ); } ``` Один `Suspense` на рівні роутера покриває всі сторінки. Кожна завантажується лише при переході. Адмін-бандл не потрапить до звичайного користувача, поки він не зайде на `/admin`.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.