Dodawanie interaktywności

Niektóre elementy na ekranie aktualizują się w odpowiedzi na interakcje użytkownika. Na przykład kliknięcie w galerię obrazów zmienia aktywny obraz. W Reakcie dane, które zmieniają się w czasie, nazywane są stanem (ang. state). Możesz dodać stan do każdego komponentu i aktualizować go w razie potrzeby. W tym rozdziale nauczysz się, jak pisać komponenty obsługujące interakcje, aktualizujące swój stan i wyświetlające różne wyniki w czasie.

Reagowanie na zdarzenia

React pozwala na dodawanie procedur obsługi zdarzeń (ang. event handlers) do twojej składni JSX. Procedury obsługi zdarzeń to twoje własne funkcje, które zostaną wywołane w odpowiedzi na interakcje użytkownika, takie jak kliknięcia, najechanie kursorem, skupienie na elementach formularza i inne.

Wbudowane komponenty, takie jak <button>, obsługują jedynie wbudowane zdarzenia przeglądarki, takie jak onClick. Jednakże możesz też tworzyć własne komponenty i nadawać ich właściwościom obsługującym zdarzenia dowolne nazwy specyficzne dla twojej aplikacji.

export default function App() {
  return (
    <Toolbar
      onPlayMovie={() => alert('Odtwarzanie!')}
      onUploadImage={() => alert('Wgrywanie!')}
    />
  );
}

function Toolbar({ onPlayMovie, onUploadImage }) {
  return (
    <div>
      <Button onClick={onPlayMovie}>
        Odtwórz film
      </Button>
      <Button onClick={onUploadImage}>
        Wgraj obraz
      </Button>
    </div>
  );
}

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

Chcesz zgłębić ten temat?

Przeczytaj rozdział Reagowanie na zdarzenia, aby dowiedzieć się, jak dodawać procedury obsługi zdarzeń.

Czytaj dalej

Stan - pamięć komponentu

Komponenty często muszą zmieniać to, co jest wyświetlane na ekranie w wyniku interakcji. Wpisywanie w formularzu powinno aktualizować jego pole, kliknięcie “następny” w kolejce obrazów powinno zmieniać wyświetlany obraz, kliknięcie “kup” powinno umieścić produkt w koszyku. Komponenty muszą “pamiętać” różne rzeczy: bieżącą wartość pola, bieżący obraz, zawartość koszyka. W Reakcie tego rodzaju pamięć specyficzna dla komponentu nazywana jest stanem.

Możesz dodać stan do komponentu za pomocą hooka useState. Hooki to specjalne funkcje, które pozwalają twoim komponentom korzystać z funkcjonalności Reacta (stan jest jedną z tych funkcjonalności). Hook useState pozwala zadeklarować zmienną stanu. Przyjmuje on stan początkowy i zwraca parę wartości: bieżący stan oraz funkcję ustawiającą stan, która pozwala na jego aktualizację.

const [index, setIndex] = useState(0);
const [showMore, setShowMore] = useState(false);

Oto jak galeria obrazów wykorzystuje i aktualizuje stan po kliknięciu:

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);
  const hasNext = index < sculptureList.length - 1;

  function handleNextClick() {
    if (hasNext) {
      setIndex(index + 1);
    } else {
      setIndex(0);
    }
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Następny
      </button>
      <h2>
        <i>{sculpture.name} </i>
        autorstwa {sculpture.artist}
      </h2>
      <h3>
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Ukryj' : 'Pokaż'} detale
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img
        src={sculpture.url}
        alt={sculpture.alt}
      />
    </>
  );
}

Chcesz zgłębić ten temat?

Przeczytaj rozdział Stan - Pamięć komponentu, aby dowiedzieć się, jak zapamiętywać wartość i aktualizować ją podczas interakcji.

Czytaj dalej

Renderowanie i aktualizowanie

Zanim twoje komponenty zostaną wyświetlone na ekranie, muszą zostać wyrenderowane przez Reacta. Zrozumienie kroków w tym procesie pomoże ci zrozumieć, jak wykonuje się twój kod i wyjaśnić jego działanie.

Wyobraź sobie, że twoje komponenty to kucharze w kuchni, którzy przygotowują smaczne dania z dostępnych składników. W tej sytuacji React jest kelnerem, który przyjmuje zamówienia od klientów i przynosi im zamówione potrawy. Ten proces zgłaszania i obsługi interfejsu użytkownika składa się z trzech kroków:

  1. Wywołanie (ang. triggering) renderowania (przekazanie zamówienia od gościa do kuchni)
  2. Renderowanie (ang. rendering) komponentu (przygotowanie zamówienia w kuchni)
  3. Aktualizowanie (ang. committing) drzewa DOM (umieszczenie zamówienia na stole)
  1. React jako kelner w restauracji, pobierający zamówienia od użytkowników i dostarczający je do Kuchni Komponentów.
    Wywołanie
  2. Kucharz komponentu Card przekazuje Reactowi świeży komponent Card.
    Renderowanie
  3. React dostarcza komponent Card użytkownikowi do jego stołu.
    Aktualizowanie

Autor ilustracji Rachel Lee Nabors

Chcesz zgłębić ten temat?

Przeczytaj rozdział Renderowanie i aktualizowanie, aby dowiedzieć się o cyklu życia aktualizacji interfejsu użytkownika.

Czytaj dalej

Stan jako migawka

W przeciwieństwie do zwykłych zmiennych javascriptowych, stan w Reakcie zachowuje się bardziej jak migawka. Ustawienie stanu nie zmienia już istniejącej zmiennej stanu, lecz wywołuje przerenderowanie. Może to być na początki zaskakujące!

console.log(count); // 0
setCount(count + 1); // Żądanie ponownego renderowania z wartością 1
console.log(count); // Nadal 0!

To zachowanie pomaga unikać subtelnych błędów. Oto mała aplikacja do czatu. Spróbuj zgadnąć, co się stanie, jeśli najpierw naciśniesz “Wyślij”, a potem zmienisz odbiorcę na Boba. Czyje imię pojawi się w alert pięć sekund później?

import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('Cześć');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`Wysłano wiadomość "${message}" do użytkownika ${to}`);
    }, 5000);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Wyślij</button>
    </form>
  );
}

Chcesz zgłębić ten temat?

Przeczytaj rozdział Stan jako migawka, aby dowiedzieć się, dlaczego stan wydaje się być “ustalony” i niezmienny wewnątrz funkcji obsługujących zdarzenia.

Czytaj dalej

Kolejkowanie serii aktualizacji stanu

Ten komponent zawiera błąd: kliknięcie “+3” zwiększa wynik tylko raz.

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(score + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Wynik: {score}</h1>
    </>
  )
}

Stan jako migawka wyjaśnia, dlaczego tak się dzieje. Ustawienie stanu tworzy żądanie nowego przerenderowania, ale nie zmienia tego stanu w już działającym kodzie. Dlatego score nadal wynosi 0 tuż po wywołaniu setScore(score + 1).

console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0
setScore(score + 1); // setScore(0 + 1);
console.log(score); // 0

Możesz naprawić to, przekazując funkcję aktualizującą (ang. updater function) podczas ustawiania stanu. Zauważ, jak zastąpienie setScore(score + 1) przez setScore(s => s + 1) naprawia przycisk “+3”. Dzięki temu możesz kolejkować wiele aktualizacji stanu.

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(s => s + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Wynik: {score}</h1>
    </>
  )
}

Chcesz zgłębić ten temat?

Przeczytaj rozdział Kolejkowanie serii aktualizacji stanu, aby dowiedzieć się, jak kolejkować sekwencję aktualizacji stanu.

Czytaj dalej

Aktualizowanie obiektów w stanie

Stan może przechowywać dowolne wartości javascriptowe, w tym obiekty. Nie powinno się jednak bezpośrednio zmieniać obiektów i tablic, które przechowuje się w stanie Reacta. Zamiast tego, gdy chcesz zaktualizować obiekt lub tablicę, musisz stworzyć nowy obiekt (lub skopiować istniejący), a następnie zaktualizować stan, aby używał tej kopii.

Zazwyczaj używa się składni rozproszenia (ang. spread syntax) ..., aby skopiować obiekty i tablice, które chcesz zmienić. Na przykład, aktualizacja zagnieżdżonego obiektu może wyglądać tak:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Imię:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Tytuł:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        Miasto:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Obraz:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' autorstawa '}
        {person.name}
        <br />
        (położone w mieście {person.artwork.city})
      </p>
      <img
        src={person.artwork.image}
        alt={person.artwork.title}
      />
    </>
  );
}

Jeśli kopiowanie obiektów w kodzie staje się uciążliwe, możesz użyć biblioteki takiej jak Immer, aby zmniejszyć ilość powtarzającego się kodu:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Chcesz zgłębić ten temat?

Przeczytaj rozdział Aktualizowanie obiektów w stanie, aby dowiedzieć się, jak poprawnie aktualizować obiekty.

Czytaj dalej

Aktualizowanie tablic w stanie

Tablice są kolejnym rodzajem zmiennych obiektów javascriptowych, które można przechowywać w stanie i należy je traktować jako tylko do odczytu. Podobnie jak w przypadku obiektów, gdy chcesz zaktualizować tablicę przechowywaną w stanie, musisz stworzyć nową tablicę (lub skopiować istniejącą), a następnie ustawić stan, aby używał nowej tablicy:

import { useState } from 'react';

const initialList = [
  { id: 0, title: 'Wielkie brzuchy', seen: false },
  { id: 1, title: 'Księżycowy krajobraz', seen: false },
  { id: 2, title: 'Terakotowa armia', seen: true },
];

export default function BucketList() {
  const [list, setList] = useState(
    initialList
  );

  function handleToggle(artworkId, nextSeen) {
    setList(list.map(artwork => {
      if (artwork.id === artworkId) {
        return { ...artwork, seen: nextSeen };
      } else {
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Lista dzieł sztuki</h1>
      <h2>Moja lista dzieł sztuki do zobaczenia:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

Jeśli kopiowanie tablic w kodzie staje się uciążliwe, możesz użyć biblioteki takiej jak Immer, aby zmniejszyć ilość powtarzającego się kodu:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Chcesz zgłębić ten temat?

Przeczytaj rozdział Aktualizowanie tablic w stanie, aby dowiedzieć się, jak poprawnie aktualizować tablice.

Czytaj dalej

Co dalej?

Przejdź do rozdziału Reagowanie na zdarzenia, aby zacząć zgłębiać ten temat strona po stronie!

Ewentualnie, jeśli już znasz te tematy, dlaczego nie przeczytać rozdziału Zarządzanie stanem?