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, який:
- Зберігає дані між рендерами
- При зміні -- автоматично перемальовує компонент
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 залежно від умов.
Корисні посилання:
- React.dev: State: A Component's Memory -- що таке state
- React.dev: useState -- API-довідник
- React.dev: Updating Objects in State -- immutability об'єктів
- React.dev: Updating Arrays in State -- immutability масивів