Lekcja 4: Komponenty Funkcyjne i Props

Komponenty to serce aplikacji React. Pozwalają dzielić interfejs użytkownika na niezależne, reużywalne części. W nowoczesnym Reactcie dominują komponenty funkcyjne, które są prostsze i łatwiejsze w użyciu dzięki Hookom (o których w kolejnych lekcjach). W tej lekcji skupimy się na tworzeniu komponentów funkcyjnych i przekazywaniu do nich danych za pomocą "props".

Komponenty Funkcyjne

Komponent funkcyjny w React to zwykła funkcja JavaScript, która przyjmuje jeden argument (obiekt "props") i zwraca elementy React (zazwyczaj JSX), opisujące, co powinno zostać wyrenderowane.

// Najprostszy komponent funkcyjny
function Welcome(props) {
  return <h1>Witaj, {props.name}!</h1>;
}

// Użycie komponentu
const element = <Welcome name="Sara" />;

// Renderowanie (zakładając istnienie elementu #root)
const root = ReactDOM.createRoot(document.getElementById(\'root\'));
root.render(element);

Kluczowe cechy komponentów funkcyjnych:

Props (Properties)

Props to sposób przekazywania danych z komponentu nadrzędnego (rodzica) do komponentu podrzędnego (dziecka). Działają podobnie do atrybutów w HTML.

W przykładzie powyżej, komponent Welcome został użyty z "propsem" name o wartości "Sara": <Welcome name="Sara" />. Wewnątrz komponentu Welcome, ta wartość jest dostępna jako props.name.

Props są tylko do odczytu (read-only). Komponent nigdy nie powinien modyfikować swoich własnych propsów. Wszystkie komponenty React muszą działać jak "czyste funkcje" w odniesieniu do swoich propsów - dla tych samych danych wejściowych (props) zawsze powinny zwracać ten sam wynik.

// Przykład przekazywania wielu propsów
function UserCard(props) {
  return (
    <div className="user-card">
      <img src={props.avatarUrl} alt={props.name} />
      <h2>{props.name}</h2>
      <p>Email: {props.email}</p>
    </div>
  );
}

// Użycie komponentu z wieloma propsami
const element = (
  <UserCard 
    name="Jan Kowalski"
    email="jan.kowalski@example.com"
    avatarUrl="https://via.placeholder.com/100"
  />
);

Możemy również użyć destrukturyzacji ES6, aby kod był bardziej czytelny:

function UserCard({ name, email, avatarUrl }) { // Destrukturyzacja props
  return (
    <div className="user-card">
      <img src={avatarUrl} alt={name} />
      <h2>{name}</h2>
      <p>Email: {email}</p>
    </div>
  );
}

Kompozycja Komponentów

Komponenty mogą renderować inne komponenty. To pozwala na budowanie złożonych interfejsów z mniejszych, reużywalnych części.

// Komponent Welcome
function Welcome(props) {
  return <h1>Witaj, {props.name}!</h1>;
}

// Komponent App, który używa komponentu Welcome wielokrotnie
function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Kasia" />
      <Welcome name="Tomek" />
    </div>
  );
}

// Renderowanie komponentu App
const root = ReactDOM.createRoot(document.getElementById(\'root\'));
root.render(<App />);

Props `children`

Specjalnym propsem jest children. Reprezentuje on zawartość umieszczoną pomiędzy tagiem otwierającym a zamykającym komponentu.

// Komponent Card, który może renderować dowolną zawartość wewnątrz
function Card(props) {
  return (
    <div className="card">
      {props.children} 
    </div>
  );
}

// Użycie komponentu Card
function App() {
  return (
    <Card>
      {/* Ta zawartość będzie dostępna jako props.children w komponencie Card */}
      <h2>Tytuł karty</h2>
      <p>To jest treść karty.</p>
      <button>Kliknij mnie</button>
    </Card>
  );
}

Możemy również użyć destrukturyzacji dla children:

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

Domyślne wartości Props (Default Props)

Możemy zdefiniować domyślne wartości dla propsów, które będą używane, jeśli komponent nadrzędny nie przekaże danej właściwości.

function Button({ label = "Kliknij", color = "blue" }) {
  const style = { backgroundColor: color, color: \'white\', padding: \'10px\', border: \'none\' };
  return <button style={style}>{label}</button>;
}

// Użycie:
<Button /> // Użyje domyślnych wartości: label="Kliknij", color="blue"
<Button label="Wyślij" /> // Użyje label="Wyślij", color="blue"
<Button color="green" /> // Użyje label="Kliknij", color="green"
<Button label="Anuluj" color="red" /> // Użyje podanych wartości

Ćwiczenie praktyczne

Pokaż rozwiązanie
// src/UserInfo.jsx
import React from \'react\';

function UserInfo({ user }) {
  return (
    <div className="UserInfo">
      <img className="Avatar" src={user.avatarUrl} alt={user.name} />
      <div className="UserInfo-name">{user.name}</div>
    </div>
  );
}

export default UserInfo;

// src/Comment.jsx
import React from \'react\';
import UserInfo from \'./UserInfo\'; // Załóżmy, że UserInfo jest w tym samym folderze

function formatDate(date) {
  return date.toLocaleDateString();
}

function Comment({ author, text, date }) {
  return (
    <div className="Comment">
      <UserInfo user={author} />
      <div className="Comment-text">{text}</div>
      <div className="Comment-date">{formatDate(date)}</div>
    </div>
  );
}

export default Comment;

// src/App.jsx
import React from \'react\';
import Comment from \'./Comment\';
import \'./App.css\'; // Załóżmy, że mamy podstawowe style

const commentData = {
  author: {
    name: \'Anna Nowak\',
    avatarUrl: \'https://via.placeholder.com/50\'
  },
  text: \'Mam nadzieję, że podoba Ci się nauka Reacta!\',
  date: new Date()
};

function App() {
  return (
    <div>
      <h1>Komentarze:</h1>
      <Comment 
        author={commentData.author}
        text={commentData.text}
        date={commentData.date}
      />
    </div>
  );
}

export default App;

Dodaj podstawowe style w App.css, aby komentarz wyglądał sensownie.

Cel: Stworzyć komponent Comment, który wyświetla komentarz użytkownika (awatar, imię, treść, data).

Kroki:

  1. W projekcie React (np. Vite) stwórz nowy plik komponentu, np. src/Comment.jsx.
  2. Zdefiniuj komponent funkcyjny Comment, który przyjmuje propsy: author (obiekt z name i avatarUrl), text i date.
  3. Użyj JSX, aby wyrenderować strukturę komentarza, wykorzystując przekazane propsy.
  4. Stwórz pomocniczy komponent UserInfo, który przyjmuje user (obiekt z name i avatarUrl) jako props i renderuje awatar oraz imię użytkownika. Użyj tego komponentu wewnątrz Comment.
  5. W komponencie App.jsx zaimportuj i użyj komponentu Comment, przekazując przykładowe dane.

Zadanie do samodzielnego wykonania

Stwórz komponent Product, który przyjmuje propsy: name (string), price (number), isOnSale (boolean), features (tablica stringów). Komponent powinien:

  1. Wyświetlać nazwę produktu jako nagłówek <h3>.
  2. Wyświetlać cenę. Jeśli isOnSale jest true, cena powinna być wyświetlona na czerwono i z dopiskiem "(Promocja!)".
  3. Wyświetlać listę cech (features) jako <ul>.
  4. Użyj domyślnej wartości dla isOnSale na false.
  5. Użyj tego komponentu w App.jsx kilka razy z różnymi danymi.

FAQ - Komponenty Funkcyjne i Props

Czym różni się komponent funkcyjny od komponentu klasowego?

Komponenty funkcyjne to prostsze funkcje JavaScript, które przyjmują props i zwracają JSX. Komponenty klasowe (starsze podejście) to klasy ES6 dziedziczące po `React.Component`, mające dostęp do stanu przez `this.state` i metod cyklu życia. Od wprowadzenia Hooków, komponenty funkcyjne mogą zarządzać stanem i efektami ubocznymi, stając się preferowanym podejściem.

Dlaczego nazwy komponentów muszą zaczynać się wielką literą?

React używa tej konwencji, aby odróżnić tagi komponentów React od zwykłych tagów HTML. Jeśli nazwa zaczyna się małą literą (np. `<button />`), React traktuje ją jako tag HTML. Jeśli zaczyna się wielką literą (np. `<Button />`), React wie, że ma do czynienia z komponentem zdefiniowanym przez użytkownika.

Czy propsy mogą być czymś innym niż stringi?

Tak, propsy mogą przyjmować dowolny typ danych JavaScript: stringi, liczby, booleany, tablice, obiekty, funkcje, a nawet inne elementy React. Wartości inne niż stringi przekazujemy, umieszczając je w nawiasach klamrowych: `<MyComponent count={10} isActive={true} data={{ key: \'value\' }} onClick={handleClick} />`.

Co to znaczy, że propsy są "read-only"?

Oznacza to, że komponent nie powinien nigdy próbować zmieniać wartości propsów, które otrzymał od rodzica. Propsy służą do przekazywania danych w dół drzewa komponentów. Jeśli komponent potrzebuje zmieniać dane, powinien używać stanu (state), o którym będzie mowa w kolejnej lekcji.

Jak przekazać funkcję jako props?

Funkcję przekazuje się tak samo jak inne wartości JavaScript - w nawiasach klamrowych. Jest to powszechny wzorzec w React, np. do obsługi zdarzeń w komponencie dziecku, które wywołują funkcję zdefiniowaną w rodzicu: `<ChildComponent onAction={handleAction} />`.

Czy mogę używać destrukturyzacji props w komponencie klasowym?

W komponencie klasowym dostęp do propsów uzyskujemy przez `this.props`. Można użyć destrukturyzacji wewnątrz metod komponentu, np. w metodzie `render()`: `const { name, email } = this.props; return (<div>{name}</div>);`. Nie można jednak destrukturyzować bezpośrednio w definicji klasy jak w funkcji.

Do czego służy `props.children`?

`props.children` zawiera wszystko, co zostało umieszczone pomiędzy tagiem otwierającym a zamykającym komponentu podczas jego użycia. Pozwala to tworzyć generyczne komponenty-kontenery (np. karty, modale), które mogą opakowywać dowolną inną zawartość JSX przekazaną przez rodzica.