Вивчай

State та useState

React State та useState — ряд перемикачів на панелі деякі увімкнені деякі вимкненіReact State та useState — ряд перемикачів на панелі деякі увімкнені деякі вимкнені

У попередньому уроці ми створювали компоненти та передавали їм дані через props. Але props -- це дані від батька, і компонент не може їх змінювати. А що, якщо потрібно реагувати на дії користувача -- клік, введення тексту, перемикання? Для цього існує state.


Навіщо потрібен state?

Спробуємо зробити лічильник без state:

function Counter() {
  let count = 0

  function handleClick() {
    count += 1
    console.log(count) // число зростає в консолі...
  }

  return (
    <div>
      <p>Лічильник: {count}</p>
      <button onClick={handleClick}>+1</button>
    </div>
  )
}

Проблема: count змінюється в пам'яті, але React не знає про це і не перемальовує компонент. На екрані завжди буде 0.

State -- це спеціальний механізм React, який:

  1. Зберігає дані між рендерами
  2. При зміні -- автоматично перемальовує компонент

useState -- базовий хук

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>Лічильник: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>Скинути</button>
    </div>
  )
}

Розберемо синтаксис

const [count, setCount] = useState(0)
//     ^       ^                  ^
//     |       |                  |
//     |       setter-функція     початкове значення
//     поточне значення
  • useState(0) -- створює state зі стартовим значенням 0
  • Повертає масив з двох елементів: [значення, функціяОновлення]
  • count -- поточне значення
  • setCount -- функція для оновлення (convention: set + назва змінної)
Увага

Ніколи не змінюй state напряму: count = 5 або count++. Завжди використовуй setter-функцію setCount(5). Тільки так React дізнається про зміну та перемалює компонент.


Кілька useState в одному компоненті

Компонент може мати стільки state-змінних, скільки потрібно:

import { useState } from 'react'

function UserForm() {
  const [name, setName] = useState('')
  const [age, setAge] = useState(0)
  const [isStudent, setIsStudent] = useState(false)

  return (
    <div>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Ім'я"
      />
      <input
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
        placeholder="Вік"
      />
      <label>
        <input
          type="checkbox"
          checked={isStudent}
          onChange={(e) => setIsStudent(e.target.checked)}
        />
        Студент
      </label>
      <p>
        {name}, {age} років, {isStudent ? 'студент' : 'не студент'}
      </p>
    </div>
  )
}

Типізація useState

TypeScript автоматично визначає тип зі стартового значення:

const [count, setCount] = useState(0)          // number
const [name, setName] = useState('')            // string
const [isOpen, setIsOpen] = useState(false)     // boolean

Але іноді потрібно вказати тип явно:

// Коли початкове значення null
const [user, setUser] = useState<string | null>(null)

// Для масивів
const [items, setItems] = useState<string[]>([])

// Для об'єктів
interface User {
  name: string
  email: string
}
const [user, setUser] = useState<User | null>(null)

Оновлення масивів (імутабельність)

React вимагає імутабельності (immutability) -- не змінюй (не мутуй) state напряму, а створюй нову копію. Якщо ти пропустив урок про мутабельність -- раджу повернутися, бо це фундамент роботи зі state в React.

Додавання елемента

const [items, setItems] = useState<string[]>(['React', 'TypeScript'])

// ПОГАНО -- мутація!
items.push('Vite')
setItems(items)

// ДОБРЕ -- новий масив
setItems([...items, 'Vite'])

Видалення елемента

// Видалити за індексом
setItems(items.filter((_, index) => index !== 2))

// Видалити за значенням
setItems(items.filter((item) => item !== 'TypeScript'))

Оновлення елемента

// Замінити елемент за індексом
setItems(items.map((item, index) =>
  index === 1 ? 'Vue' : item
))

Сортування

// sort мутує масив, тому спочатку копіюємо
setItems([...items].sort())

Оновлення об'єктів (імутабельність)

interface User {
  name: string
  email: string
  age: number
}

const [user, setUser] = useState<User>({
  name: 'Олексій',
  email: 'alex@test.com',
  age: 25,
})

// ПОГАНО -- мутація!
user.name = 'Марія'
setUser(user)

// ДОБРЕ -- новий об'єкт
setUser({ ...user, name: 'Марія' })

// Оновити кілька полів
setUser({ ...user, name: 'Марія', age: 30 })
Інфо

Чому імутабельність? React порівнює старий та новий state за посиланням (як ми вивчили в уроці про мутабельність). Якщо ти мутуєш об'єкт, посилання не змінюється, і React думає, що нічого не змінилось. Новий об'єкт через spread -- це нове посилання, і React перемалює компонент.


Functional updates (prev => ...)

Коли нове значення залежить від попереднього, використовуй функцію:

function Counter() {
  const [count, setCount] = useState(0)

  function handleTripleIncrement() {
    // ПОГАНО -- всі три виклики "бачать" одне й те саме count
    setCount(count + 1)
    setCount(count + 1)
    setCount(count + 1)
    // Результат: count + 1, а не count + 3!

    // ДОБРЕ -- кожен виклик отримує актуальне попереднє значення
    setCount(prev => prev + 1)
    setCount(prev => prev + 1)
    setCount(prev => prev + 1)
    // Результат: count + 3
  }

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleTripleIncrement}>+3</button>
    </div>
  )
}
Порада

Правило: якщо нове значення залежить від попереднього -- використовуй функціональну форму setCount(prev => prev + 1). Якщо не залежить -- можна напряму setCount(5).


Практика: список завдань

Поєднаємо все вивчене в одному прикладі:

import { useState } from 'react'

interface Todo {
  id: number
  text: string
  done: boolean
}

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([])
  const [input, setInput] = useState('')

  function addTodo() {
    if (!input.trim()) return

    const newTodo: Todo = {
      id: Date.now(),
      text: input.trim(),
      done: false,
    }
    setTodos(prev => [...prev, newTodo])
    setInput('')
  }

  function toggleTodo(id: number) {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    )
  }

  function removeTodo(id: number) {
    setTodos(prev => prev.filter(todo => todo.id !== id))
  }

  return (
    <div>
      <h1>Мої завдання ({todos.length})</h1>

      <div>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Нове завдання..."
        />
        <button onClick={addTodo}>Додати</button>
      </div>

      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <span
              style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
              onClick={() => toggleTodo(todo.id)}
            >
              {todo.text}
            </span>
            <button onClick={() => removeTodo(todo.id)}>Видалити</button>
          </li>
        ))}
      </ul>
    </div>
  )
}

export default TodoList

Підсумок

  • State -- дані компонента, що при зміні викликають перемальовування
  • useState(initialValue) -- повертає [значення, setЗначення]
  • Оновлюй state тільки через setter-функцію, не мутуй напряму
  • Імутабельність: для масивів -- spread, filter, map; для об'єктів -- spread
  • Functional updates (prev => ...) -- коли нове значення залежить від попереднього
  • Компонент може мати скільки завгодно useState

Що далі?

Ми вже вміємо створювати компоненти, передавати їм дані та керувати станом. У наступному уроці навчимось рендерити списки та робити умовний рендеринг -- показувати різний UI залежно від умов.

Інфо

Корисні посилання: