Kurs JavaScript i React · Moduł 3: Stan i zdarzenia
Obsługa zdarzeń
W tej lekcji8
W kosmicznej podróży przez React, obsługa zdarzeń to jak system kontroli lotu, pozwalający na reagowanie na różne akcje użytkownika. Jest to kluczowy element tworzenia interaktywnych interfejsów, które odpowiadają na działania użytkownika, takie jak kliknięcia, wprowadzanie tekstu czy ruchy myszką.
Podstawy obsługi zdarzeń w React
W React, obsługa zdarzeń działa podobnie jak w standardowym HTML, ale z kilkoma istotnymi różnicami:
Nazwy zdarzeń używają konwencji camelCase zamiast małych liter:
- HTML:
onclick,onmouseover - React:
onClick,onMouseOver
- HTML:
W JSX przekazujemy funkcję jako handler zdarzenia, a nie string:
- HTML:
<button onclick="handleClick()">Kliknij</button> - React:
<button onClick={handleClick}>Kliknij</button>
- HTML:
Aby zapobiec domyślnemu zachowaniu, musimy jawnie wywołać
preventDefault:- HTML:
<a href="#" onclick="return false">Kliknij</a> - React:
<a href="#" onClick={(e) => e.preventDefault()}>Kliknij</a>
- HTML:
Reagowanie na działania użytkownika
Zdarzenia to sposób, w jaki komponenty React reagują na działania użytkownika. Przyjrzyjmy się podstawowym typom zdarzeń i jak je obsługiwać.
Obsługa kliknięć
Najprostszym i najczęściej używanym zdarzeniem jest onClick:
1function LaunchButton() {
2 const handleLaunch = () => {
3 console.log('Rozpoczynamy odliczanie do startu!');
4 // Logika startu rakiety...
5 };
6
7 return (
8 <button
9 className="launch-button"
10 onClick={handleLaunch}
11 >
12 Rozpocznij odliczanie
13 </button>
14 );
15}Argumenty w handlerach zdarzeń
Każdy handler zdarzenia otrzymuje obiekt zdarzenia (event) jako pierwszy argument. Zawiera on informacje o zdarzeniu i oferuje metody takie jak preventDefault():
1function NavigationLink() {
2 const handleClick = (event) => {
3 event.preventDefault(); // Zapobiega domyślnej akcji (np. przejściu do innej strony)
4 console.log('Link kliknięty, ale strona się nie przeładowała');
5
6 // Niestandardowa nawigacja lub inna logika...
7 };
8
9 return (
10 <a
11 href="/destination"
12 onClick={handleClick}
13 >
14 Przejdź do celu podróży
15 </a>
16 );
17}Obiekt, który React przekazuje do handlera, to SyntheticEvent - opakowanie natywnego zdarzenia przeglądarki. Ma ten sam interfejs (target, preventDefault(), stopPropagation()) i działa tak samo w każdej przeglądarce, a oryginalne zdarzenie znajdziesz w event.nativeEvent. Kolejność jest więc taka: użytkownik klika, przeglądarka tworzy zdarzenie, React opakowuje je w SyntheticEvent i dopiero wtedy wywołuje twój handler.
Przekazywanie argumentów do handlerów zdarzeń
Czasami potrzebujemy przekazać dodatkowe argumenty do handlera zdarzenia. Możemy to zrobić na dwa sposoby:
1. Używając funkcji strzałkowej (arrow function)
1function MissionControl({ missions }) {
2 const selectMission = (missionId, event) => {
3 console.log(`Wybrano misję o ID: ${missionId}`);
4 console.log('Zdarzenie:', event);
5 // Logika wyboru misji...
6 };
7
8 return (
9 <div className="mission-control">
10 <h2>Dostępne misje</h2>
11 <ul>
12 {missions.map(mission => (
13 <li key={mission.id}>
14 {mission.name}
15 <button onClick={(e) => selectMission(mission.id, e)}>
16 Wybierz
17 </button>
18 </li>
19 ))}
20 </ul>
21 </div>
22 );
23}2. Używając bind
1function MissionControl({ missions }) {
2 const selectMission = (missionId, event) => {
3 console.log(`Wybrano misję o ID: ${missionId}`);
4 // Logika wyboru misji...
5 };
6
7 return (
8 <div className="mission-control">
9 <h2>Dostępne misje</h2>
10 <ul>
11 {missions.map(mission => (
12 <li key={mission.id}>
13 {mission.name}
14 <button onClick={selectMission.bind(null, mission.id)}>
15 Wybierz
16 </button>
17 </li>
18 ))}
19 </ul>
20 </div>
21 );
22}Podejście z funkcją strzałkową jest bardziej powszechne i zazwyczaj czytelniejsze.
Popularne zdarzenia w React
React obsługuje szeroki zakres zdarzeń, które możemy podzielić na kilka kategorii:
Zdarzenia myszy
onClick- kliknięcieonDoubleClick- podwójne kliknięcieonMouseEnter- najechanie kursorem na elementonMouseLeave- wyjście kursorem poza elementonMouseMove- ruch kursora nad elementem
Zdarzenia formularzy
onChange- zmiana wartości elementu formularzaonSubmit- wysłanie formularzaonFocus- element otrzymuje focusonBlur- element traci focus
Zdarzenia klawiatury
onKeyDown- wciśnięcie klawiszaonKeyPress- przestarzałe, zamiast niego używajonKeyDownonKeyUp- zwolnienie klawisza
Zdarzenia dotyku (dla urządzeń mobilnych)
onTouchStart- rozpoczęcie dotykuonTouchMove- ruch podczas dotykuonTouchEnd- zakończenie dotyku
Przykłady obsługi różnych zdarzeń
Zdarzenia myszy ze stanem
Stan zapamiętuje, czy kursor jest nad panelem i czy panel został kliknięty, a zdarzenia onMouseEnter, onMouseLeave i onClick tylko ten stan zmieniają:
1function InteractivePanel() {
2 const [isHovered, setIsHovered] = useState(false);
3 const [isClicked, setIsClicked] = useState(false);
4
5 return (
6 <div
7 className={`panel ${isHovered ? 'hovered' : ''} ${isClicked ? 'clicked' : ''}`}
8 onMouseEnter={() => setIsHovered(true)}
9 onMouseLeave={() => {
10 setIsHovered(false);
11 setIsClicked(false);
12 }}
13 onClick={() => setIsClicked(!isClicked)}
14 >
15 <h3>Panel interaktywny</h3>
16 <p>
17 {isHovered ? 'Kursor nad panelem' : 'Najedź na panel'}
18 {isClicked ? ' - Panel aktywny' : ' - Kliknij, aby aktywować'}
19 </p>
20 </div>
21 );
22}Zdarzenia klawiatury
Klawisze obsługujemy tak samo jak kliknięcia - przez onKeyDown na elemencie, a event.key mówi, który klawisz wciśnięto:
1function SpaceshipControls() {
2 const [position, setPosition] = useState({ x: 50, y: 50 });
3
4 const handleKeyDown = (event) => {
5 switch(event.key) {
6 case 'ArrowUp':
7 setPosition(prev => ({ ...prev, y: Math.max(0, prev.y - 5) }));
8 break;
9 case 'ArrowDown':
10 setPosition(prev => ({ ...prev, y: Math.min(100, prev.y + 5) }));
11 break;
12 case 'ArrowLeft':
13 setPosition(prev => ({ ...prev, x: Math.max(0, prev.x - 5) }));
14 break;
15 case 'ArrowRight':
16 setPosition(prev => ({ ...prev, x: Math.min(100, prev.x + 5) }));
17 break;
18 default:
19 return;
20 }
21 };
22
23 return (
24 <div className="spaceship-simulator" tabIndex={0} onKeyDown={handleKeyDown}>
25 <h2>Symulator statku kosmicznego</h2>
26 <p>Kliknij symulator i steruj statkiem strzałkami</p>
27
28 <div className="universe" style={{ position: 'relative', width: '400px', height: '400px', background: 'black' }}>
29 <div
30 className="spaceship"
31 style={{
32 position: 'absolute',
33 left: `${position.x}%`,
34 top: `${position.y}%`,
35 transform: 'translate(-50%, -50%)',
36 width: '20px',
37 height: '20px',
38 background: 'red'
39 }}
40 ></div>
41 </div>
42
43 <div className="coordinates">
44 <p>Pozycja X: {position.x}</p>
45 <p>Pozycja Y: {position.y}</p>
46 </div>
47 </div>
48 );
49}tabIndex={0} pozwala elementowi div przyjąć fokus, więc onKeyDown działa po kliknięciu w symulator. Nasłuch na całym dokumencie wymaga useEffect ze sprzątaniem - poznasz go w module o efektach.
Przekazywanie funkcji jako props
Często potrzebujemy przekazać funkcje obsługi zdarzeń z komponentu rodzica do komponentu potomnego. Jest to powszechna praktyka, która pozwala na komunikację między komponentami:
1function MissionControl({ onLaunch, onAbort }) {
2 const [countdown, setCountdown] = useState(10);
3 const [isLaunching, setIsLaunching] = useState(false);
4 // useRef przechowuje identyfikator timera między renderami (zmiana ref nie powoduje ponownego renderowania)
5 const timerRef = useRef(null);
6
7 const stopTimer = () => {
8 clearInterval(timerRef.current);
9 timerRef.current = null;
10 };
11
12 const startCountdown = () => {
13 setIsLaunching(true);
14 let secondsLeft = 10;
15 setCountdown(secondsLeft);
16
17 timerRef.current = setInterval(() => {
18 secondsLeft -= 1;
19 setCountdown(secondsLeft);
20 if (secondsLeft === 0) {
21 stopTimer();
22 setIsLaunching(false);
23 onLaunch(); // Wywołanie funkcji z props po zakończeniu odliczania (poza funkcją aktualizującą stan)
24 }
25 }, 1000);
26 };
27
28 const abortLaunch = () => {
29 stopTimer(); // Najpierw zatrzymaj odliczanie
30 setIsLaunching(false);
31 setCountdown(10);
32 onAbort(); // Wywołanie funkcji z props przy przerwaniu
33 };
34
35 return (
36 <div className="mission-control">
37 <h2>Centrum kontroli misji</h2>
38 <div className="countdown">
39 {isLaunching
40 ? `Start za: ${countdown}`
41 : 'Gotowy do startu'}
42 </div>
43
44 <div className="control-buttons">
45 {!isLaunching ? (
46 <button onClick={startCountdown}>Rozpocznij odliczanie</button>
47 ) : (
48 <button onClick={abortLaunch} className="abort">Przerwij start</button>
49 )}
50 </div>
51 </div>
52 );
53}
54
55// Użycie komponentu
56function SpaceProgram() {
57 const handleLaunch = () => {
58 console.log('Rakieta wystartowała!');
59 // Logika po starcie...
60 };
61
62 const handleAbort = () => {
63 console.log('Start przerwany!');
64 // Logika po przerwaniu...
65 };
66
67 return (
68 <div className="space-program">
69 <h1>Program Kosmiczny</h1>
70 <MissionControl
71 onLaunch={handleLaunch}
72 onAbort={handleAbort}
73 />
74 </div>
75 );
76}Funkcja przekazana do setState (aktualizacja funkcyjna) musi być czysta: React w trybie deweloperskim (StrictMode) wywołuje ją dwa razy, więc wywołanie w niej onLaunch() uruchomiłoby rakietę dwukrotnie. Dlatego sekundy odliczamy w zwykłej zmiennej secondsLeft, a onLaunch() i onAbort() wywołujemy bezpośrednio w kodzie timera i handlera. Identyfikator timera trzymamy w useRef, żeby abortLaunch mógł go zatrzymać - useRef poznasz dokładniej przy komponentach niekontrolowanych w lekcji o przepływie danych.
Obsługa formularzy
Formularze są kluczowym elementem interakcji z użytkownikiem w aplikacjach webowych. React oferuje dwa podejścia do obsługi formularzy: kontrolowane i niekontrolowane komponenty.
Formularze kontrolowane
W kontrolowanych komponentach, dane formularza są kontrolowane przez stan React:
1function MissionSetup() {
2 const [missionData, setMissionData] = useState({
3 name: '',
4 destination: 'Moon',
5 crew: 3,
6 duration: 7
7 });
8
9 const handleChange = (e) => {
10 const { name, value } = e.target;
11 setMissionData({
12 ...missionData,
13 [name]: value
14 });
15 };
16
17 const handleSubmit = (e) => {
18 e.preventDefault();
19 console.log('Dane misji:', missionData);
20 // Logika wysyłania danych...
21 };
22
23 return (
24 <div className="mission-setup">
25 <h2>Konfiguracja misji</h2>
26
27 <form onSubmit={handleSubmit}>
28 <div className="form-group">
29 <label htmlFor="name">Nazwa misji:</label>
30 <input
31 type="text"
32 id="name"
33 name="name"
34 value={missionData.name}
35 onChange={handleChange}
36 required
37 />
38 </div>
39
40 <div className="form-group">
41 <label htmlFor="destination">Cel podróży:</label>
42 <select
43 id="destination"
44 name="destination"
45 value={missionData.destination}
46 onChange={handleChange}
47 >
48 <option value="Moon">Księżyc</option>
49 <option value="Mars">Mars</option>
50 <option value="ISS">Międzynarodowa Stacja Kosmiczna</option>
51 <option value="Asteroid">Pas asteroid</option>
52 </select>
53 </div>
54
55 <div className="form-group">
56 <label htmlFor="crew">Liczba załogantów:</label>
57 <input
58 type="number"
59 id="crew"
60 name="crew"
61 min="1"
62 max="10"
63 value={missionData.crew}
64 onChange={handleChange}
65 />
66 </div>
67
68 <div className="form-group">
69 <label htmlFor="duration">Czas trwania (dni):</label>
70 <input
71 type="number"
72 id="duration"
73 name="duration"
74 min="1"
75 value={missionData.duration}
76 onChange={handleChange}
77 />
78 </div>
79
80 <button type="submit">Zaplanuj misję</button>
81 </form>
82 </div>
83 );
84}W powyższym przykładzie:
- Każde pole formularza ma swój odpowiednik w stanie
- Wartość każdego pola jest kontrolowana przez stan
- Zdarzenie
onChangeaktualizuje stan, co z kolei aktualizuje wartość pola - Zdarzenie
onSubmitjest używane do obsługi przesyłania formularza
Obsługa wielu typów pól formularza
Różne pola formularza wymagają nieco innego podejścia:
1function AstronautRegistration() {
2 const [formData, setFormData] = useState({
3 name: '',
4 age: '',
5 specialty: 'pilot',
6 experience: '',
7 isTrainee: false,
8 bio: '',
9 preferredShift: 'morning'
10 });
11
12 const handleChange = (e) => {
13 const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
14 setFormData({
15 ...formData,
16 [e.target.name]: value
17 });
18 };
19
20 const handleSubmit = (e) => {
21 e.preventDefault();
22 console.log('Dane astronauty:', formData);
23 // Logika rejestracji...
24 };
25
26 return (
27 <div className="astronaut-registration">
28 <h2>Rejestracja astronauty</h2>
29
30 <form onSubmit={handleSubmit}>
31 {/* Pole tekstowe */}
32 <div className="form-group">
33 <label htmlFor="name">Imię i nazwisko:</label>
34 <input
35 type="text"
36 id="name"
37 name="name"
38 value={formData.name}
39 onChange={handleChange}
40 required
41 />
42 </div>
43
44 {/* Pole liczbowe */}
45 <div className="form-group">
46 <label htmlFor="age">Wiek:</label>
47 <input
48 type="number"
49 id="age"
50 name="age"
51 value={formData.age}
52 onChange={handleChange}
53 required
54 />
55 </div>
56
57 {/* Select */}
58 <div className="form-group">
59 <label htmlFor="specialty">Specjalizacja:</label>
60 <select
61 id="specialty"
62 name="specialty"
63 value={formData.specialty}
64 onChange={handleChange}
65 >
66 <option value="pilot">Pilot</option>
67 <option value="engineer">Inżynier</option>
68 <option value="scientist">Naukowiec</option>
69 <option value="doctor">Lekarz</option>
70 </select>
71 </div>
72
73 {/* Radio buttons */}
74 <div className="form-group">
75 <p>Preferowana zmiana:</p>
76 <label>
77 <input
78 type="radio"
79 name="preferredShift"
80 value="morning"
81 checked={formData.preferredShift === 'morning'}
82 onChange={handleChange}
83 />
84 Poranna
85 </label>
86 <label>
87 <input
88 type="radio"
89 name="preferredShift"
90 value="evening"
91 checked={formData.preferredShift === 'evening'}
92 onChange={handleChange}
93 />
94 Wieczorna
95 </label>
96 </div>
97
98 {/* Checkbox */}
99 <div className="form-group">
100 <label>
101 <input
102 type="checkbox"
103 name="isTrainee"
104 checked={formData.isTrainee}
105 onChange={handleChange}
106 />
107 Status praktykanta
108 </label>
109 </div>
110
111 {/* Textarea */}
112 <div className="form-group">
113 <label htmlFor="bio">Biografia:</label>
114 <textarea
115 id="bio"
116 name="bio"
117 value={formData.bio}
118 onChange={handleChange}
119 rows="4"
120 ></textarea>
121 </div>
122
123 {/* Range */}
124 <div className="form-group">
125 <label htmlFor="experience">Lata doświadczenia: {formData.experience}</label>
126 <input
127 type="range"
128 id="experience"
129 name="experience"
130 min="0"
131 max="30"
132 value={formData.experience}
133 onChange={handleChange}
134 />
135 </div>
136
137 <button type="submit">Zarejestruj</button>
138 </form>
139 </div>
140 );
141}Obsługa błędów i walidacja formularzy
W rzeczywistych aplikacjach, ważne jest, aby walidować dane wprowadzane przez użytkownika:
1function MissionPlanner() {
2 const [formData, setFormData] = useState({
3 missionName: '',
4 launchDate: '',
5 budget: ''
6 });
7
8 const [errors, setErrors] = useState({});
9
10 const validateForm = () => {
11 const newErrors = {};
12
13 // Walidacja nazwy misji
14 if (!formData.missionName.trim()) {
15 newErrors.missionName = 'Nazwa misji jest wymagana';
16 } else if (formData.missionName.length < 3) {
17 newErrors.missionName = 'Nazwa misji musi mieć co najmniej 3 znaki';
18 }
19
20 // Walidacja daty startu
21 if (!formData.launchDate) {
22 newErrors.launchDate = 'Data startu jest wymagana';
23 } else {
24 const selectedDate = new Date(formData.launchDate);
25 const today = new Date();
26
27 if (selectedDate <= today) {
28 newErrors.launchDate = 'Data startu musi być w przyszłości';
29 }
30 }
31
32 // Walidacja budżetu
33 if (!formData.budget) {
34 newErrors.budget = 'Budżet jest wymagany';
35 } else if (isNaN(formData.budget) || Number(formData.budget) <= 0) {
36 newErrors.budget = 'Budżet musi być liczbą dodatnią';
37 }
38
39 setErrors(newErrors);
40 return Object.keys(newErrors).length === 0; // Formularz jest prawidłowy, jeśli nie ma błędów
41 };
42
43 const handleChange = (e) => {
44 const { name, value } = e.target;
45 setFormData({
46 ...formData,
47 [name]: value
48 });
49
50 // Usuwanie błędu dla pola, które jest zmieniane
51 if (errors[name]) {
52 setErrors({
53 ...errors,
54 [name]: undefined
55 });
56 }
57 };
58
59 const handleSubmit = (e) => {
60 e.preventDefault();
61
62 if (validateForm()) {
63 console.log('Dane misji zostały wysłane:', formData);
64 // Logika wysyłania danych...
65 } else {
66 console.log('Formularz zawiera błędy');
67 }
68 };
69
70 return (
71 <div className="mission-planner">
72 <h2>Planowanie misji kosmicznej</h2>
73
74 <form onSubmit={handleSubmit}>
75 <div className="form-group">
76 <label htmlFor="missionName">Nazwa misji:</label>
77 <input
78 type="text"
79 id="missionName"
80 name="missionName"
81 value={formData.missionName}
82 onChange={handleChange}
83 className={errors.missionName ? 'error' : ''}
84 />
85 {errors.missionName && <div className="error-message">{errors.missionName}</div>}
86 </div>
87
88 <div className="form-group">
89 <label htmlFor="launchDate">Data startu:</label>
90 <input
91 type="date"
92 id="launchDate"
93 name="launchDate"
94 value={formData.launchDate}
95 onChange={handleChange}
96 className={errors.launchDate ? 'error' : ''}
97 />
98 {errors.launchDate && <div className="error-message">{errors.launchDate}</div>}
99 </div>
100
101 <div className="form-group">
102 <label htmlFor="budget">Budżet (w milionach $):</label>
103 <input
104 type="number"
105 id="budget"
106 name="budget"
107 value={formData.budget}
108 onChange={handleChange}
109 className={errors.budget ? 'error' : ''}
110 />
111 {errors.budget && <div className="error-message">{errors.budget}</div>}
112 </div>
113
114 <button type="submit">Zaplanuj misję</button>
115 </form>
116 </div>
117 );
118}Podsumowanie
Obsługa zdarzeń jest kluczowym elementem tworzenia interaktywnych aplikacji w React. Dzięki niej możemy:
- Reagować na działania użytkownika, takie jak kliknięcia, ruch myszy, czy wprowadzanie tekstu
- Aktualizować stan komponentu na podstawie interakcji
- Komunikować się między komponentami poprzez przekazywanie funkcji jako props
- Obsługiwać formularze i walidować dane wprowadzane przez użytkownika
Prawidłowa obsługa zdarzeń, w połączeniu z zarządzaniem stanem, pozwala tworzyć dynamiczne, responsywne interfejsy użytkownika, które zapewniają użytkownikom intuicyjne doświadczenie.
Kod do tej lekcji: App.jsx
1import React, { useState } from 'react';
2
3function EventDemo() {
4 const [clickCount, setClickCount] = useState(0);
5 const [inputText, setInputText] = useState('');
6 const [isHovered, setIsHovered] = useState(false);
7 const [selectedOption, setSelectedOption] = useState('Mars');
8
9 const handleButtonClick = () => {
10 setClickCount(clickCount + 1);
11 };
12
13 const handleInputChange = (event) => {
14 setInputText(event.target.value);
15 };
16
17 const handleSubmit = (event) => {
18 event.preventDefault();
19 alert(`Formularz wysłany z tekstem: "${inputText}"`);
20 };
21
22 const handleKeyDown = (event) => {
23 if (event.key === 'Enter') {
24 alert('Naciśnięto Enter!');
25 }
26 };
27
28 return (
29 <div className="event-demo">
30 <h1>Demo obsługi zdarzeń w React</h1>
31
32 <div className="demo-section">
33 <h2>Zdarzenia myszy</h2>
34 <button
35 onClick={handleButtonClick}
36 onMouseEnter={() => setIsHovered(true)}
37 onMouseLeave={() => setIsHovered(false)}
38 className={isHovered ? 'hovered' : ''}
39 >
40 Kliknij mnie! (Kliknięto: {clickCount} razy)
41 </button>
42 </div>
43
44 <div className="demo-section">
45 <h2>Zdarzenia formularza</h2>
46 <form onSubmit={handleSubmit}>
47 <input
48 type="text"
49 value={inputText}
50 onChange={handleInputChange}
51 onKeyDown={handleKeyDown}
52 placeholder="Wpisz tekst i naciśnij Enter lub wyślij formularz"
53 />
54 <select
55 value={selectedOption}
56 onChange={(e) => setSelectedOption(e.target.value)}
57 >
58 <option value="Mars">Mars</option>
59 <option value="Księżyc">Księżyc</option>
60 <option value="Wenus">Wenus</option>
61 <option value="Jowisz">Jowisz</option>
62 </select>
63 <button type="submit">Wyślij formularz</button>
64 </form>
65
66 <div className="output">
67 <p>Aktualny tekst: {inputText}</p>
68 <p>Wybrana opcja: {selectedOption}</p>
69 </div>
70 </div>
71
72 <div className="demo-section">
73 <h2>Zdarzenia dotyku i interakcji</h2>
74 <div
75 className="interactive-area"
76 onTouchStart={() => console.log('Dotyk rozpoczęty')}
77 onTouchEnd={() => console.log('Dotyk zakończony')}
78 onDoubleClick={() => alert('Podwójne kliknięcie!')}
79 >
80 Kliknij podwójnie lub dotknij (sprawdź konsolę)
81 </div>
82 </div>
83 </div>
84 );
85}
86
87function App() {
88 return (
89 <div className="app">
90 <EventDemo />
91 </div>
92 );
93}
94
95export default App;Widzisz błąd w tej lekcji?
Zadania praktyczne w grze
- Układanie w poziomie
Ułóż poprawną składnię aktualizacji stanu z użyciem poprzedniej wartości:
- Edytor kodu
Stwórz licznik tlenu: const [oxygen, setOxygen] = useState(21). Wyświetl tekst Poziom O2: {oxygen}% i dodaj dwa przyciski: przycisk + zwiększa poziom o 1 (<button onClick={() => setOxygen(prev => prev + 1)}>+</button>), a przycisk - zmniejsza go o 1. Poziom nie może wyjść poza zakres 0-100.
- Klikanie w kolejności
Zbuduj poprawny atrybut obsługi kliknięcia w JSX:
- Klikanie w kolejności
Ułóż proces używania useState: