Strona główna / Artykuły

Od JSX do zwykłych obiektów: jak działają elementy, komponenty i właściwości w React

Zobacz, do czego kompiluje się JSX, dlaczego komponenty to po prostu funkcje, oraz jak właściwości, dziecięce elementy, wartości domyślne, rozszerzanie i sloty wynikają z tego jednego modelu myślowego.

Wydawca: REACTAPP.TOP9 min czytania1983 słów

Źródła: JSX, Components & Props — The Mental Model Every React Developer Actually Needs

Wiele błędów w React wynika z niejasnego pojęcia o tym, czym właściwie są JSX, komponenty i atrybuty. Gdy już wiesz, że JSX to wywołanie funkcji, które tworzy zwykły obiekt, a komponent to funkcja przyjmująca jeden taki obiekt jako argumenty, reguły, które wydawały się arbitralne, zaczynają mieć sens. Ten artykuł buduje ten model, wychodząc od wyników kompilatora, a następnie stosuje go do atrybutów, children, wartości domyślnych, mechanizmu rozszerzania, zasady niezmienności oraz wzorców kompozycji, których używasz codziennie.

JSX to wywołania funkcji, a nie HTML

Często mówi się o JSX jako o „HTML wewnątrz JavaScriptu”, co jest niepełne. Dokładniej mówiąc, JSX to cukier syntaktyczny, który kompilator przekształca w zwykłe wywołania funkcji.

Weźmy prosty nagłówek:

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

Kompilator taki jak Babel przekształca to w wywołanie React.createElement, przekazując nazwę tagu, obiekt z atrybutami oraz treść:

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

To jest klasyczna transformacja. Od wersji React 17 większość narzędzi korzysta z automatycznego silnika JSX, który zamiast tego wysyła wywołania do funkcji jsx() importowanej z react/jsx-runtime, więc nie jest już konieczne posiadanie obiektu React w zasięgu dostępu. Idea pozostaje identyczna: markup staje się wywołaniem funkcji.

To wywołanie zwraca zwykły obiekt JavaScript o nazwie element React:

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

Element React to nic więcej niż opis tego, co powinno się pojawić na ekranie. React przemierza drzewo tych obiektów i określa, jak dopasować rzeczywisty DOM do tego opisu; to drzewo opisów jest tym, co ludzie zwykle rozumieją pod pojęciem Virtual DOM.

Co wyjaśnia model

Zrozumienie zasady „JSX staje się wywołaniem funkcji, które zwraca obiekt” rozwiązuje kilka powszechnych problemów:

  • Brak instrukcji if wewnątrz JSX. Nie można umieścić instrukcji wewnątrz argumentu funkcji, dlatego używa się wyrażenia ternarnego lub &&, albo przenosi logikę powyżej instrukcji return.
  • className zamiast class. JSX to JavaScript, a class jest tam słowem rezerwowanym.
  • Jeden element korzeniowy. Funkcja zwraca jedną wartość, więc komponent musi zwracać jeden element (który może zawierać wiele dzieci).

Komponent to funkcja, która zwraca elementy

Komponenty to funkcje, których wartość zwracana jest elementem React lub null, aby nic nie zostało wyświetlone.

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

Nie ma żadnej ukrytej mechaniki, a we współczesnym React nie ma potrzeby odwoływania się do klas. React wywołuje funkcję wtedy, gdy musi wiedzieć, co ma wyświetlić. Gdy dane się zmieniają, React ponownie ją wywołuje, otrzymuje nowy drzewo elementów, porównuje je z poprzednim i aktualizuje tylko te części DOM, które się różnią. To porównanie nazywa się rekonsolidacją.

Dwie zasady, których musi przestrzegać każdy komponent

Nazwy muszą zaczynać się od wielkiej litery

JSX wykorzystuje zasady kapitalizacji, aby określić znaczenie tagu. Tag z wielką literą odnosi się do komponentu, więc React wywołuje twoją funkcję:

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

Tag pisany małą literą jest traktowany jako wbudowany element DOM, więc React próbowałby stworzyć literalny element <greeting> zamiast wywołać jakąkolwiek funkcję:

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

Zwróć dokładnie jeden element korzeniowy

Próba zwrócenia dwóch elementów-siostrzanych obok siebie nie udaje się, ponieważ oznacza to zwrócenie dwóch wartości z jednej funkcji:

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

Istnieją dwa rozwiązania. Otocz siostrzane elementy w element nadrzędny lub użyj Fragmentu, zapisanego jako <>...</>, który grupuje je bez dodawania dodatkowego węzła do DOM. Poniższy fragment pokazuje obie wersje jedna po drugiej.

// ✅ 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>
    </>
  );
}

Należy preferować Fragment, chyba że otoczka jest potrzebna do stylizacji lub układu.

Props to argumenty funkcji

Jeśli komponenty są funkcjami, to props to ich argumenty. W tym przypadku komponent odczytuje wartość name z obiektu, który otrzymuje:

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

A tutaj jest renderowany z ustawionym tym atrybutem:

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

React gromadzi wszystkie atrybuty zapisane w tagu JSX do jednego obiektu, props, i przekazuje go jako pierwszy argument. To jest cały mechanizm.

Destrukcja w sygnaturze

Powtarzanie props. szybko staje się chaotyczne. Destrukcja obiektu w liście parametrów nadaje nazwy każdemu używanemu atrybutowi i umożliwia szybkie zobaczenie wejść komponentu:

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

Miejsce wywołania pozostaje niezmienione:

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

Wartości domyślne z destrukcją

Gdy atrybut jest opcjonalny, należy nadać mu wartość domyślną bezpośrednio w wzorcu destrukcji. Jeśli role nie zostanie podany, używana jest wartość domyślna "Developer":

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

W ten sposób używany jest domyślny role:

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

Rozbieranie wartości domyślnych ma zastosowanie tylko wtedy, gdy wartość jest undefined. Wyraźne przekazanie null omija tę domyślną wartość, co należy pamiętać, gdy właściwości pochodzą z danych API.

children: kompozycja wbudowana w składnię

Treść umieszczona pomiędzy otwierającymi i zamykającymi tagami komponentu automatycznie trafia jako właściwość children. Komponent Card może po prostu wyświetlić to, co otrzymał w swoim otoczeniu:

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

Osoba wywołująca decyduje, co ma się tam znajdować:

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

Kompozycja za pomocą children to sposób na tworzenie elastycznych, wielokrotnie używalnych komponentów. Zamiast przewidywać każdą możliwą wariację za pomocą kolejnej właściwości, komponent dostarcza strukturę, a rodzic – treść. Biblioteki takie jak shadcn/ui i Material UI budują w ten sposób okna dialogowe, karty i przyciski.

Każda wartość JavaScript może być właściwością

Zakrzywione nawiasy w JSX oznaczają „wykonaj to jako JavaScript”, więc właściwość może przechowywać dowolną wartość: łańcuchy, liczby, wartości logiczne, tablice, obiekty lub funkcje. Ten komponent otrzymuje kilka rodzajów tych wartości jednocześnie, renderując odznakę warunkowo, mapując tablicę na elementy listy oraz podłączając funkcję zwrotną do przycisku:

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>
  );
}

Miejsce wywołania pokazuje, w jaki sposób przekazywane są poszczególne rodzaje wartości:

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

Kwestie godne uwagi:

  • Zwykłe wartości łańcuchowe można napisać bezpośrednio w cudzysłowach.
  • Wszystko inne, w tym liczby, wartości logiczne, tablice, obiekty i funkcje, musi znajdować się w zakrzywionych nawiasach.
  • Funkcja typu prop, takia jak onDelete, służy do przekazywania informacji o zdarzeniach przez dziecko z powrotem do jego rodzica. To właśnie ten wzorzec callback sprawia, że możliwe jest przenoszenie stanu: stan znajduje się w wspólnym rodzicu, a dzieci mogą go zmieniać jedynie poprzez wywoływanie funkcji przekazanych przez rodzica.
  • Props są tylko do odczytu

    Nigdy nie modyfikuj propów. Przypisanie wartości do pola obiektu prop w tajemnicy zmienia dane należące do rodzica:

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

    Lepiej obliczyć nową wartość i pozostawić oryginalną nietkniętą:

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

    Dane przepływają w jednym kierunku, od rodzica do dziecka, co zapewnia przewidywalność aplikacji React. Dziecko, które potrzebuje, aby rodzic coś zmienił, wywołuje funkcję dostarczoną przez rodzica.

    Przekazywanie propów za pomocą rest i spread

    Gdy komponent otacza element DOM, często chce się przyjąć wiele atrybutów bez wymieniania każdego z nich. Syntaksa typu rest w parametrze wyodrębnia te właściwości, którymi sam komponent się zajmuje, np. label, a pozostałe gromadzi w inputProps, które następnie są przekazywane do elementu input:

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

    Każdy dodatkowy atrybut, taki jak type, placeholder lub onChange, trafia bezpośrednio do podległego elementu:

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

    Biblioteki stale używają tego wzorca, ale przekazywanie dowolnych atrybutów do elementu DOM może spowodować przekazywanie wartości, których ten nie rozumie, co wywołuje ostrzeżenia o nieznanych atrybutach. Należy starannie decydować, co i gdzie przekazuje się. Przykład ten również wyświetla element label bez powiązania go z polem wejściowym; w prawdziwym kodzie należy je połączyć za pomocą htmlFor i id, aby etykieta była dostępna.

    Trzy poziomy elastyczności komponentów

    Komponenty zazwyczaj rozwijają się przez rozpoznawalne etapy wraz ze wzrostem wymagań.

    Poziom 1: atrybut dla każdej wariacji

    Najprostszy przycisk przyjmuje oddzielny atrybut dla każdego elementu, który może wyświetlić:

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

    Każda nowa wersja wymaga kolejnej właściwości, przez co API rośnie bez ograniczeń. Ten problem jest często nazywany eksplozją właściwości; różni się od techniki przekazywania danych przez wiele warstw, zwanej „drillingiem właściwości”.

    Poziom 2: kompozycja dzieci

    Umożliwienie użytkownikowi przekazywania treści jako children eliminuje większość tych właściwości. Przycisk zachowuje jedynie variant służący do stylizacji:

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

    Teraz użytkownik kontroluje to, co się pojawia wewnątrz, włączając ikony:

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

    Poziom 3: nazwane pola

    Gdy komponent potrzebuje kilku odrębnych obszarów, należy przyjąć JSX za pomocą nazwanych właściwości. Układ strony może zawierać nagłówek, panel boczny oraz główną treść:

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

    Każdy obszar jest wypełniany w miejscu wywołania:

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

    Sloty to zwykłe właściwości przechowujące JSX: osoby wywołujące komponent kontrolują to, co wypełnia każdą obszar, komponent kontroluje strukturę, a układ przetrwa zmiany wymagań. Aby lepiej poznać tę ścieżkę refaktoryzacji, zobacz naprawa przeładowania właściwościami w React za pomocą kompozycji i slotów.

    Krótka charakterystyka modelu

    • JSX: syntaktyczny „cukier” dla wywołań funkcji tworzących elementy, takich jak React.createElement().
    • Element React: zwykły obiekt JavaScript opisujący interfejs użytkownika.
    • Komponent: funkcja, która zwraca elementy React.
    • Właściwości: pojedynczy obiekt argumentów przekazywanych do tej funkcji.
    • children: specjalna właściwość przechowująca zawartość JSX w formie struktury zagnieżdżonej.
  • Mutacja właściwości: nigdy nie jest dozwolona; traktuj właściwości jako tylko do odczytu.
  • Czystość to zasada, która pozwala na skalowanie

    Wszystko to zależy od czystości: te same właściwości powinny zawsze generować ten sam wynik. Gdy renderowanie nie ma skutków ubocznych, nigdy nie modyfikuje właściwości ani stanu zewnętrznego, React może bezpiecznie pomijać, powtarzać i przestawiać operacje, a ty możesz analizować każdy komponent oddzielnie. Tryb ścisły nawet renderuje komponenty dwa razy w fazie rozwoju, aby ujawnić nieczystą logikę.

    Złamując zasadę czystości, przyciągasz błędy, które pojawiają się daleko od ich przyczyny. Przestrzegając jej, twoje komponenty pozostają przewidywalne, łatwe do testowania i utrzymania w miarę rozwoju aplikacji.

    Literatura pokrewna

  • Naprawa przejść wysokości w Vue dla asynchronicznie renderowanych składników webowych — Dowiedz się, dlaczego przejście rozszerzania w Vue animuje się od 0px do 0px przy opóźnionym aktualizowaniu składników webowych, oraz jak ResizeObserver opóźnia animację do chwili, gdy istnieje rzeczywista wysokość.