Skip to content

DKprofile/react-octo-example

Repository files navigation

Краткий вводный курс по реакту с тайпскриптом

Что реакт вообще такое

Реакт - это библиотека для разработки интерфейсов и только интерфейсов с виртуальным DOM

В реакте встроен только базовый стейт менеджмент, шаблонизатор и несколько базовых лайф сайкл хуков

Что нужно понимать

  1. Не нужно использовать классовые компоненты, используйте только функциональные
  2. Реакт не является ангуляром, нет встроенного роутинга, нет встроенного стейт менеджмента, нет встроенного API
  3. Реакт не является фреймворком, он не навязывает какой-то один способ решения задач, он лишь предоставляет базовые инструменты для разработки интерфейсов

Как работает react?

  1. React создает виртуальный DOM
  2. Когда что-то меняется, React сравнивает старый виртуальный DOM с новым и вычисляет, что именно изменилось
  3. 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:

  1. Создали функциональный компонент Counter - это основной строительный блок React-приложений. Компоненты позволяют разделить интерфейс на независимые, повторно используемые части.

  2. Использовали хук useState для управления состоянием - useState(0) создает переменную состояния count с начальным значением 0 и функцию setCount для её изменения. Когда значение меняется, React автоматически перерисовывает компонент.

  3. Добавили обработчики событий onClick - они вызывают функцию setCount при нажатии на кнопки, что изменяет состояние компонента.

  4. Вернули JSX-разметку - это специальный синтаксис, похожий на HTML, который описывает, как должен выглядеть компонент.

  5. Применили CSS модули - это позволяет создавать локальные стили, которые не конфликтуют с другими компонентами.

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

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>
)

Состояние (State)

Состояние - это данные, которые могут изменяться со временем и влияют на отображение компонента.

Ключевые моменты:

  • Состояние принадлежит конкретному компоненту
  • Когда состояние изменяется, компонент перерисовывается
  • Состояние нельзя изменять напрямую, только через специальные функции (например, setCount)
  • Состояние сохраняется между перерисовками компонента

Как работает useState:

const [count, setCount] = useState(0);
  • useState(0) - создает состояние с начальным значением 0
  • count - текущее значение состояния
  • setCount - функция для изменения состояния
  • При вызове setCount(newValue) React обновляет значение и перерисовывает компонент

Важно: Никогда не изменяйте состояние напрямую (count = 5), всегда используйте функцию setCount.

Хуки (Hooks)

Хуки - это функции, которые позволяют "подключаться" к возможностям React (состоянию, эффектам и т.д.) в функциональных компонентах.

Основные хуки:

  1. useState - управляет состоянием компонента

    const [value, setValue] = useState(initialValue);
  2. useEffect - выполняет побочные эффекты (запросы к API, подписки на события и т.д.)

    useEffect(() => {
      // Код, который выполнится после рендера
      return () => {
        // Код, который выполнится при размонтировании компонента
      };
    }, [dependencies]); // Массив зависимостей
  3. useContext - получает доступ к контексту React

    const value = useContext(MyContext);
  4. useRef - создает ссылку, которая сохраняется между рендерами

    const myRef = useRef(initialValue);
  5. useMemo - мемоизирует вычисляемое значение

    const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  6. 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

Формы - важная часть многих веб-приложений. В 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>
  );
};

CSS для формы:

/* 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;
}

Что мы сделали в этом примере:

  1. Создали интерфейс для типизации данных формы - TypeScript помогает нам определить структуру данных формы.

  2. Использовали useState для управления состоянием формы - Мы храним все поля формы в одном объекте состояния.

  3. Создали обработчик изменений полей - Функция handleChange обновляет соответствующее поле в состоянии при вводе пользователя.

  4. Реализовали обработку отправки формы - Функция handleSubmit предотвращает стандартное поведение формы и обрабатывает данные.

  5. Добавили условный рендеринг - Показываем либо форму, либо сообщение об успешной отправке в зависимости от состояния.

  6. Применили типизацию событий - Используем типы ChangeEvent и FormEvent из React для правильной типизации обработчиков событий.

  7. Стилизовали форму с помощью CSS модулей - Создали локальные стили, которые не конфликтуют с другими компонентами.

Ключевые моменты при работе с формами в React:

  • Контролируемые компоненты - React контролирует значения полей ввода через состояние.
  • Обработка событий - Используем обработчики onChange для отслеживания ввода и onSubmit для отправки формы.
  • Предотвращение перезагрузки страницы - Используем e.preventDefault() в обработчике отправки.
  • Деструктуризация объектов - Используем const { name, value } = e.target для удобного доступа к свойствам.
  • Динамическое обновление полей - Используем синтаксис [name]: value для обновления нужного поля по его имени.
  • Типизация с TypeScript - Определяем интерфейсы для данных и типы для событий.

Этот пример демонстрирует основные принципы работы с формами в React и TypeScript, которые можно применить в большинстве проектов.

Валидация форм с помощью Zod и React Hook Form

Zod - это библиотека для валидации схем с первоклассной поддержкой TypeScript. Она позволяет создавать сложные схемы валидации и автоматически выводить типы TypeScript из этих схем. Рассмотрим пример интеграции Zod с React Hook Form.

Установка необходимых пакетов

npm install react-hook-form @hookform/resolvers zod

Пример формы с Zod-валидацией

import { 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>
  );
};

CSS для формы с Zod-валидацией:

/* 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 с React Hook Form:

1. Типобезопасность

Zod автоматически выводит типы TypeScript из схемы валидации, что обеспечивает полную типобезопасность вашей формы. Тип UserFormData создается автоматически из схемы userSchema.

2. Декларативная валидация

Zod позволяет описывать правила валидации в декларативном стиле, что делает код более читаемым и поддерживаемым.

3. Сложные правила валидации

С помощью метода .refine() можно создавать сложные правила валидации, которые зависят от нескольких полей (например, проверка совпадения паролей).

4. Вложенные объекты и массивы

Zod поддерживает валидацию вложенных объектов и массивов, что позволяет работать со сложными структурами данных.

5. Трансформация данных

Zod может не только валидировать, но и трансформировать данные, например, преобразовывать строки в числа или даты.

Ключевые моменты при использовании Zod с React Hook Form:

  1. Определение схемы:

    const schema = z.object({
      field: z.string().min(3)
    });
  2. Интеграция с React Hook Form:

    const { register, handleSubmit } = useForm({
      resolver: zodResolver(schema)
    });
  3. Вывод типов из схемы:

    type FormData = z.infer<typeof schema>;
  4. Сложные правила валидации:

    schema.refine(data => condition, {
      message: 'Сообщение об ошибке',
      path: ['fieldName'] // Поле, к которому относится ошибка
    })
  5. Условная валидация:

    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';

CSS для компонентов

/* 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;
}

Использование в App.tsx

// src/App.tsx
import { TodoApp } from './components/TodoApp/TodoApp';

function App() {
  return (
    <div className="App">
      <TodoApp />
    </div>
  );
}

export default App;

Ключевые принципы разделения на компоненты:

  1. Принцип единственной ответственности

    • Каждый компонент должен выполнять только одну задачу
    • Например, TodoItem отвечает только за отображение одной задачи
  2. Композиция компонентов

    • Сложные компоненты строятся из более простых
    • TodoApp содержит TodoList, который содержит несколько TodoItem
  3. Передача данных через props

    • Родительские компоненты передают данные дочерним через props
    • Дочерние компоненты уведомляют родителей о событиях через функции обратного вызова
  4. Мемоизация компонентов

    • Использование memo для предотвращения ненужных перерисовок
    • Особенно важно для компонентов, которые часто перерисовываются
  5. Локальное состояние

    • Состояние хранится на самом верхнем уровне, где оно необходимо
    • Например, состояние всех задач хранится в TodoApp
  6. Типизация с TypeScript

    • Определение интерфейсов для props каждого компонента
    • Определение типов данных в отдельных файлах для переиспользования
  7. Модульные стили

    • Каждый компонент имеет свой CSS-модуль
    • Стили изолированы и не влияют на другие компоненты

Преимущества такого подхода:

  • Повторное использование - компоненты можно использовать в разных частях приложения
  • Тестируемость - легче писать модульные тесты для небольших компонентов
  • Поддерживаемость - изменения в одном компоненте не влияют на другие
  • Разделение ответственности - разные разработчики могут работать над разными компонентами
  • Производительность - мемоизация и оптимизация отдельных компонентов
  • Читаемость - код становится более понятным и структурированным

Этот пример демонстрирует, как разделить приложение на компоненты, следуя лучшим практикам React и TypeScript.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors