Реакт - это библиотека для разработки интерфейсов и только интерфейсов с виртуальным DOM
В реакте встроен только базовый стейт менеджмент, шаблонизатор и несколько базовых лайф сайкл хуков
- Не нужно использовать классовые компоненты, используйте только функциональные
- Реакт не является ангуляром, нет встроенного роутинга, нет встроенного стейт менеджмента, нет встроенного API
- Реакт не является фреймворком, он не навязывает какой-то один способ решения задач, он лишь предоставляет базовые инструменты для разработки интерфейсов
- React создает виртуальный DOM
- Когда что-то меняется, React сравнивает старый виртуальный DOM с новым и вычисляет, что именно изменилось
- React обновляет только то, что изменилось
src/examples/counter/Counter.tsx
import { useState } from 'react';
import style from './Counter.module.css';
export const Counter = () => {
const [count, setCount] = useState(0); // возвращает массив из двух элементов, первый элемент - значение стейта, второй элемент - функция для изменения стейта
return (
<div className={style.counter}>
<p>{count}</p>
<div className={style.btns}>
<button className={style.button} onClick={() => setCount(count + 1)}>+</button>
<button className={style.button} onClick={() => setCount(count - 1)}>-</button>
</div>
</div>
)
}В этом примере мы создали простой счетчик, который демонстрирует основные концепции React:
-
Создали функциональный компонент Counter - это основной строительный блок React-приложений. Компоненты позволяют разделить интерфейс на независимые, повторно используемые части.
-
Использовали хук useState для управления состоянием -
useState(0)создает переменную состоянияcountс начальным значением 0 и функциюsetCountдля её изменения. Когда значение меняется, React автоматически перерисовывает компонент. -
Добавили обработчики событий onClick - они вызывают функцию
setCountпри нажатии на кнопки, что изменяет состояние компонента. -
Вернули JSX-разметку - это специальный синтаксис, похожий на HTML, который описывает, как должен выглядеть компонент.
-
Применили CSS модули - это позволяет создавать локальные стили, которые не конфликтуют с другими компонентами.
CSS модули - это подход к стилизации компонентов в React, который решает проблему глобальной области видимости обычных CSS-стилей.
Как это работает:
- Создаете файл с расширением
.module.css(например,Counter.module.css) - Импортируете его в компонент:
import style from './Counter.module.css' - Используете классы через объект
style:className={style.counter}
Преимущества:
- Стили изолированы для каждого компонента
- Имена классов автоматически преобразуются в уникальные (например,
counterстановитсяCounter_counter__a1b2c3) - Нет конфликтов имен между разными компонентами
- Легко понять, какие стили относятся к какому компоненту
Пример CSS модуля:
/* Counter.module.css */
.counter {
display: flex;
flex-direction: column;
align-items: center;
}
.btns {
display: flex;
gap: 10px;
}
.button {
padding: 5px 10px;
border-radius: 4px;
background-color: #0077ff;
color: white;
border: none;
cursor: pointer;
}JSX (JavaScript XML) - это расширение синтаксиса JavaScript, которое позволяет писать HTML-подобный код внутри JavaScript.
Основные особенности:
- Выглядит как HTML, но на самом деле это JavaScript
- Позволяет легко описывать структуру UI компонентов
- Автоматически преобразуется в обычный JavaScript при сборке
Правила JSX:
- Каждый компонент должен возвращать один корневой элемент (или фрагмент
<>...</>) - Все теги должны быть закрыты (включая самозакрывающиеся, например
<img />) - JavaScript-выражения вставляются в фигурных скобках:
{count} - Атрибуты пишутся в camelCase:
onClickвместоonclick classзаменяется наclassName
Пример JSX:
return (
<div className={style.counter}>
<p>{count}</p>
<div className={style.btns}>
<button className={style.button} onClick={() => setCount(count + 1)}>+</button>
<button className={style.button} onClick={() => setCount(count - 1)}>-</button>
</div>
</div>
)Состояние - это данные, которые могут изменяться со временем и влияют на отображение компонента.
Ключевые моменты:
- Состояние принадлежит конкретному компоненту
- Когда состояние изменяется, компонент перерисовывается
- Состояние нельзя изменять напрямую, только через специальные функции (например,
setCount) - Состояние сохраняется между перерисовками компонента
Как работает useState:
const [count, setCount] = useState(0);useState(0)- создает состояние с начальным значением 0count- текущее значение состоянияsetCount- функция для изменения состояния- При вызове
setCount(newValue)React обновляет значение и перерисовывает компонент
Важно: Никогда не изменяйте состояние напрямую (count = 5), всегда используйте функцию setCount.
Хуки - это функции, которые позволяют "подключаться" к возможностям React (состоянию, эффектам и т.д.) в функциональных компонентах.
Основные хуки:
-
useState - управляет состоянием компонента
const [value, setValue] = useState(initialValue);
-
useEffect - выполняет побочные эффекты (запросы к API, подписки на события и т.д.)
useEffect(() => { // Код, который выполнится после рендера return () => { // Код, который выполнится при размонтировании компонента }; }, [dependencies]); // Массив зависимостей
-
useContext - получает доступ к контексту React
const value = useContext(MyContext);
-
useRef - создает ссылку, которая сохраняется между рендерами
const myRef = useRef(initialValue);
-
useMemo - мемоизирует вычисляемое значение
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
useCallback - мемоизирует функцию
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
Правила хуков:
- Хуки можно вызывать только на верхнем уровне функционального компонента (не внутри условий, циклов или вложенных функций)
- Хуки можно вызывать только из функциональных компонентов или других хуков
- Имена всех хуков начинаются с
use
Пример использования нескольких хуков:
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>Прошло секунд: {seconds}</div>;
}Формы - важная часть многих веб-приложений. В React есть два подхода к работе с формами: контролируемые и неконтролируемые компоненты. Рассмотрим пример контролируемой формы:
import { useState, FormEvent, ChangeEvent } from 'react';
import styles from './Form.module.css';
interface FormData {
name: string;
email: string;
message: string;
}
export const ContactForm = () => {
// Создаем состояние для хранения данных формы
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
message: ''
});
// Состояние для отображения сообщения об успешной отправке
const [isSubmitted, setIsSubmitted] = useState(false);
// Обработчик изменения полей ввода
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
// Обновляем состояние, сохраняя предыдущие значения и изменяя только нужное поле
setFormData(prevData => ({
...prevData,
[name]: value
}));
};
// Обработчик отправки формы
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // Предотвращаем стандартное поведение формы (перезагрузку страницы)
// Здесь обычно был бы код для отправки данных на сервер
console.log('Отправленные данные:', formData);
// Показываем сообщение об успешной отправке
setIsSubmitted(true);
// Сбрасываем форму через 3 секунды
setTimeout(() => {
setFormData({
name: '',
email: '',
message: ''
});
setIsSubmitted(false);
}, 3000);
};
return (
<div className={styles.formContainer}>
<h2>Свяжитесь с нами</h2>
{isSubmitted ? (
<div className={styles.successMessage}>
Спасибо! Ваше сообщение отправлено.
</div>
) : (
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.formGroup}>
<label htmlFor="name">Имя:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
className={styles.input}
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="email">Email:</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className={styles.input}
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="message">Сообщение:</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
required
className={styles.textarea}
rows={5}
/>
</div>
<button type="submit" className={styles.submitButton}>
Отправить
</button>
</form>
)}
</div>
);
};/* Form.module.css */
.formContainer {
max-width: 500px;
margin: 0 auto;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.form {
display: flex;
flex-direction: column;
gap: 15px;
}
.formGroup {
display: flex;
flex-direction: column;
gap: 5px;
}
.input, .textarea {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.textarea {
resize: vertical;
}
.submitButton {
padding: 12px;
background-color: #0077ff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.submitButton:hover {
background-color: #0055cc;
}
.successMessage {
padding: 20px;
text-align: center;
color: #2e7d32;
background-color: #e8f5e9;
border-radius: 4px;
font-size: 18px;
}-
Создали интерфейс для типизации данных формы - TypeScript помогает нам определить структуру данных формы.
-
Использовали useState для управления состоянием формы - Мы храним все поля формы в одном объекте состояния.
-
Создали обработчик изменений полей - Функция
handleChangeобновляет соответствующее поле в состоянии при вводе пользователя. -
Реализовали обработку отправки формы - Функция
handleSubmitпредотвращает стандартное поведение формы и обрабатывает данные. -
Добавили условный рендеринг - Показываем либо форму, либо сообщение об успешной отправке в зависимости от состояния.
-
Применили типизацию событий - Используем типы
ChangeEventиFormEventиз React для правильной типизации обработчиков событий. -
Стилизовали форму с помощью CSS модулей - Создали локальные стили, которые не конфликтуют с другими компонентами.
- Контролируемые компоненты - React контролирует значения полей ввода через состояние.
- Обработка событий - Используем обработчики
onChangeдля отслеживания ввода иonSubmitдля отправки формы. - Предотвращение перезагрузки страницы - Используем
e.preventDefault()в обработчике отправки. - Деструктуризация объектов - Используем
const { name, value } = e.targetдля удобного доступа к свойствам. - Динамическое обновление полей - Используем синтаксис
[name]: valueдля обновления нужного поля по его имени. - Типизация с TypeScript - Определяем интерфейсы для данных и типы для событий.
Этот пример демонстрирует основные принципы работы с формами в React и TypeScript, которые можно применить в большинстве проектов.
Zod - это библиотека для валидации схем с первоклассной поддержкой TypeScript. Она позволяет создавать сложные схемы валидации и автоматически выводить типы TypeScript из этих схем. Рассмотрим пример интеграции Zod с React Hook Form.
npm install react-hook-form @hookform/resolvers zodimport { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import styles from './ZodForm.module.css';
// Определение схемы валидации с помощью Zod
const userSchema = z.object({
username: z.string()
.min(3, 'Имя пользователя должно содержать минимум 3 символа')
.max(20, 'Имя пользователя должно содержать максимум 20 символов'),
email: z.string()
.email('Введите корректный email адрес'),
age: z.number({ invalid_type_error: 'Возраст должен быть числом' })
.int('Возраст должен быть целым числом')
.positive('Возраст должен быть положительным числом')
.min(18, 'Вам должно быть не менее 18 лет')
.max(120, 'Введите корректный возраст'),
website: z.string()
.url('Введите корректный URL')
.optional()
.or(z.literal('')),
password: z.string()
.min(8, 'Пароль должен содержать минимум 8 символов')
.regex(/[A-Z]/, 'Пароль должен содержать хотя бы одну заглавную букву')
.regex(/[0-9]/, 'Пароль должен содержать хотя бы одну цифру')
.regex(/[^A-Za-z0-9]/, 'Пароль должен содержать хотя бы один специальный символ'),
confirmPassword: z.string(),
terms: z.literal(true, {
errorMap: () => ({ message: 'Вы должны принять условия использования' })
})
}).refine(data => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword']
});
// Вывод типа из схемы Zod
type UserFormData = z.infer<typeof userSchema>;
export const ZodValidationForm = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset
} = useForm<UserFormData>({
resolver: zodResolver(userSchema),
defaultValues: {
username: '',
email: '',
age: undefined,
website: '',
password: '',
confirmPassword: '',
terms: false
}
});
const onSubmit = async (data: UserFormData) => {
// Имитация отправки данных на сервер
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('Отправленные данные:', data);
// Сброс формы после успешной отправки
reset();
// Здесь можно показать сообщение об успешной отправке
alert('Форма успешно отправлена!');
};
return (
<div className={styles.formContainer}>
<h2>Регистрация с Zod-валидацией</h2>
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
<div className={styles.formGroup}>
<label htmlFor="username">Имя пользователя:</label>
<input
id="username"
className={styles.input}
{...register('username')}
/>
{errors.username && (
<p className={styles.errorText}>{errors.username.message}</p>
)}
</div>
<div className={styles.formGroup}>
<label htmlFor="email">Email:</label>
<input
id="email"
className={styles.input}
type="email"
{...register('email')}
/>
{errors.email && (
<p className={styles.errorText}>{errors.email.message}</p>
)}
</div>
<div className={styles.formGroup}>
<label htmlFor="age">Возраст:</label>
<input
id="age"
className={styles.input}
type="number"
{...register('age', { valueAsNumber: true })}
/>
{errors.age && (
<p className={styles.errorText}>{errors.age.message}</p>
)}
</div>
<div className={styles.formGroup}>
<label htmlFor="website">Веб-сайт (необязательно):</label>
<input
id="website"
className={styles.input}
type="url"
{...register('website')}
/>
{errors.website && (
<p className={styles.errorText}>{errors.website.message}</p>
)}
</div>
<div className={styles.formGroup}>
<label htmlFor="password">Пароль:</label>
<input
id="password"
className={styles.input}
type="password"
{...register('password')}
/>
{errors.password && (
<p className={styles.errorText}>{errors.password.message}</p>
)}
</div>
<div className={styles.formGroup}>
<label htmlFor="confirmPassword">Подтверждение пароля:</label>
<input
id="confirmPassword"
className={styles.input}
type="password"
{...register('confirmPassword')}
/>
{errors.confirmPassword && (
<p className={styles.errorText}>{errors.confirmPassword.message}</p>
)}
</div>
<div className={styles.checkboxGroup}>
<input
id="terms"
type="checkbox"
{...register('terms')}
/>
<label htmlFor="terms">Я принимаю условия использования</label>
{errors.terms && (
<p className={styles.errorText}>{errors.terms.message}</p>
)}
</div>
<button
type="submit"
className={styles.submitButton}
disabled={isSubmitting}
>
{isSubmitting ? 'Отправка...' : 'Зарегистрироваться'}
</button>
</form>
</div>
);
};/* ZodForm.module.css */
.formContainer {
max-width: 500px;
margin: 0 auto;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
.form {
display: flex;
flex-direction: column;
gap: 15px;
}
.formGroup {
display: flex;
flex-direction: column;
gap: 5px;
}
.input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.input:focus {
outline: none;
border-color: #0077ff;
box-shadow: 0 0 0 2px rgba(0, 119, 255, 0.2);
}
.checkboxGroup {
display: flex;
align-items: flex-start;
gap: 10px;
position: relative;
padding-bottom: 20px;
}
.checkboxGroup input {
margin-top: 3px;
}
.checkboxGroup .errorText {
position: absolute;
bottom: 0;
left: 0;
}
.errorText {
color: #d32f2f;
font-size: 14px;
margin: 4px 0 0;
}
.submitButton {
padding: 12px;
background-color: #0077ff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.submitButton:hover:not(:disabled) {
background-color: #0055cc;
}
.submitButton:disabled {
background-color: #cccccc;
cursor: not-allowed;
}Zod автоматически выводит типы TypeScript из схемы валидации, что обеспечивает полную типобезопасность вашей формы. Тип UserFormData создается автоматически из схемы userSchema.
Zod позволяет описывать правила валидации в декларативном стиле, что делает код более читаемым и поддерживаемым.
С помощью метода .refine() можно создавать сложные правила валидации, которые зависят от нескольких полей (например, проверка совпадения паролей).
Zod поддерживает валидацию вложенных объектов и массивов, что позволяет работать со сложными структурами данных.
Zod может не только валидировать, но и трансформировать данные, например, преобразовывать строки в числа или даты.
-
Определение схемы:
const schema = z.object({ field: z.string().min(3) });
-
Интеграция с React Hook Form:
const { register, handleSubmit } = useForm({ resolver: zodResolver(schema) });
-
Вывод типов из схемы:
type FormData = z.infer<typeof schema>;
-
Сложные правила валидации:
schema.refine(data => condition, { message: 'Сообщение об ошибке', path: ['fieldName'] // Поле, к которому относится ошибка })
-
Условная валидация:
z.string().optional().or(z.literal(''))
Использование Zod с React Hook Form значительно упрощает валидацию форм в React-приложениях, обеспечивая надежную типизацию и выразительный API для определения правил валидации.
Одна из ключевых концепций React - это композиция компонентов. Разделение приложения на небольшие, переиспользуемые компоненты делает код более организованным, поддерживаемым и тестируемым. Рассмотрим пример приложения списка задач (Todo List), разделенного на компоненты.
src/
├── components/
│ ├── TodoApp/
│ │ ├── TodoApp.tsx
│ │ └── TodoApp.module.css
│ ├── TodoList/
│ │ ├── TodoList.tsx
│ │ └── TodoList.module.css
│ ├── TodoItem/
│ │ ├── TodoItem.tsx
│ │ └── TodoItem.module.css
│ ├── TodoForm/
│ │ ├── TodoForm.tsx
│ │ └── TodoForm.module.css
│ └── TodoFilter/
│ ├── TodoFilter.tsx
│ └── TodoFilter.module.css
├── types/
│ └── todo.ts
├── App.tsx
└── main.tsx
// src/types/todo.ts
export interface Todo {
id: number;
text: string;
completed: boolean;
}
export type TodoFilter = 'all' | 'active' | 'completed';// src/components/TodoApp/TodoApp.tsx
import { useState, useCallback } from 'react';
import { TodoList } from '../TodoList/TodoList';
import { TodoForm } from '../TodoForm/TodoForm';
import { TodoFilter } from '../TodoFilter/TodoFilter';
import { Todo, TodoFilter as FilterType } from '../../types/todo';
import styles from './TodoApp.module.css';
export const TodoApp = () => {
// Состояние для хранения списка задач
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: 'Изучить React', completed: true },
{ id: 2, text: 'Изучить TypeScript', completed: false },
{ id: 3, text: 'Создать приложение', completed: false }
]);
// Состояние для текущего фильтра
const [filter, setFilter] = useState<FilterType>('all');
// Функция для добавления новой задачи
const addTodo = useCallback((text: string) => {
setTodos(prevTodos => [
...prevTodos,
{
id: Date.now(),
text,
completed: false
}
]);
}, []);
// Функция для переключения статуса задачи
const toggleTodo = useCallback((id: number) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
// Функция для удаления задачи
const deleteTodo = useCallback((id: number) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
}, []);
// Фильтрация задач в зависимости от выбранного фильтра
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true; // 'all'
});
return (
<div className={styles.todoApp}>
<h1>Список задач</h1>
<TodoForm onAddTodo={addTodo} />
<TodoFilter
currentFilter={filter}
onFilterChange={setFilter}
/>
<TodoList
todos={filteredTodos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
<div className={styles.stats}>
<p>Всего задач: {todos.length}</p>
<p>Выполнено: {todos.filter(todo => todo.completed).length}</p>
<p>Осталось: {todos.filter(todo => !todo.completed).length}</p>
</div>
</div>
);
};// src/components/TodoList/TodoList.tsx
import { memo } from 'react';
import { TodoItem } from '../TodoItem/TodoItem';
import { Todo } from '../../types/todo';
import styles from './TodoList.module.css';
interface TodoListProps {
todos: Todo[];
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
export const TodoList = memo(({ todos, onToggle, onDelete }: TodoListProps) => {
if (todos.length === 0) {
return <p className={styles.emptyMessage}>Нет задач для отображения</p>;
}
return (
<ul className={styles.todoList}>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
});
TodoList.displayName = 'TodoList';// src/components/TodoItem/TodoItem.tsx
import { memo } from 'react';
import { Todo } from '../../types/todo';
import styles from './TodoItem.module.css';
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
export const TodoItem = memo(({ todo, onToggle, onDelete }: TodoItemProps) => {
return (
<li className={`${styles.todoItem} ${todo.completed ? styles.completed : ''}`}>
<label className={styles.todoLabel}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className={styles.todoCheckbox}
/>
<span className={styles.todoText}>{todo.text}</span>
</label>
<button
onClick={() => onDelete(todo.id)}
className={styles.deleteButton}
aria-label="Удалить задачу"
>
✖
</button>
</li>
);
});
TodoItem.displayName = 'TodoItem';// src/components/TodoForm/TodoForm.tsx
import { useState, FormEvent, ChangeEvent, memo } from 'react';
import styles from './TodoForm.module.css';
interface TodoFormProps {
onAddTodo: (text: string) => void;
}
export const TodoForm = memo(({ onAddTodo }: TodoFormProps) => {
const [text, setText] = useState('');
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (text.trim()) {
onAddTodo(text.trim());
setText('');
}
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
return (
<form onSubmit={handleSubmit} className={styles.todoForm}>
<input
type="text"
value={text}
onChange={handleChange}
placeholder="Что нужно сделать?"
className={styles.todoInput}
/>
<button
type="submit"
className={styles.addButton}
disabled={!text.trim()}
>
Добавить
</button>
</form>
);
});
TodoForm.displayName = 'TodoForm';// src/components/TodoFilter/TodoFilter.tsx
import { memo } from 'react';
import { TodoFilter as FilterType } from '../../types/todo';
import styles from './TodoFilter.module.css';
interface TodoFilterProps {
currentFilter: FilterType;
onFilterChange: (filter: FilterType) => void;
}
export const TodoFilter = memo(({ currentFilter, onFilterChange }: TodoFilterProps) => {
return (
<div className={styles.filterContainer}>
<button
className={`${styles.filterButton} ${currentFilter === 'all' ? styles.active : ''}`}
onClick={() => onFilterChange('all')}
>
Все
</button>
<button
className={`${styles.filterButton} ${currentFilter === 'active' ? styles.active : ''}`}
onClick={() => onFilterChange('active')}
>
Активные
</button>
<button
className={`${styles.filterButton} ${currentFilter === 'completed' ? styles.active : ''}`}
onClick={() => onFilterChange('completed')}
>
Выполненные
</button>
</div>
);
});
TodoFilter.displayName = 'TodoFilter';/* src/components/TodoApp/TodoApp.module.css */
.todoApp {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.stats {
margin-top: 20px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
display: flex;
justify-content: space-between;
}
/* src/components/TodoList/TodoList.module.css */
.todoList {
list-style: none;
padding: 0;
margin: 20px 0;
}
.emptyMessage {
text-align: center;
color: #888;
font-style: italic;
margin: 20px 0;
}
/* src/components/TodoItem/TodoItem.module.css */
.todoItem {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.todoLabel {
display: flex;
align-items: center;
flex-grow: 1;
cursor: pointer;
}
.todoCheckbox {
margin-right: 10px;
}
.todoText {
font-size: 16px;
}
.completed .todoText {
text-decoration: line-through;
color: #888;
}
.deleteButton {
background: none;
border: none;
color: #f44336;
cursor: pointer;
font-size: 18px;
padding: 0 5px;
}
/* src/components/TodoForm/TodoForm.module.css */
.todoForm {
display: flex;
margin-bottom: 20px;
}
.todoInput {
flex-grow: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px 0 0 4px;
font-size: 16px;
}
.addButton {
padding: 10px 15px;
background-color: #0077ff;
color: white;
border: none;
border-radius: 0 4px 4px 0;
cursor: pointer;
font-size: 16px;
}
.addButton:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
/* src/components/TodoFilter/TodoFilter.module.css */
.filterContainer {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.filterButton {
padding: 8px 12px;
margin: 0 5px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.filterButton.active {
background-color: #0077ff;
color: white;
border-color: #0077ff;
}// src/App.tsx
import { TodoApp } from './components/TodoApp/TodoApp';
function App() {
return (
<div className="App">
<TodoApp />
</div>
);
}
export default App;-
Принцип единственной ответственности
- Каждый компонент должен выполнять только одну задачу
- Например,
TodoItemотвечает только за отображение одной задачи
-
Композиция компонентов
- Сложные компоненты строятся из более простых
TodoAppсодержитTodoList, который содержит несколькоTodoItem
-
Передача данных через props
- Родительские компоненты передают данные дочерним через props
- Дочерние компоненты уведомляют родителей о событиях через функции обратного вызова
-
Мемоизация компонентов
- Использование
memoдля предотвращения ненужных перерисовок - Особенно важно для компонентов, которые часто перерисовываются
- Использование
-
Локальное состояние
- Состояние хранится на самом верхнем уровне, где оно необходимо
- Например, состояние всех задач хранится в
TodoApp
-
Типизация с TypeScript
- Определение интерфейсов для props каждого компонента
- Определение типов данных в отдельных файлах для переиспользования
-
Модульные стили
- Каждый компонент имеет свой CSS-модуль
- Стили изолированы и не влияют на другие компоненты
- Повторное использование - компоненты можно использовать в разных частях приложения
- Тестируемость - легче писать модульные тесты для небольших компонентов
- Поддерживаемость - изменения в одном компоненте не влияют на другие
- Разделение ответственности - разные разработчики могут работать над разными компонентами
- Производительность - мемоизация и оптимизация отдельных компонентов
- Читаемость - код становится более понятным и структурированным
Этот пример демонстрирует, как разделить приложение на компоненты, следуя лучшим практикам React и TypeScript.