Небольшое SPA для управления списком задач. Проект используется как учебное приложение для отработки архитектуры React-приложения, серверного состояния, маршрутизации, типизации, тестирования и оптимизации клиентского бандла.
Демо: https://sergey-bondarenko-dev.github.io/todo-react
- добавление, выполнение и удаление задач;
- удаление всех задач через доступный диалог подтверждения;
- поиск с регистронезависимой и безопасной HTML-подсветкой совпадений;
- статистика выполненных задач;
- состояния первоначальной загрузки и фонового обновления списка;
- отображение ошибок запросов, повторная загрузка и уведомления об ошибках изменений;
- прокрутка к первой незавершённой задаче;
- отдельная страница задачи по маршруту
/tasks/:id; - обработка неизвестных маршрутов;
- анимация добавления и удаления элементов;
- два источника данных:
json-serverиlocalStorage.
- React 19 и TypeScript;
- Vite 7;
- React Router 8;
- Redux Toolkit, RTK Query и React Redux;
- Motion с
LazyMotion; - Radix UI;
- Sonner;
- SCSS Modules;
- ESLint и Stylelint;
- Vitest, React Testing Library и jest-dom;
- MSW и jsdom;
- Playwright;
- json-server;
- gh-pages;
- rollup-plugin-visualizer.
Код организован по принципам, близким к Feature-Sliced Design:
e2e/ # браузерные сквозные сценарии Playwright
src/
├── app/ # store, маршрутизация и глобальные стили
├── pages/ # страницы списка и отдельной задачи
├── widgets/ # композиция основного Todo-интерфейса
├── features/ # добавление, поиск и статистика
├── entities/ # RTK Query API и UI задачи
├── shared/ # API-адаптеры, UI-kit, типы, утилиты и ресурсы
└── test/ # общая настройка тестов, MSW server и Redux helpers
Компоненты не работают с хранилищем данных напрямую. Они используют сгенерированные RTK Query hooks, а endpoints обращаются к единому taskRepository. Репозиторий выбирает серверную или локальную реализацию через переменную окружения.
tasksApi описывает запросы списка и отдельной задачи, а также мутации добавления, переключения состояния и удаления. При добавлении задачи RTK Query ждёт ответ репозитория и пессимистично добавляет созданную задачу в кэш списка без повторного запроса. Переключение состояния оптимистично обновляет кэши списка и отдельной задачи, а при ошибке откатывает оба изменения. Инвалидация тегов синхронизирует данные после успешного переключения и удаления.
В Redux store зарегистрированы reducer и middleware RTK Query. Локальными React-состояниями остаются только UI-данные, например поисковая строка и значение формы.
HTTP-адаптер проверяет успешность ответа и во время выполнения валидирует структуру полученных задач. Ошибки HTTP, сети, JSON и структуры ответа приводятся к единому сериализуемому формату. Основной список и страница задачи различают первоначальную загрузку, фоновое обновление, ошибку и успешное состояние. При ошибке загрузку можно повторить, а уже полученные данные сохраняются на экране при неудачном фоновом запросе.
Ошибки добавления, изменения и удаления задач показываются через toast-уведомления. В статическом режиме отдельно обрабатываются недоступность localStorage и превышение его квоты. Удаление всех задач подтверждается с помощью доступного диалога на базе Radix UI.
Приложение использует declarative API React Router:
/— список задач;/tasks/:id— подробности задачи;*— страница 404.
BrowserRouter получает basename из import.meta.env.BASE_URL. Production-сборка использует /todo-react, поэтому маршруты корректно работают в подпапке GitHub Pages. Перед деплоем index.html копируется в 404.html, чтобы прямые переходы на вложенные маршруты возвращали SPA.
По умолчанию приложение обращается к json-server по адресу http://localhost:3001/tasks.
Если VITE_STATIC_BACKEND=true, используется адаптер localStorage. Оба адаптера реализуют один контракт TasksApi, поэтому RTK Query и UI не зависят от способа хранения. В .env.production статический режим включён автоматически.
Перед передачей строки в dangerouslySetInnerHTML текст задачи экранируется как HTML, а поисковый запрос — как часть регулярного выражения. После этого найденные фрагменты оборачиваются в <mark>.
Добавление и удаление TodoItem анимируются через AnimatePresence и облегчённый компонент m. Удаление начинается только после успешной мутации: RTK Query обновляет список, а AnimatePresence удерживает DOM-узел до завершения exit-анимации.
Вместо полного motion используется LazyMotion с динамически импортируемым domAnimation. Это выносит animation features в отдельный чанк и уменьшает начальный JavaScript. Настройка prefers-reduced-motion учитывается через Motion hook и глобальные стили.
Для просмотра состава бандла используется интерактивный treemap с raw, gzip и Brotli-размерами:
npm run build:analyzeОтчёт создаётся в dist/stats.html. Обычная production-сборка визуализатор не подключает.
Тесты запускаются через Vitest в окружении jsdom. Компонентные сценарии написаны с React Testing Library и user-event: проверки выполняются через доступные роли, подписи и наблюдаемое пользователем поведение.
MSW перехватывает HTTP-запросы на сетевой границе, поэтому интеграционные тесты используют реальные RTK Query endpoints, Redux store и API-адаптер вместо моков импортированных модулей. Для каждого теста создаётся отдельный store, а обработчики MSW сбрасываются между сценариями.
Браузерные e2e-тесты запускаются через Playwright в Chromium. Playwright автоматически поднимает Vite со статическим адаптером, а каждый сценарий выполняется в изолированном browser context со своим localStorage.
Тестами покрыты:
- безопасная подсветка поиска и базовые UI-компоненты;
- валидация и отправка формы добавления задачи;
- чтение, runtime-валидация и мутации
localStorage-репозитория; - запросы, нормализация ошибок, инвалидация и ручное обновление кэша RTK Query;
- загрузка, поиск, повторный запрос, переключение и удаление задач;
- подтверждение и отмена удаления всех задач;
- ошибки мутаций и toast-уведомления;
- маршрутизация и страница отдельной задачи;
- сохранение добавления, выполнения и удаления задачи после перезагрузки страницы;
- переход на страницу задачи и отображение неизвестного маршрута в реальном браузере.
Однократный запуск всего набора:
npm testРежим наблюдения во время разработки:
npm run test:watchЗапуск e2e-тестов в headless Chromium:
npm run test:e2eИнтерактивный режим Playwright:
npm run test:e2e:ui- Node.js 22.22 или новее;
- npm.
Установить зависимости:
npm installУстановить Chromium для запуска e2e-тестов:
npx playwright install chromiumДля разработки с HTTP API запустите два процесса.
Терминал 1:
npm run serverТерминал 2:
npm run devDev-сервер будет доступен по адресу, который выведет Vite. json-server запускается на http://localhost:3001.
PowerShell:
$env:VITE_STATIC_BACKEND='true'; npm run devLinux/macOS:
VITE_STATIC_BACKEND=true npm run devВ этом режиме задачи сохраняются в localStorage текущего браузера.
| Команда | Назначение |
|---|---|
npm run dev |
Запустить Vite dev server |
npm run server |
Запустить json-server на порту 3001 |
npm run build |
Собрать production-версию |
npm run build:analyze |
Собрать приложение и создать dist/stats.html |
npm run preview |
Локально открыть production-сборку |
npm run typecheck |
Проверить TypeScript |
npm run lint |
Проверить TypeScript/TSX через ESLint |
npm run lint:styles |
Проверить CSS/SCSS через Stylelint |
npm test |
Однократно запустить тесты через Vitest |
npm run test:watch |
Запустить Vitest в режиме наблюдения |
npm run test:e2e |
Запустить e2e-тесты через Playwright в headless Chromium |
npm run test:e2e:ui |
Открыть интерактивный режим Playwright |
npm run deploy |
Собрать и опубликовать приложение через gh-pages |
npm run typecheck
npm run lint
npm run lint:styles
npm test
npm run test:e2e
npm run build