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

З JSX да простых об’ектаў: як функцыонуюць элементы, складовыя і параметры React

Пазіце, да ў чым заканчваецца кампіляванне JSX, чаму компоненты ў сутнасі яўляюцяся функціямі, і як з гэтай адной ментальнай моделі выкалываюцься пропсы, дзецявыя элементы, стандартныя значэння, распрасцваранне і слоты.

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

Крыніцы: JSX, Components & Props — The Mental Model Every React Developer Actually Needs

Многі багі ў React выходзяць з нечыстай картынкі таго, чым на самае працэ ёсць JSX, компаненты і пропсы. Калі вы зрозумеете, што JSX ёстся вызыв функцыі, што гэты вызыв стварае звычайны об’ект, а компанент — гэта функцыя, якая прымеўа адзін об’ект аргументаў, правілы, якія спачатку здаваліся дэтэрміністычнымі, пачынаюць мякча. У гэтым артыкуле гэтае модэлюванне ствараецца, выходячы з рэзультатаў компілятора, а пасля яго прыменяюць да пропсаў, children, значэнняў за замовчаннем, механізмаў распрасцёрування, незменнасці і шаблонаў складання, якія вы будете вжываць ўсё час.

JSX — гэта вызывы функцый, а не HTML

JSX часта называюць „HTML усередине JavaScript“, але гэта непачаткова. Точней кажучы, JSX — гэта синтаксічны „цукор“, які компілятор ператварае на звычайныя вызывы функцый.

Возьмім просты загалоўак:

const element = <h1 className="title">Hello World</h1>;

Кампайляры, такія як Babel, ператвараюць іх у вызов React.createElement, пры яком пасуюць назва тагу, об’ект з атрыбутамі і контэнт:

const element = React.createElement("h1", { className: "title" }, "Hello World");

Это класычны спосаб ператварэння. За часів React 17 большасць інструментальных комплексаў выкарыстоўвае автаматычны рантайм JSX, які заместа таго выдае вызывы функцыі jsx(), імпортаванай з react/jsx-runtime; таму ў межах коду больш не патрэбны элементы React. Ідея застаецца тая ж: маркап становіцься вызамом функцыі.

Этот вызов вяртае звычны об’ект JavaScript, які называецца элементам React:

{
  type: "h1",
  props: {
    className: "title",
    children: "Hello World"
  }
}

Элемент React — гэта простае апісанне таго, што павінна быць праказана на экране. React праходзіць па дрэву такіх об’ектаў і вырашае, як прыблізіць рэальны DOM да ўсёга, што апісана; гэта дрэва апісанняў і є тым, што люди зазвычай маюць на увазе пад Вяртуальным DOM.

Што пояснюе модэль

Зберагчы ў памяці тэзу «JSX стае вызывам функцыі, якая вяртае об’ект», можна расплыць кальколька распашчых пытанняў:

  • Няма оператаў if унутры JSX. Нельга размішчаць оператыв унутры аргумента функцыі, таму вы вжываеце трынарны выраз або оператар &&, альбо пераносіце логіку вышэй за return.
  • className заместо class. JSX — это JavaScript, а class там ўжо ёсць зарезерваваное слова.
  • Адны корневы элемент. Функцыя вяртае аднае значэнне, таму компонент таксама должен вяртаць адны элемент (які можа мець багато дзецячых элементаў).

Компонент — это функцыя, якая вяртае элементы

Компоненты — это функцыі, якіх значэнне вяртання ёсць элемент React або null, калі трэба ніч не адрасаваць.

function Greeting() {
  return <h1>Hello, Devendra!</h1>;
}

Няма жадных скрытых механізмоў, і ў сучасным React няма патрэбы ў класах. React вызывае функцію кожны раз, калі яму трэба знать, што адрасаваць. Калі даныя зменяюцца, React зноў яе вызывае, прыме наступны дрэва элементаў, порашчыць яго з пярэднім і апডейтуе толькі тыя часткі DOM, якія разніцяюцца. Гэтае порашчэнне называецца супраавязкай.

Два правілы, якіх павінен следаваць кожны компонент

Імены павінны начынацца з вялікай літеры

JSX выкарыстоўвае правілы напісання, каб вярнуцца, што значыць той чы іншы тэг. Тэг, напісаны з вялікай літеры, апісвае компонент, таму React вызывае вашу функцію:

// ✅ React treats this as a component
<Greeting />

Тэг, напісаны малай літерой, спрацоўвае як вбудованы элемент DOM, таму React паспрабуе стварыць літэральны элемент <greeting> у замест на вызыв чаго-небудзь іншага:

// ❌ React treats this as a native DOM element (like <div>)
<greeting />

Павінен быць вярнуты роўна адны коран

Прагненне вывести два сяброўскіе элементы па боку аднаго другога не вяршыцца, таму што гэта адпавядае выдачы двух значэнняў з адной функцыі:

// ❌ This will throw an error
function Card() {
  return (
    <h2>Title</h2>
    <p>Description</p>
  );
}

Є два спосабы рашэння. Загуранцаваць сяброўскіе элементы ў абмежваючы элемент альбо выкарыстаць Fragment, напісаны <>...</>, які групавае іх без дадання дапаможнага вузла ў DOM. Нижэйшы фрагмент паказвае оба варыянты адразу.

// ✅ Wrap in a parent
function Card() {
  return (
    <div>
      <h2>Title</h2>
      <p>Description</p>
    </div>
  );
}// ✅ Or use a Fragment (renders nothing extra in the DOM)
function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

Вядзьміце за прыклад Fragment, як толькі загуранне не патрэбна для стылювання або макетавання.

Props — это аргументы функцыі

Якщо компоненты ўособліваюцься функцыямі, то props — это ўсе іх аргументы. Хутчэй, компонент чытае значэння name з об’екта, які ён прымеў:

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

І хутчэй ён атрыбуецца гэтым значэнням і выводзіцца на экран:

// Usage
<Greeting name="Devendra" />

React сабіра ўсе атрыбуты, запісаныя на тагу JSX, у адны об’ект props і перадае яго як першы аргумент. Гэта і є весь механізм.

Распаковка ў сигнатуре

Павтарэнне props. быстра створяе хаос. Распаковка об’екта ў спісе параметраў дае назву кожнаму атрыбуту, які вы викорыстоўваете, і дазволяе за мінуту пабачыць вхідныя даны компонента:

function Greeting({ name, role }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Месца вызову застаецца незменным:

<Greeting name="Devendra" role="Senior Engineer" />

Стандартныя значэння з распаковкай

Калі атрыбут є необав’язковым, задайце яму стандартнае значэння безпосередна ў шаблоне распаковкі. Якщо role не заданы, будзе вжывацца "Developer":

function Greeting({ name, role = "Developer" }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Такі спосаб відобразыць стандартную ролю:

// role will be "Developer" if not provided
<Greeting name="Devendra" />

Дэструктурызацыя значэнняў па значэнню прыменяецца толькі калі значэнне ў стане undefined. Актыўнае выкарыстоўванне null абходзіць гэтае значэння, што варта памяцаць, калі прызначэнні выйшлі з дадзеных API.

children: складанне, ўбудованае ў сынтаксіс

Контэнт, які знаходзится межа ўвядзенням і закрыццем тага компонента, апошнюе автаматычна як прызначэнне children. Компонент Card можа проста адобразіць усё, што яму даўця ўнутры його обгорака:

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

Той, хто вызывае компонент, выбірае, што будзе ўнутры:

// Usage
<Card>
  <h2>My Title</h2>
  <p>Some description here.</p>
</Card>

Складанне через children — гэта спосаб стварэння гнучкіх, можна перызначаць компонентаў. Уместо таго, каб прыпускать кожную варіяцыю за дапамогою іншага прызначэння, компонент забезпечвае структуру, а родны компонент — контэнт. Бібліятекі такія як shadcn/ui і Material UI ствараюць дыялогі, карткі і кнопкі такім спосабам.

Будзь-яка значэнне JavaScript можа быць прыёмнікам

Закругленыя скобкі ў JSX означаюць «асэціераваць гэта як JavaScript», таму прыёмнік можа хаваць будзь-яе значэнне: строкі, числы, логічныя значэння, масівы, об’екты чы рашчыткі. Гэты компонент прымеяе калькі разных типаў адразу: умовна атрыбутуе значэння, ператварае масіў у элементы спісу і падключае функцыю-вызывач да кантакту:

function UserCard({ name, age, isAdmin, hobbies, onDelete }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <ul>
        {hobbies.map(h => <li key={h}>{h}</li>)}
      </ul>
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}

Месца вызыву паказвае, як кожны тип перадаецца:

<UserCard
  name="Devendra"
  age={28}
  isAdmin={true}
  hobbies={["coding", "teaching", "gaming"]}
  onDelete={() => console.log("deleted")}
/>

Трыбуны, на якія трэба зважыць:

  • Звычныя строковыя значэння можна запісаць безпосередзе ў лапкі.
  • Усё інша, уключаючы числы, логічныя значэння, масівы, об’екты чы рашчыткі, пакладаецца ў закругленыя скобкі.
  • Функцыя-прап, такая як onDelete, — гэта спосаб, яким дзецявы элементы паведамляють подзяйскія змены своему абяцелю. Самэўсёлкаваеся шаблона калбэка ёсць тым, што дазволяе выконваць пераказ стану: стан знаходзіцца ў спяльнам абяцелю, а дзецявы элементы зменяюць яго толькі праз вызыв функцый, якія абяцель прысылае.
  • Прапы ў режыме толькі для чытання

    Ніколі не мутаваць прапы. Змена значэння якога-небудзь поля об’екта прапа таямна зменяе даны, якія належаць абяцелю:

    // ❌ Never do this
    function Greeting({ user }) {
      user.name = "Modified"; // This will cause silent bugs
      return <h1>{user.name}</h1>;
    }
    

    Замест таго вырашыць новае значэнне і залишыць первісна недастрыжаным:

    // ✅ Create a new value instead
    function Greeting({ user }) {
      const displayName = user.name.toUpperCase();
      return <h1>{displayName}</h1>;
    }
    

    Данныя трапляюцца толькі ў аднам напрамку, з абяцелю на дзецявы элемент, і гэта якраз робіць дапраўкі React прыгаднымі. Дзецявы элемент, якому трэба, каб абяцель чагось зменіў, вызывае функцыю, яку абяцель запроставіў.

    Перадача прапов за дапамогою rest і spread

    Калі компанент абгруэтуе элемент DOM, частаючы хочацца прыймаць многа атрыбуты без неабяжлівага перыяўлення кожнага з іх. Синтаксіс «Rest» у параметры выделяе тыя атрыбуты, якімі сам компанент керуе, напрыклад label, а рэштку сабральвае ў inputProps, які пасля таго перадаеэлементу input:

    function Input({ label, ...inputProps }) {
      return (
        <div>
          <label>{label}</label>
          <input {...inputProps} />
        </div>
      );
    }
    

    Кожны дапаможніцкі атрыбут, такі як type, placeholder чы onChange, пераходзіць працоўна да базовага элемента:

    // All extra props (type, placeholder, onChange, etc.) flow straight to <input>
    <Input
      label="Email"
      type="email"
      placeholder="you@example.com"
      onChange={handleChange}
    />
    

    Бібліятэкі постаўляюць наступны патэрн часта, але размешчанне даказальных налаштаванняў на элементе DOM можа прынесці значэнні, якія ён не розумее, чым вызываюцца адзвешчанні пра неканальныя атрыбуты. Будзьце рашучы ў тым, калі і дзе вы яны размешчаеце. У прыкладзе таксама атрыбут label прадстаўляецца без з’яёднання яго з элементам вводу; у рэальным кодзе ўжо павінны быць з’яёднаны за дапамогою htmlFor і id, каб атрыбут label быў доступны.

    Тры рэвэлі вымовкі компаненту

    Компаненты зазвычай развіваюцца па адзінаковым этапам у меру зростання ўпэўненых патрабаванняў.

    Рэвэл 1: атрыбут для кожнай варіяцыі

    Найпростейшы кантакт прыймае окремы атрыбут для кожнага варіанта, які ён можа прадстаўляць:

    function Button({ text, color, icon }) {
      return <button style={{ color }}>{icon} {text}</button>;
    }
    

    Кожная новая варіяцыя вымагае ўтворэння яшчо адной настройкі, і API расте без меры. Гэтую проблему часта называюць «выбухам настройкаў»; яна не аднакова з процэсам перадачы даных через многія шары, які называецца «бурэннея настройкаў».

    Рывень 2: складанне з дзецячымі элементамі

    Якщо дазволіць тым, хто вызывае компонент, перадаваць контэнт праз children, большасць такіх настройкаў знікне. Кнопка будзе заставалася толькі з variant для стылю:

    function Button({ children, variant }) {
      return <button className={`btn btn-${variant}`}>{children}</button>;
    }
    

    Тепер той, хто вызывае компонент, кантролюе тое, што будзе прадставлена ўнутрь, укладаючы там і іконкі:

    <Button variant="primary">
      <Icon name="save" /> Save Changes
    </Button>
    

    Рывень 3: іменаваныя слоты

    Калі компоненту патрэбны калькі розных регіонаў, можна прыймаць JSX праз іменаваныя настройкі. Макет стороніцы можа включаць заголовак, бокавую панель і галоўны контэнт:

    function PageLayout({ header, sidebar, children }) {
      return (
        <div className="layout">
          <header>{header}</header>
          <aside>{sidebar}</aside>
          <main>{children}</main>
        </div>
      );
    }
    

    Кожны регіон заполняецца на месцы вызыву:

    <PageLayout
      header={<Navbar />}
      sidebar={<FilterPanel />}
    >
      <ProductList />
    </PageLayout>
    

    Слоты — это звычныя пропы, якія храняють JSX: тыя, хто вызывае компонент, кантролююць тое, што заполняе кожную зону, сам компонент кантролюе структуру, а лейаут застаёцца актуальным нават пад час змены ваклагаў. Чыба глэбачэй разумець гэты падход да рефакторавання, адзірніце як выправіць перавантажэння пропаў у React за дапамогою композіцыі і слотаў.

    Кораткае апісанне модэлю

    • JSX: синтаксічны «цукор» для вызоў функцый стварэння элементаў, такіх як React.createElement().
    • Элемент React: звычны об’ект JavaScript, які описвае UI.
    • Компонент: функцыя, якая вяртае элементы React.
    • Пропы: адзіны об’ект аргументаў, якія пасылаюцца да гэтай функцыі.
    • children: спецыяльны проп, які храніць вярнуты ўсероўнішы JSX-контэнт.
  • Змена значэння пропаў: ніколи не дазволяецца; пропы трэба спрыяваць як толькі для чытання.
  • Чыстасць — гэта правіла, якія дапамагаюць расту

    Усё гэта залежыць ад чыстасці: тыя ж пропы завжды должны даўаць той самы выхід. Калі процес атрыбутавання не мае побачных эфектаў, не змінюе пропы і не прабягае за межы зовнішняго стану, React можа без апасцярыбы прыхіліцца да павторэння, перакрыття чысткі роботы, а вы можете аналізаваць кожны компонент окрема. У режыме строгасці компоненты нават атрыбутуюцца два разы пад час розработкі, каб выявіць нечыстую логіку.

    Якщо нарушыце чыстасць, вы прывабіце багі, якія проявляюцца далека ад свайго падстаўнага канкрэтнага прычыны. Якщо яе падтрымаце, вашы компоненты застануцца прыведамымі, лёгкімі для тэставання і падтрымкі, калі аплікацыя расте.