Галоўная / Артыкулы

Чаму React.lazy() патрэбна стандартная экспортна функцыя і як заваносіць функцыі з іменамі

З'ясавайце, што прымае React.lazy() ад дынамічнага імпорту, чаму іменаваныя экспорты гэта наражаюць, як перенаслаляваць іх на стандартныя, а таксама як узгадваюцца Suspense і разбіўка коду.

Выдавец: REACTAPP.TOP9 хв чытання2056 слоў

Крыніцы: 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 для праблем стану завантажэння, а таксама дадзіце межу памылак для невялікіх завантажэннях.
  • Разделяйце большыя, рэдкая выкорыстоўваныя экраны, а не маленькія компаненты.
  • Калі адхэленне выйшла ўнутрь бібліятэкі, парабяльце значэнне, яке вы перадаеце, з значэннем, якое чакае контракт API, прычаму не спрыяць думкі пра баг у бібліятэкі.