Przekazywanie wartości do komponentu

Komponenty reactowe używają właściwości (ang. props, od “properties”) do komunikowania się między sobą. Każdy komponent nadrzędny może przekazać informacje do własnych potomków poprzez właściwości. Właściwości mogą kojarzyć się z atrybutami HTML-owymi, jednak różnica polega na tym, że przez właściwości można przekazywać dowolne wartości javascriptowe, w tym obiekty, tablice czy funkcje.

W tej sekcji dowiesz się

  • Jak przekazać wartości do komponentu
  • Jak odczytać właściwości komponentu
  • Jak określić domyślną wartość dla właściwości
  • Jak przekazać kod JSX-owy do komponentu
  • Jak właściwości zmieniają się w czasie

Właściwości, które możesz już znać

Właściwości (ang. props) to informacje, które przekazujemy znacznikowi JSX-owemu. Na przykład, znacznikowi <img> możemy przekazać właściwości className, src, alt, width czy height:

function Avatar() {
  return (
    <img
      className="avatar"
      src="https://i.imgur.com/1bX5QH6.jpg"
      alt="Lin Lanying"
      width={100}
      height={100}
    />
  );
}

export default function Profile() {
  return <Avatar />;
}

Właściwości, które możesz przekazać do znacznika <img>, są predefiniowane (ReactDOM przestrzega standardu HTML). Jednak do własnych komponentów, np. <Avatar>, możesz przekazać dowolne właściwości!

Przekazywanie wartości do komponentu

W poniższym kodzie komponent Profile nie przekazuje swojemu potomkowi Avatar żadnych wartości:

export default function Profile() {
return <Avatar />;
}

Aby dodać do komponentu Avatar właściwości, wystarczą dwa kroki.

Krok 1: Przekaż właściwości do komponentu potomnego

Najpierw przekażmy do komponentu Avatar jakieś wartości. Na przykład, niech będą to person (obiekt) oraz size (liczba):

export default function Profile() {
return (
<Avatar person={{name: 'Lin Lanying', imageId: '1bX5QH6'}} size={100} />
);
}

Notatka

Jeśli nie wiesz lub nie pamiętasz, o co chodzi z podwójnymi nawiasami klamrowymi za person=: to po prostu obiekt zapisany wewnątrz JSX-owych klamerek.

Teraz możemy odczytać te wartości wewnątrz komponentu Avatar.

Krok 2: Odczytaj wartości wewnątrz komponentu potomnego

Aby odczytać te właściwości, wypiszmy ich nazwy oddzielone przecinkiem i zapisane wewnątrz ({ oraz }) zaraz po słowach function Avatar. Dzięki temu będziemy mogli odwołać się do nich jak do zmiennych.

function Avatar({person, size}) {
// tutaj można używać person i size
}

Teraz wystarczy dodać do komponentu Avatar logikę, która używa właściwości person i size do renderowania - i gotowe!

To, co wyrenderuje Avatar, możemy kontrolować na wiele różnych sposobów, przekazując różne wartości dla właściwości. Spróbuj zmienić którąś z nich!

import { getImageUrl } from './utils.js';

function Avatar({person, size}) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

export default function Profile() {
  return (
    <div>
      <Avatar
        size={100}
        person={{
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2',
        }}
      />
      <Avatar
        size={80}
        person={{
          name: 'Aklilu Lemma',
          imageId: 'OKS67lh',
        }}
      />
      <Avatar
        size={50}
        person={{
          name: 'Lin Lanying',
          imageId: '1bX5QH6',
        }}
      />
    </div>
  );
}

Właściwości pozwalają myśleć o komponentach nadrzędnych i potomnych jako o bytach niezależnych. Możemy, na przykład, zmienić wartości przekazywane przez właściwości person i size w Profile i nie musimy wiedzieć, jak Avatar z nich korzysta. Podobnie możemy zmienić sposób użycia tych wartości w Avatar bez patrzenia na kod Profile.

Możesz myśleć o właściwościach jak o “pokrętłach”, którymi można sterować. Pełnią taką samą rolę co argumenty w funkcjach - tak naprawdę właściwości jedynym argumentem dla komponentu! Funkcyjne komponenty reactowe przyjmują jeden argument - obiekt props:

function Avatar(props) {
let person = props.person;
let size = props.size;
// ...
}

Zwykle jednak nie ma potrzeby korzystać z samego obiektu props, dlatego zazwyczaj się je destrukturyzuje na poszczególne właściwości.

Zwróć uwagę

Nie zapomnij o parze klamer { i } wewnątrz nawiasów okrągłych ( i ):

function Avatar({person, size}) {
// ...
}

Powyższy zapis nazywamy “destrukturyzacją”. Jest on równoważny do odczytu właściwości z parametru funkcji:

function Avatar(props) {
let person = props.person;
let size = props.size;
// ...
}

Określanie domyślnej wartości dla właściwości

Jeśli chcesz nadać właściwości domyślną wartość, która będzie użyta za każdym razem, gdy nie przekażemy żadnej wartości do komponentu, możesz to zrobić dodając do zapisu destrukturyzującego symbol = i podając po nim wartość domyślną:

function Avatar({person, size = 100}) {
// ...
}

Teraz gdy wyrenderujemy <Avatar person={...} /> bez podawania właściwości size, zostanie ona ustawiona na wartość 100.

Wartość domyślna jest używana tylko wtedy, gdy właściwość size zostanie pominięta lub otrzyma wartość size={undefined}. Jeśli jednak przekażesz size={null} lub size={0}, domyślna wartość nie zostanie użyta.

Przekazywanie właściwości za pomocą operatora rozwinięcia

Niekiedy przekazywanie właściwości może okazać się bardzo uciążliwe:

function Profile({person, size, isSepia, thickBorder}) {
return (
<div className="card">
<Avatar
person={person}
size={size}
isSepia={isSepia}
thickBorder={thickBorder}
/>
</div>
);
}

Ogólnie rzecz biorąc, nie ma niczego złego w powtarzającym się kodzie - czasami może to nawet pozytywnie wpłynąć na jego czytelność. Z reguły jednak zależy nam na zwięzłości. Niektóre komponenty przekazują potomkom wszystkie swoje właściwości, jak to ma miejsce w przypadku Profile i Avatar poniżej. Z racji tego, że Profile nie korzysta z żadnej z właściwości, warto użyć operatora rozwinięcia (ang. spread operator):

function Profile(props) {
return (
<div className="card">
<Avatar {...props} />
</div>
);
}

To sprawi, że wszystkie właściwości komponentu Profile trafią do Avatar bez konieczności wypisywania każdej z nich.

Używaj operatora rozwinięcia z umiarem. Jeśli nagminnie używasz go w niemal każdym komponencie, to coś jest nie tak. Zwykle świadczy to o potrzebie podzielenia komponentów i przekazania potomków jako JSX. Ale o tym za chwilę!

Przekazywanie potomków jako JSX

Dość często można spotkać takie oto zagnieżdżenie wbudowanych znaczników przeglądarkowych:

<div>
<img />
</div>

W podobny sposób można także zagnieździć własne komponenty:

<Card>
<Avatar />
</Card>

Kiedy zagnieżdżasz jakiś kod wewnątrz znacznika JSX, komponent nadrzędny do tego kodu otrzyma go jako wartość we właściwości children. Dla przykładu, poniższy komponent Card otrzyma właściwość children ustawioną na <Avatar /> i wyrenderuje ją wewnątrz kontenera div:

import Avatar from './Avatar.js';

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

export default function Profile() {
  return (
    <Card>
      <Avatar
        size={100}
        person={{
          name: 'Katsuko Saruhashi',
          imageId: 'YfeOqp2',
        }}
      />
    </Card>
  );
}

Spróbuj zastąpić <Avatar> wewnątrz <Card> jakimś tekstem, aby zobaczyć na własne oczy, że komponent Card może opakowywać dowolną treść. Nie musi on “wiedzieć”, co renderuje. Ten wzorzec ma szerokie spektrum zastosowań i z pewnością spotkasz się z nim jeszcze nieraz.

Komponent z właściwością children można sobie wyobrazić jako taki z “dziurą”, którą komponent nadrzędny może “zapełnić” dowolnym kodem JSX. Dość często stosuje się children w komponentach opakowujących coś wizualnie: panelach, siatkach itp.

Komponent Card w kształcie puzzla z miejscem na elementy "potomne" jak tekst czy Avatar

Autor ilustracji Rachel Lee Nabors

Jak właściwości zmieniają się w czasie

Komponent Clock przedstawiony poniżej otrzymuje od swojego “rodzica” dwie właściwości: color oraz time. (Celowo pominęliśmy tu kod rodzica, ponieważ korzysta on ze stanu, o którym będzie mowa w dalszych rozdziałach.)

Spróbuj zmienić kolor, wybierając opcję z poniższej listy rozwijanej:

export default function Clock({ color, time }) {
  return (
    <h1 style={{ color: color }}>
      {time}
    </h1>
  );
}

Ten przykład pokazuje, że komponent może otrzymywać wartości właściwości zmienne w czasie. Właściwości nie są zawsze statyczne! Tutaj wartość dla time zmienia się co sekundę, a dla color w momencie wybrania opcji z listy rozwijanej. Właściwości odzwierciedlają dane komponentu w określonym momencie, a nie tylko na początku.

Warto jednak pamiętać, że właściwości są niemutowalne (ang. immutable) — określenie to pochodzi z informatyki i oznacza “niezmienność”. Kiedy komponent chce zmienić swoje właściwości (na przykład w odpowiedzi na interakcję użytkownika lub nowe dane), musi “poprosić” swojego “rodzica”, aby ten przekazał mu inne wartości - czyli nowy obiekt! Wtedy stare właściwości zostaną zapomniane, a niedługo potem silnik JavaScriptu odzyska zajmowaną przez nie pamięć.

Nie próbuj “zmieniać właściwości”. Kiedy zechcesz zareagować na dane wprowadzone przez użytkownika (jak np. zmiana wybranego koloru), musisz “ustawić stan”, o czym nauczysz się w rozdziale pt. Stan - Pamięć komponentu.

Powtórka

  • Aby przekazać właściwości, dodaj je do kodu JSX, tak jak to robisz z atrybutami w HTML-u.
  • Aby odczytać wartości właściwości, użyj destrukturyzacji function Avatar({ person, size }).
  • Możesz ustawić domyślną wartość, np. size = 100, która zostanie użyta, gdy właściwość nie ma wartości lub jest ona ustawiona na undefined.
  • Możesz przekazać wszystkie właściwości za pomocą operatora rozwinięcia <Avatar {...props} />; ale nie nadużywaj tego sposobu!
  • Zagnieżdżony kod JSX, jak np. <Card><Avatar /></Card>, zostanie przekazany do komponentu Card jako właściwość children.
  • Właściwości są jak niezmienialne “migawki” z danego momentu w czasie: każde renderowanie komponentu dostarcza nową wersję właściwości.
  • Nie można zmieniać wartości właściwości. Jeśli potrzebujesz interaktywności, musisz ustawiać stan.

Wyzwanie 1 z 3:
Wyodrębnij komponent

Ten komponent Gallery zawiera bardzo podobny kod dla dwóch profili. Wyodrębnij z niego komponent Profile, aby zmniejszyć powtarzalność w kodzie. Następnie pomyśl, jakie właściwości należy przekazać do Profile.

import { getImageUrl } from './utils.js';

export default function Gallery() {
  return (
    <div>
      <h1>Wybitni naukowcy</h1>
      <section className="profile">
        <h2>Maria Skłodowska-Curie</h2>
        <img
          className="avatar"
          src={getImageUrl('szV5sdG')}
          alt="Maria Skłodowska-Curie"
          width={70}
          height={70}
        />
        <ul>
          <li>
            <b>Profesja: </b>
            fizyka i chemia
          </li>
          <li>
            <b>Nagrody: 4 </b>
            (Nagroda Nobla w dziedzinie fizyki, Nagroda Nobla w dziedzinie chemii,
            Medal Davy'ego, Medal Matteucciego)
          </li>
          <li>
            <b>Odkrycia: </b>
            polon (pierwiastek)
          </li>
        </ul>
      </section>
      <section className="profile">
        <h2>Katsuko Saruhashi</h2>
        <img
          className="avatar"
          src={getImageUrl('YfeOqp2')}
          alt="Katsuko Saruhashi"
          width={70}
          height={70}
        />
        <ul>
          <li>
            <b>Profesja: </b>
            geochemia
          </li>
          <li>
            <b>Nagrody: 2 </b>
            (Nagroda Miyake w dziedzinie geochemii, Nagroda Tanaki)
          </li>
          <li>
            <b>Odkrycia: </b>
            metoda pomiaru dwutlenku węgla w wodzie morskiej
          </li>
        </ul>
      </section>
    </div>
  );
}