Чаму React.lazy() патрэбна стандартная экспортна функцыя і як заваносіць функцыі з іменамі
З'ясавайце, што прымае React.lazy() ад дынамічнага імпорту, чаму іменаваныя экспорты гэта наражаюць, як перенаслаляваць іх на стандартныя, а таксама як узгадваюцца Suspense і разбіўка коду.
Крыніцы: React.lazy() Looks Simple Until the Export Is Wrong
React.lazy() — адзін з самых корактных API-яўленняў у React: ён абгортае дынамічны імпорт, выканае рэндарыць рэзультат унутроц Suspense, а бандлер раздзеляе гэты компонент на свой сабскомпонент. Проблема заключаецца ў умове, якая захаваная сінтаксам: імпортаваны модуль павінен выклікваць компонент як свой стандартны экспорт. Як толькі файл будзе выкарыстоўваць імпорт па імені, прыложэнне збяжае з адной з бядзей, якія выкалікаюцца глыбака ўнутроц React і не паказваюць рэальную прычыну. У гэтым артыкуле пояснюецца, якая цэннась на самае праходзіць через lazy(), чаму імпорты па імені яго ламаюць, два спосабы выправлення проблемы і прыкмета дэбаггіну, якая паслужыць і для іншых API-яўленняў.
Звычны вызыв выглядае так:
const Dashboard = lazy(() => import("./Dashboard"));
Нічога ў гэтым не здаецца рызыкаваным, і самэ гэта прычына таго, што бяда є каліткаваюцая, калі яна выйшла.
Успешны варыянт: стандартны экспорт
Пачатакніце з файлу Dashboard.jsx, які выявляе компонент і экспортуе яго як стандартны.
const Dashboard = () => {
return <div>Dashboard</div>;
};
export default Dashboard;
У файле, які выкарыстоўвае компонент, імпортуйце lazy з React і наставьце яго на даны модуль.
import { lazy } from "react";
const Dashboard = lazy(() => import("./Dashboard"));
Зазвычай ніхто не думае пра тое, што вядзецца між гэтымі двумя рядкамі; React проста завантажвае компонент калі ён упершыню атрыбуецца. Цікавая частка — это выраз усередине функцыі-стрэлкі.
import("./Dashboard")
Гэта фундаментальна разліка ад оператывы статычнага імпорту:
import Dashboard from "./Dashboard";
Першы варіант — это дынамічны імпорт, а дынамічны імпорт вяртае Promise. Чаканне яго дапамагае легчэй уявіць результат:
const module = await import("./Dashboard");
Вяртаець не ўтварэнне Dashboard. Це об’ект прастора імен модуля, об’ект, якога атрыбуты ўсунуліся з модуля. Так как файл заканчываецца гэтым рядком:
export default Dashboard;
распрацаваны модуль мае прыблізна такую структуру:
{
default: Dashboard
}
Самэўсёлкава такая структура і є тым, для чаго створана функція React.lazy().
Адна маленькая зміна, якая гэта паспеўшае
Тепер заўважым, што каманда воліе атрыбуты вывозу з іменамі. Файл раней выглядаў так:
const Dashboard = () => {
return <div>Dashboard</div>;
};
export default Dashboard;
Ён перапісываецца так, каб вывозіць компонент па імені:
export const Dashboard = () => {
return <div>Dashboard</div>;
};
Выклік функцыі lazy застаецца абсолюютна тым самым:
const Dashboard = lazy(() => import("./Dashboard"));
Нічога не выглядае некоректна. Компонент існуе, файл існуе, а шлях правільны. Звычайны статычны імпорт таго ж файла працаваў бы без проблем:
import { Dashboard } from "./Dashboard";
Змініўся толькі формат об’екта модуля.
Порэшчанне двух форматаў модуляў
Стандартны экспорт, напісаны так:
export default Dashboard;
стварае модуль, які выглядае прыблізна так:
{
default: Dashboard
}
Экспорт з ім’ям, напісаны так:
export const Dashboard = ...
стварае модуль такога формату:
{
Dashboard: Dashboard
}
React.lazy() чытае компонент з аднаго конкрэтнага месца:
module.default
Ён ніколі не адзіраецца на атрыбут з ім’ям:
module.Dashboard
Таму экспорты з ім’ям самі па сабе не ўскладнены. Проблема заключаецца толькі ў тым, што модуль больш не падходзіць па формату, на які рэгулюецца lazy(). У процесе разработкі React зазвычай паведамляе, што элемент lazy вырашыўся як undefined або што тип элемента недзейсны, што описвае сімптамы, а не відсутнасць ключа default.
Чаму lazy() шукае значэнне default
Дзяржаўка стане ясней, калі паглядзіце на тое, што передаёцца функцыі lazy().
lazy(() => import("./Dashboard"))
У гэтым месцы React не прымеў компонент. Ён прымае функцыю-завантажувач, якая вяртае Promise, які згодна рэшуецца ў модуль. Пасля чаго React берае атрыбут default гэтага модуля і спрыяе яго як компонент. Ланцюг выглядае так:
lazy()
↓
loader function
↓
import("./Dashboard")
↓
Promise
↓
module
↓
module.default
↓
React component
Калі модуль мае элемент default, у React ёсць все неабходнае:
{
default: Dashboard
}
Калі модуль мае толькі іменаваны элемент, у React няма default, які можна было б прыняць:
{
Dashboard: Dashboard
}
Сяродзе тут і выступае несувабодасць. Чытанне default — гэта разумная прымета: гэта ўзнака экспорту, па якой можу сагласавацца кожны модуль, без таго, каб React трэба было ведаць ўсё пра вашу назвуванню.
Адаптация першая: пераканаліце на стандартны экспорт
Якщо компанент являецца галоўным элементам, які надае файл, то простейшы спосаб адладжвання — экспортуваць яго як стандартны.
const Dashboard = () => {
return <div>Dashboard</div>;
};
export default Dashboard;
Пасля гэтага первоначальны вызов працюе як і планавалася:
const Dashboard = lazy(() => import("./Dashboard"));
Для файлаў, якія представляюць адзін галоўны компанент, гэта є чысты і стандартны выбар.
Адаптация другая: заставіце іменаваны экспорт і переназначыце яго
Інодзе змяна экспорту не ёсць можлівасцю або небажана, напрыклад, калі кодавая база вымагае іменаваных экспортаў за дапамогою правіла лінт-перагляду або калі файл экспортуе калькі компанентаў. Іменаваная версія можа застацца такой, якая ёсць:
export const Dashboard = () => {
return <div>Dashboard</div>;
};
Вы все ўсё можаце выкарыстоўваць lazy(); вам проста трэба сказаць, які экспорт трэба спрацоўваць як стандартны. Ладер прыўязывае .then() да дынамічнага імпорту і вяртае новы об’ект, у яком module.Dashboard знаходзіцца пад ключам default.
const Dashboard = lazy(() =>
import("./Dashboard").then((module) => ({
default: module.Dashboard,
}))
);
Спачатку гэта выглядае дзівна, але логіка простая:
Load the module
↓
Get the Dashboard export
↓
Put it under "default"
↓
Give it back to React.lazy()
Іншымі словамі, гэты об’ект:
{
Dashboard: Dashboard
}
ператвараецца ў гэты пры тым, як React яго бачыць:
{
default: Dashboard
}
Тепер React.lazy() прымае самэй той формат, які ён чакае. Якщо проект лязівно завантажвае многа іменаваныя экспорты, цікава ідея — абгуліць гэты патэрн у маленькій спяльной дапаможнай функцыі, каб перэзначэнне знаходзілася ў адным месцы. Таксама трэба розмешчаць кожны вызов lazy() у масштабе модуля, а не ў тэле компонента; яго выявленне пад час рендараў стварае новы лязівны компонент кожны раз і скасоўвае яго стан.
Чаму статычныя імпорты ніколі не сталкнуцца з гэтай проблемай
Пры статычным іменаваным імпорце вы явна указваеце, які экспорт хочаце:
import { Dashboard } from "./Dashboard";
JavaScript сам адначасова вырашвае пытанне прыяжэння Dashboard. Пры стандартным імпорце вы прасіце аб стандартным экспорце:
import Dashboard from "./Dashboard";
А лязівны загрузчык, тым часам, нічога не выбирае:
lazy(() => import("./Dashboard"))
У яго практыкуецца всё об’ект модуля для React, і React павінен выбраць з яго што-небудзь. Самэўсёлкі вывоз мае значэнне толькі у разе адкладання выкарыстоўвання компонента.
Як Suspense адпавядае за практыкуванне
React.lazy() рэдка практыкуецца без Suspense:
<Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</Suspense>
Этая пара становіцца зрозумелай, калі памятаць, што компонент не ўсёга доступны адразу. Браузер можа патрабаваць завантажыць околачны фрагмент JavaScript, прычаму React можа адрасаваць Dashboard, і за гэты час React павінен матыць што-небудзь іншае для адрасавання. Атрыбут fallback стварае гэта. Полная последовальнасць выглядае так:
React needs Dashboard
↓
Dashboard hasn't been loaded
↓
Dynamic import starts
↓
Suspense shows fallback
↓
JavaScript chunk loads
↓
Module resolves
↓
React gets module.default
↓
Dashboard renders
Яксягонаследоўнасць, lazy() не ёсць такім вже таўнамучным: прычына чакання — це Suspense, а default экспорт выкананы ў канцы. Іншы важлівы момент — якщо завантажэнне часткі не выйшла, Promise адхіляецца, і Suspense не справляецца з такым случаем; для перакрыць гэтую ситуацію патрэбны межы для адліквідацыі памылак наваколе компонента lazy.
Раздзелэнне коду — гэта справжняя перадчына
Галоўная прычына викорыстання дынамічных імпортаў — це раздзелэнне коду. Без такога вызову:
lazy(() => import("./Dashboard"))
код панелі керування, верагатна, будзе включаны ў JavaScript, неабходны для запуску аплікацыі. За дапамогою дынамічного імпорту бандлер можа пераказаць яго ў самастоятню частку. Канцэптуальна выходны код дзеліцца на пачатковы бандл:
Initial bundle
├── React
├── App
└── Home
і частку, якая завантажваецца пазней:
Dashboard chunk
└── Dashboard
Код панелі керування завантажаецца толькі тады, калі корыстувач практычна яго патрэбуе. Хорашыя прыклады:
- Панелі керування адміністратараў
- Сторункі аналітыки
- Экраны налашоўкаў
- Большы рэдагары
- Маршруты, якія корыстувачы рэдка адвідуюць
- Тэжкія компаненты, якія выкарыстоўваюць великія залежнасці
Гэта не значыць, што кожны компанент павінен быць „ледзівым“. Кожны разделены код адбывае запит да сеті і стварае стан завантажэння, таму маленькія кнопкі не отрымаюць жадных выгод ад свайго саеўскага фрагмента. Мета не ў тым, каб усё было „ледзівым“; мета — каб пачатковы пакет не мітываў коду, які корыстувач пакуль не патрэбуе.
Адказы з внутранняй часткі React не завжды ўсё ж таки багі React
Калі з внутранняй часткі React выяўляецца асключэнне, бывае спакуса падазроўваць сам React. След стэка можа мітываць такія фрагменты:
lazyInitializer
resolveLazy
beginWork
І законным вывадам ў такі момент яе, што ў самай React апынулася проблема. Перш чым пераходзіць да такога вываду, пераканайцеся, што умовы API, якое вы викорыстоўваете, супараднуюцца з вашымі патрэбамі. Ключовы вопыт тут – што саме React.lazy() чакае, калі ўжо будзе вярнуцца яго загрузчык. У спрощанай форме адпаведзенне здаецца такім:
Promise<{
default: Component
}>
Пораўняйце гэта з тым, што на самай працоўнай единіцы фактычна є. Якщо файл містіць гэта:
export const Dashboard = ...
тады фактычна атрыбутаваная значэнне будзе такім:
{
Dashboard: Dashboard
}
Як толькі паставіць гэта рядом, прычына становіцца ясной. Сама React не зламаная; экспорты працоўнай единіцы і умовы загрузчыка проста не супараднуюцца.
Ментальная модель для розумення вызоваў lazy()
Уместна не чытаць гэты рядок як "загрузка панелі керування асінхронна":
const Dashboard = lazy(() => import("./Dashboard"));
Кращэ спрыяе розуменню, якшо чытаць гэта як невялікую ланцюговую працэсу:
When Dashboard is needed
↓
run the dynamic import
↓
get the module
↓
take its default export
↓
use that as the component
Эта модэль дапамагае быстрасці ў дыягназаванні такіх проблем. Яна таксама поясняе, чырэй першы з наступных файлаў працюе адразу:
// Dashboard.jsx
export default function Dashboard() {
return <div>Dashboard</div>;
}
тады калі другі выклекае дапамогу ў дадатковай працы:
// Dashboard.jsx
export const Dashboard = () => {
return <div>Dashboard</div>;
};
колі яго заваношваюць за дапамогою такога вызову:
lazy(() => import("./Dashboard"));
Шырэйшы урок дэбагавання
Этот маленькі детаўл React несе ў сабе шырэйшы урок. Калі API паводзіцца неспадзевана, трэба адыходзіць ад синтаксісу, які вы напісалі, і абдзерваць значэнне, якое насправды пераводзіцца через API. Вызов, падобны наступнаму, здаецца трывіальным:
lazy(() => import("./Dashboard"))
Пад спадом гэта ланцуг трансфармацыяў:
dynamic import
↓
Promise
↓
module object
↓
default export
↓
React component
Калі гэты ланцуг стае видным, апарэнне памылкі становіцца простым для разумення, і той самы падход ўсунецца ў JavaScript. У замяну таго, каб спытацца, чаму бібліятэка выклеквае памылку, трэба спытацца, што самэўсёлі вы пасылаеце функцыі і што яна чакае пачуць у адпаведзенні. Інодзе весь разлік заключаецца ў адным толькі імені власнасці:
default
Галоўныя выводы
import()вярнэ об’ект модуля, а не компонент, аReact.lazy()чытае компонент з яго власнасціdefault.- Іменаваныя экспорты ўсё гаразд; іх проста трэба переназначыць за дапамою
.then((module) => ({ default: module.Name }))калі яны завантажваюцца з запазджэнням. - Декларуйце компоненты з запазджэнням у межах модуля, загорніце іх у
Suspenseдля праблем стану завантажэння, а таксама дадзіце межу памылак для невялікіх завантажэннях.