Эта лекция охватывает современные подходы к разработке React-приложений с использованием TypeScript. Мы рассмотрим компоненты, JSX, композицию, типизацию props, управление состоянием с помощью хуков useState и useEffect.
Простой компонент:
import React from 'react';
function Welcome() {
return <h1>Добро пожаловать в React!</h1>;
}
export default Welcome;
Компоненты — это обычные JavaScript-функции, которые принимают props в качестве аргумента и возвращают React-элементы (JSX).
Преимущества компонентов:
JSX (JavaScript XML) — это синтаксическое расширение JavaScript, позволяющее писать HTML-подобную разметку внутри JavaScript-кода.
Основы JSX:
import React from 'react';
function Greeting() {
const name = 'Анна';
const isLoggedIn = true;
return (
<div>
<h1>Привет, {name}!</h1>
<p>Результат: {2 + 2}</p>
{isLoggedIn ? <p>Вы вошли в систему</p> : <p>Пожалуйста, войдите</p>}
</div>
);
}
export default Greeting;
Ключевые правила JSX:
{} для JavaScript-выражений]className вместо class, onClick вместо onclick- Закрывайте все теги, включая самозакрывающиеся: `<img />`, `<br />`
Выражения в JSX:
Внутри JSX можно использовать любые JavaScript-выражения: переменные, вызовы функций, математические операции, тернарные операторы. Нельзя использовать операторы if или циклы for напрямую — для этого нужно использовать тернарный оператор, логическое И (&&), или метод map() для списков.
// Условный рендеринг
{isLoggedIn && <p>Добро пожаловать!</p>}
// Списки
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
Композиция — это фундаментальный принцип React, позволяющий создавать сложные интерфейсы из небольших, переиспользуемых компонентов. React предпочитает композицию наследованию.
Пример композиции:
import React from 'react';
function Button({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
return <button onClick={onClick}>{children}</button>;
}
function Card({ title, content }: { title: string; content: string }) {
return (
<div className="card">
<h2>{title}</h2>
<p>{content}</p>
</div>
);
}
function App() {
const handleClick = () => console.log('Кнопка нажата!');
return (
<div>
<Card
title="Заголовок карточки"
content="Это содержимое карточки"
/>
<Button onClick={handleClick}>Нажми меня</Button>
</div>
);
}
export default App;
Паттерн контейнерных компонентов:
Контейнерные компоненты используют специальный prop children для композиции. Это позволяет создавать гибкие обертки:
function Container({ children }: { children: React.ReactNode }) {
return <div className="container">{children}</div>;
}
// Использование
<Container>
<h1>Заголовок</h1>
<p>Содержимое</p>
</Container>
Принципы хорошей композиции:
TypeScript обеспечивает типобезопасность и помогает отловить ошибки на этапе разработки. Для типизации props используются типы (type).
Типизация props:
import React from 'react';
type UserProps = {
name: string;
age: number;
isActive: boolean;
hobbies: string[];
}
function UserProfile({ name, age, isActive, hobbies }: UserProps) {
return (
<div className="user-profile">
<h2>{name}</h2>
<p>Возраст: {age}</p>
<p>Статус: {isActive ? 'Активен' : 'Неактивен'}</p>
<ul>
{hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
</div>
);
}
export default UserProfile;
Типы для props:
string, number, booleanstring[], number[]() => void, (id: number) => voidОператор ? в TypeScript делает свойство необязательным. Это полезно, когда не все props обязательны для работы компонента.
Необязательные props:
type ButtonProps = {
text: string; // обязательный
variant?: 'primary' | 'secondary'; // необязательный
disabled?: boolean; // необязательный
onClick?: () => void; // необязательный
}
function Button({
text,
variant,
disabled,
onClick
}: ButtonProps) {
return (
<button
className={"btn"}
disabled={disabled}
onClick={onClick}
>
{text}
</button>
);
}
useState — это хук, позволяющий добавить состояние в компоненты. Он возвращает массив из двух элементов: текущее значение состояния и функцию для его обновления.
Базовое использование:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState<number>(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<h2>Счетчик: {count}</h2>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
</div>
);
}
export default Counter;
Синтаксис:
const [state, setState] = useState<Type>(initialValue);
state — текущее значение состоянияsetState — функция для обновления состоянияinitialValue — начальное значениеВажные моменты:
setState не изменяет состояние немедленно — изменение произойдет при следующем рендереsetState планирует повторный рендер компонентаTypeScript обеспечивает типобезопасность при работе с состоянием.
Примитивные типы:
// TypeScript автоматически выводит тип
const [count, setCount] = useState(0); // number
const [text, setText] = useState(''); // string
const [isActive, setIsActive] = useState(false); // boolean
// Явная типизация
const [count, setCount] = useState<number>(0);
const [name, setName] = useState<string>('');
Сложные типы:
type User = {
id: number;
name: string;
email: string;
}
// Состояние может быть null или User
const [user, setUser] = useState<User | null>(null);
// Массив объектов
const [users, setUsers] = useState<User[]>([]);
// Объект с несколькими полями
type FormData = {
username: string;
email: string;
age: number;
}
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
age: 0
});
TypeScript предупредит вас, если вы попытаетесь установить значение неправильного типа.
Компонент может иметь несколько состояний, управляемых разными useState хуками.
Множественные независимые состояния:
function UserForm() {
const [username, setUsername] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [age, setAge] = useState<number>(0);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
// Используйте состояния независимо
}
Связанные данные в одном объекте:
interface FormData {
username: string;
email: string;
age: number;
}
function UserForm() {
const [formData, setFormData] = useState<FormData>({
username: '',
email: '',
age: 0
});
const updateFormData = (field: keyof FormData, value: string | number) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
return (
<form>
<input
type="text"
value={formData.username}
onChange={(e) => updateFormData('username', e.target.value)}
/>
{/* другие поля */}
</form>
);
}
Рекомендации:
useState для независимых значенийuseState с объектом для связанных данныхЖизненный цикл компонента React с хуком useEffect
Базовый синтаксис useEffect:
useEffect(() => {
// Код эффекта
return () => {
// Функция очистки (опционально)
};
}, [dependencies]);
Три варианта работы useEffect:
useEffect(() => {
console.log('Компонент смонтирован');
}, []); // Пустой массив зависимостей
useEffect(() => {
console.log('Count изменился:', count);
}, [count]); // Эффект зависит от count
useEffect(() => {
console.log('Компонент отрендерился');
}); // Без массива зависимостей
Функция очистки:
useEffect(() => {
const interval = setInterval(() => {
console.log('Тик');
}, 1000);
// Очистка перед размонтированием
return () => {
clearInterval(interval);
};
}, []);
Функция очистки вызывается перед размонтированием компонента или перед повторным выполнением эффекта.
Соединим все изученные концепции в одном приложении:
import React, { useState } from 'react';
// Интерфейсы
type Task = {
id: number;
title: string;
completed: boolean;
}
type TaskItemProps = {
task: Task;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
type TaskListProps = {
tasks: Task[];
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
type AddTaskFormProps = {
onAdd: (title: string) => void;
}
// Компонент элемента задачи
function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
return (
<div>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggle(task.id)}
/>
<span>{task.title}</span>
<button onClick={() => onDelete(task.id)}>Удалить</button>
</div>
);
}
// Компонент списка задач
function TaskList({ tasks, onToggle, onDelete }: TaskListProps) {
if (tasks.length === 0) {
return <p>Нет задач</p>;
}
return (
<div className="task-list">
{tasks.map(task => (
<TaskItem
key={task.id}
task={task}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</div>
);
}
// Компонент формы добавления задачи
function AddTaskForm({ onAdd }: AddTaskFormProps) {
const [title, setTitle] = useState<string>('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (title.trim()) {
onAdd(title);
setTitle('');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Новая задача"
/>
<button type="submit">Добавить</button>
</form>
);
}
// Главный компонент
function TodoApp() {
const [tasks, setTasks] = useState<Task[]>([
{ id: 1, title: 'Изучить React', completed: false },
{ id: 2, title: 'Изучить TypeScript', completed: true },
]);
const addTask = (title: string) => {
const newTask: Task = {
id: Date.now(),
title,
completed: false
};
setTasks(prev => [...prev, newTask]);
};
const toggleTask = (id: number) => {
setTasks(prev =>
prev.map(task =>
task.id === id ? { id: task.id, title: task.title, completed: !task.completed } : task
)
);
};
const deleteTask = (id: number) => {
setTasks(prev => prev.filter(task => task.id !== id));
};
const completedCount = tasks.filter(t => t.completed).length;
return (
<div>
<h1>Список задач</h1>
<p>Выполнено: {completedCount} из {tasks.length}</p>
<AddTaskForm onAdd={addTask} />
<TaskList tasks={tasks} onToggle={toggleTask} onDelete={deleteTask}/>
</div>
);
}
export default TodoApp;