Модуль 16

Хуки: useState та useEffect

Ось готовий урок у стилі David Malan / CS50, створений спеціально для тебе. Пристебни паски безпеки, ми занурюємось у світ React Hooks! 🚀


🎓 CS50: React Hooks — Магія useState та useEffect

Привіт, світе! 👋 Це CS50, і сьогодні ми розберемо тему, яка перетворює "мертвий" код на живий, інтерактивний організм.

Ми говоримо про Хуки (Hooks). Зокрема, про двох китів, на яких тримається сучасний React: useState та useEffect.


1. 🔥 Вступ: Проблема "Амнезії" Функцій

Уявіть собі ситуацію. Ви приходите в кав’ярню, замовляєте капучино. Бариста робить каву, віддає вам, ви йдете. Ви повертаєтесь через хвилину і кажете: "Я хочу ще цукру". А бариста дивиться на вас порожніми очима і питає: "А ви хто?".

Проблема: Звичайні функції в JavaScript мають амнезію. Як тільки функція виконалася (відмалювала компонент) і завершила роботу — вона забуває все, що всередині неї відбувалося. Всі змінні знищуються.

Але зачекайте! ✋ Коли ми пишемо додаток (наприклад, Instagram), і ви ставите лайк ❤️, додаток має запам'ятати, що лайк стоїть, навіть якщо ви проскролили стрічку туди-сюди.

Тут виникає два питання: 1. Як змусити функцію "пам'ятати" дані між оновленнями екрана? 2. Як змусити функцію реагувати на зміни (наприклад, завантажити нові пости, коли ви догортали до низу)?

Без цього ваш сайт — це просто красива, але дурна картинка. Хуки — це імпланти пам'яті та реакції для ваших функцій.


2. 🧠 Теоретична база (Що під капотом?)

Давайте розберемо це без складних слів, як для друзів.

📍 1. useState — Це рюкзак пам'яті

Уявіть, що ваш компонент — це мандрівник. Кожного разу, коли React оновлює екран, мандрівник перероджується (функція запускається заново). Але useState дає йому рюкзак, який передається від минулої версії мандрівника до нової.

Синтаксис:

const [count, setCount] = useState(0);
  • count (змінна): Те, що лежить у рюкзаку зараз.
  • setCount (функція): Єдиний спосіб покласти щось нове в рюкзак.
  • 0 (аргумент): Що покласти в рюкзак на самому початку (початкове значення).

👉 Важливо зрозуміти: Ви не можете просто написати count = 5. React про це не дізнається. Тільки setCount(5) "смикає" React і каже: "Ей! Дані змінилися! Перемалюй екран!".


📍 2. useEffect — Це ваш особистий асистент

Якщо useState — це пам'ять, то useEffect — це дії, які виходять за межі простого відображення HTML. Це побічні ефекти (side effects).

Аналогія: Ви (компонент) зайшли в кімнату (відрендерились). Що далі? * Може, треба увімкнути кондиціонер? (Запит на сервер). * Може, треба підписатися на газету? (Event Listener). * Може, треба змінити заголовок сторінки?

Як це працює:

useEffect(() => {
  // Дія, яку треба зробити
}, [ /* Масив залежностей */ ]);

Масив залежностей (Dependency Array) — це найважливіше! Це пульт керування асистентом: * [] (порожній): "Зроби це один раз, коли я тільки з'явився (mount)". * [count]: "Роби це кожного разу, коли змінюється змінна count". * (без масиву): "Роби це кожного разу, коли щось відбувається" (Обережно! Це може покласти ваш браузер).


3. 🧪 Приклади (Step-by-Step)

Приклад 1: Лічильник лайків (useState)

Давайте почнемо з бази. Ми хочемо кнопку, яка рахує кліки.

Питання до вас: Що буде, якщо ми просто створимо змінну let likes = 0 всередині компонента і будемо її збільшувати? (Спойлер: На екрані нічого не зміниться, бо React не стежить за звичайними змінними).

Ось як правильно:

import { useState } from 'react';

function LikeButton() {
  // 1. Оголошуємо стан
  const [likes, setLikes] = useState(0);

  function handleClick() {
    // 2. Змінюємо стан
    setLikes(likes + 1);
  }

  return (
    <button onClick={handleClick}>
      ❤️ Лайків: {likes}
    </button>
  );
}

Результат: Кожен клік змушує React запустити функцію LikeButton знову, але likes тепер буде 1, потім 2, і так далі.


Приклад 2: "Шпигун" за заголовком (useEffect)

Тепер уявімо, що ми хочемо, щоб кількість лайків відображалася у вкладці браузера (document.title).

Це зміна зовнішнього світу (браузера), а не самого React-компонента. Це робота для useEffect.

import { useState, useEffect } from 'react';

function LikeSpy() {
  const [likes, setLikes] = useState(0);

  // Цей код спрацьовує КОЖЕН раз, коли змінюється likes
  useEffect(() => {
    document.title = `Лайків: ${likes}`;
    console.log("Заголовок оновлено!");
  }, [likes]); // <--- Дивіться на масив залежностей!

  return (
    <button onClick={() => setLikes(likes + 1)}>
      Клікни мене ({likes})
    </button>
  );
}

Чому так? Ми сказали React: "Слухай, як тільки зміниться змінна likes, будь ласка, піди і онови заголовок вкладки".


Приклад 3: Таймер зі зворотним відліком (Складніше + Cleanup)

А що, якщо ми пішли з кімнати (видалили компонент), а асистент продовжує працювати? Це витік пам'яті!

Нам потрібна функція очищення (cleanup function). Це як вимкнути світло перед виходом.

import { useState, useEffect } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    // Запускаємо інтервал
    const intervalId = setInterval(() => {
      setSeconds(s => s + 1);
      console.log('Тік-так...');
    }, 1000);

    // 🧹 Функція очищення (спрацює, коли компонент зникне)
    return () => {
      clearInterval(intervalId);
      console.log('Таймер зупинено!');
    };
  }, []); // Порожній масив = запускаємо 1 раз на старті

  return <h1>Минуло секунд: {seconds}</h1>;
}

4. 🛠 Практична частина

Час забруднити руки кодом! Відкривайте редактор (або CodeSandbox).

🔹 Завдання 1: Input Mirror Створіть input. Використовуйте useState, щоб текст, який ви вводите, одразу відображався в параграфі <p> під інпутом.

🔹 Завдання 2: Toggle (Перемикач) Зробіть кнопку "Показати/Приховати". При кліку текст під нею має зникати або з'являтися. (Підказка: useState(true) або false, і умовний рендеринг show && <Text />).

🔹 Завдання 3: Виправити баг Студент написав такий код для useEffect, але він створює нескінченний цикл і вішає браузер. Чому? Виправте це.

useEffect(() => {
  setCount(count + 1); // Ой-ой...
}); // Чого тут не вистачає?

🔹 Завдання 4: Міні-кейс "Курс валют" Зробіть компонент, який при завантаженні (один раз!) виводить у консоль "Отримання даних...", а через 2 секунди (використайте setTimeout як імітацію запиту) змінює текст на екрані з "Завантаження..." на "1 USD = 41 UAH".

🔹 Завдання 5: А що, якщо... Що станеться, якщо у масиві залежностей useEffect вказати змінну, яка ніколи не змінюється? Перевірте свою гіпотезу.


5. 💡 Мислення як у розробника

Як думає Senior Developer, коли працює з хуками?

  1. "Чи потрібно це в стейті?" Новачки пхають у useState все підряд.

    • Правило: Якщо значення можна вирахувати з інших пропсів або стейту — не створюйте для нього окремий стейт.
    • Приклад: Є firstName і lastName. Не робіть стейт fullName. Просто напишіть const fullName = firstName + ' ' + lastName прямо в тілі функції.
  2. "Синхронізація, а не події" Не думайте про useEffect як про "щось, що стається після кліку". Думайте про це як про синхронізацію.

    • "Я хочу, щоб заголовок сторінки завжди був синхронізований з кількістю лайків".
  3. Обережніше з об'єктами useState({ name: 'David', age: 30 }). Якщо ви хочете змінити тільки вік, не забудьте скопіювати старі дані: setUser({ ...user, age: 31 }). Якщо ви просто напишете setUser({ age: 31 }), ім'я зникне!


6. 🧩 Підсумок

Отже, що ми сьогодні поклали у свій ментальний рюкзак:

  1. useState — це пам'ять компонента. Змінили стейт ➡️ React оновив картинку.
  2. useEffect — це міст до зовнішнього світу (API, таймери, DOM).
  3. Масив залежностей — це гальма для useEffect. Забули про них — отримали хаос.

Тепер ви вмієте створювати компоненти, які реагують на користувача і живуть своїм життям.

👀 Тизер: Але що, якщо нам треба передати дані через 10 компонентів вниз? Передавати пропси вручну — це пекло (prop drilling). На наступному занятті ми розглянемо Context API — телепорт для ваших даних.

А поки що — це був CS50. Практикуйтесь!

(Код закривається, світло гасне)