Главная / Статьи / Готовая к производству база React: что на самом деле делает каждый пакет

Готовая к производству база React: что на самом деле делает каждый пакет

Настройте Vite, Tailwind v4, Redux Toolkit, React Router, Jest и Prettier для React-приложения, а также поймите, зачем нужен каждый из этих пакетов и каждая строка конфигурации.

3165 слов

Выполнение команды npm create vite создает React-приложение, которое отображается на экране, но это не то приложение, которое можно было бы передать реальным пользователям: в нем нет системы стилизации, общего состояния, механизма навигации, тестов и унифицированного формата кода. В этом руководстве пошагово создается эта недостающая основа с использованием Tailwind CSS, Redux Toolkit, React Router, Jest вместе с React Testing Library и Prettier. Для каждого пакета задаются два вопроса: что он на самом деле делает и что сломается, если его не использовать? К концу вы получите рабочую основу для разработки новых функций, а также сможете читать свой собственный файл package.json и объяснять каждую строку в нем.

Краткий обзор используемых инструментов:

  • Tailwind CSS — для стилизации
  • Redux Toolkit — для хранения общих данных приложения
  • React Router — для навигации между страницами
  • Jest, React Testing Library и небольшая инструментальная сборка Babel, необходимая для запуска тестов
  • Prettier, чтобы форматирование больше не зависело от личных предпочтений
  • Некоторые из этих инструментов устанавливаются одной командой. Другие скрывают удивительные детали; например, React Testing Library на самом деле представляет собой три отдельных пакета с разными функциями.

    Начните с чистого проекта Vite, используя шаблон React + TypeScript:

    npm create vite@latest react-production-stack -- --template react-ts
    cd react-production-stack
    npm install
    

    Tailwind CSS: стилизация задаётся сразу

    Пакеты: tailwindcss, @tailwindcss/vite

    Поскольку стилизация влияет на каждый компонент, имеет смысл убедиться, что она работает, прежде чем добавлять что-либо ещё.

    npm install tailwindcss @tailwindcss/vite
    

    Это устанавливает оба пакета в качестве обычных зависимостей, а не devDependencies. Строго говоря, ни один из пакетов не выполняется в браузере: плагин Vite выполняет свою работу во время сборки, и только генерируемый CSS попадает в финальный бандл для продакшена. Поэтому многие команды помещают их в категорию devDependencies, и для одностраничного приложения в бандле любой из вариантов приводит к одинаковому результату. Выберите одну конвенцию и соблюдайте её последовательно.

    Далее зарегистрируйте плагин вместе с React-плагином в конфигурации Vite:

    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    import tailwindcss from '@tailwindcss/vite'
    
    export default defineConfig({
      plugins: [react(), tailwindcss()],
    })
    

    Затем замените содержимое src/index.css на один импорт. Вот весь файл:

    /* Tailwind v4 is CSS-first. No config file, no content globs. */
    @import 'tailwindcss';
    

    На самом деле это и всё необходимое для настройки. Tailwind v4 использует подход CSS-first: здесь нет файла tailwind.config.js, а также нет списка глобальных элементов, поскольку он самостоятельно сканирует исходные файлы в поиске имён классов.

    Проверка работы

    Временно добавьте несколько классов-утилит к заголовку в файле App.tsx, например text-3xl font-bold text-blue-600, запустите команду npm run dev и проверьте, изменился ли заголовок. Если да, значит плагин и импорт CSS связаны между собой.

    Почему утилиты вместо отдельных таблиц стилей

    Tailwind размещает стили непосредственно в маркиупе, на которое они влияют. При использовании отдельного файла CSS легко изменить компонент, забыв про соответствующую таблицу стилей, в результате чего постепенно накапливаются бесполезные и устаревшие правила. Особенно часто на панелях управления повторяются одни и те же элементы (карточки, значки, кнопки), и использование общего набора утилит позволяет сохранять визуальную целостность при меньшем объеме кода для обслуживания. Компромисс заключается в необходимости изменения привычек: вместо создания собственных имен классов, таких как .card-header-active, каждый элемент формируется из небольших, заранее определенных классов.

    Redux Toolkit: хранилище данных и мост к React

    Пакеты: @reduxjs/toolkit, react-redux

    Эти два пакета легко спутать, но они выполняют разные функции:

    • @reduxjs/toolkit — это само хранилище данных: оно хранит данные приложения и применяет к ним обновления.
    • react-redux — это соединение с React: он предоставляет компонент <Provider> и хуки, которые используются компонентами для чтения и обновления этих данных.

    Оба пакета необходимы, поскольку ни один из них не может выполнять функции другого.

    npm install @reduxjs/toolkit react-redux
    

    Создайте хранилище данных в файле src/app/store.ts. Оно начинается с пустого словаря редьюсеров и экспортирует два типа, производных от хранилища, чтобы остальная часть приложения не нуждалась в их ручном определении:

    import { configureStore } from '@reduxjs/toolkit'
    
    export const store = configureStore({
      reducer: {},
    })
    
    export type RootState = ReturnType<typeof store.getState>
    export type AppDispatch = typeof store.dispatch
    

    Объект reducer: {} пока остаётся пустым. Слайсы будут добавлены тогда, когда появятся реальные функции, такие как проекты или задачи; нет смысла создавать состояние до того, как оно понадобится на каком-либо экране.

    Затем определяются типизированные хуки в файле src/app/hooks.ts. Вспомогательные функции withTypes, доступные в новых версиях React Redux, один раз связывают useDispatch и useSelector с типами вашего хранилища, благодаря чему компоненты получают полную типизацию без необходимости аннотировать каждый вызов:

    import { useDispatch, useSelector } from 'react-redux'
    import type { AppDispatch, RootState } from './store'
    
    export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
    export const useAppSelector = useSelector.withTypes<RootState>()
    

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

    На этом этапе хранилище уже существует, но React о нём не знает. Тег <Provider> делает его доступным для всех компонентов, находящихся ниже него, поэтому он размещается в самом верху дерева в файле src/main.tsx:

    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import { Provider } from 'react-redux'
    import { store } from './app/store'
    import App from './App'
    import './index.css'
    
    createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <Provider store={store}>
          <App />
        </Provider>
      </StrictMode>,
    )
    

    Теперь любой элемент, отображаемый внутри <Provider>, может вызывать функции useAppSelector и useAppDispatch.

    Проверка корректной работы

    Запустите приложение и убедитесь, что страница продолжает отображаться без ошибки "could not find react-redux context". Эта ошибка появляется каждый раз, когда компонент использует хуки Redux вне контекста Provider. При отсутствии данных в хранилище пока нечего тестировать.

    Когда использовать Redux, а когда достаточно useState

    Не всё должно находиться в Redux, и сохранение всего состояния в хранилище так же ошибочно, как и хранение всего локально. Практическое правило:

    • useState — для данных, важных только для одного экрана или компонента: открыта ли модальная вкладка, каково текущее значение поля ввода, какой выбран вариант в выпадающем меню.
  • Redux используется для хранения данных, необходимых одновременно нескольким экранам или компонентам, таких как список задач, отображаемый на нескольких страницах, или отдельная задача, которая появляется на панели управления, в списке задач и в виде деталей задачи.
  • Если какое-либо состояние иначе пришлось бы передавать через несколько уровней или дублировать между экранами, это хороший признак того, что оно должно находиться в хранилище состояния.

    React Router: маршрутизация до появления первой реальной страницы

    Пакет: react-router

    Внедрение маршрутизации до появления любой реальной страницы может показаться преждевременным, но это быстро окупается: каждый новый экран становится еще одним элементом <Route>, вместо того чтобы позже приходилось перестраивать всё приложение.

    npm install react-router
    

    Разместите таблицу маршрутов в отдельном модуле src/routes/AppRoutes.tsx. В настоящее время она сопоставляет / с компонентом-заменителем, оформленным с помощью инструментов Tailwind:

    import { Route, Routes } from 'react-router'
    
    function Placeholder() {
      return (
        <div className="flex min-h-screen items-center justify-center">
          <p className="text-slate-600">Routes coming soon</p>
        </div>
      )
    }
    
    export function AppRoutes() {
      return (
        <Routes>
          <Route path="/" element={<Placeholder />} />
        </Routes>
      )
    }
    

    Затем src/App.tsx просто отображает эту таблицу маршрутов:

    import { AppRoutes } from './routes/AppRoutes'
    
    function App() {
      return <AppRoutes />
    }
    
    export default App
    

    Наконец, оберните приложение в <BrowserRouter> внутри src/main.tsx, рядом с провайдером Redux:

    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import { Provider } from 'react-redux'
    import { BrowserRouter } from 'react-router'
    import { store } from './app/store'
    import App from './App'
    import './index.css'
    
    createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <Provider store={store}>
          <BrowserRouter>
            <App />
          </BrowserRouter>
        </Provider>
      </StrictMode>,
    )
    

    Результативная цепочка выглядит так: main.tsx<App /><AppRoutes /> → тот <Route>, который соответствует URL. Redux и роутер являются независимыми, поэтому порядок их вложения не имеет значения; единственное требование — чтобы оба оборачивали <App>.

    Проверка работы

    Запустите npm run dev и откройте /. Если появляется текст-заменитель, значит <BrowserRouter>, <Routes> и <Route> правильно настроены.

    Jest и React Testing Library: четыре задачи, одиннадцать пакетов

    Пакеты: jest, @testing-library/react, babel-jest и ещё несколько

    Этот этап занимает больше всего времени. Сами концепции не сложны, но «добавление тестов» на самом деле означает установку примерно одиннадцати пакетов, выполняющих четыре разных функции, а затем пройдение хотя бы одного теста с использованием роутера. Группировка пакетов по задачам значительно упрощает понимание процесса.

    Группа А: запускатель тестов и имитированный браузер

    npm install -D jest jest-environment-jsdom
    
    • jest — это инструмент для запуска тестов. Он находит файлы с расширением *.test.tsx, выполняет их и сообщает о результатах успешного/неудачного прохождения. Без него ничто в этом разделе не сможет работать.
    • jest-environment-jsdom необходим, поскольку Jest работает в среде Node, где отсутствует объект document. Этот пакет предоставляет имитацию DOM, чтобы компоненты могли отображаться на экране.

    Группа B: React Testing Library состоит из трех пакетов

    npm install -D @testing-library/react
    npm install -D @testing-library/jest-dom
    npm install -D @testing-library/user-event
    

    То, что люди называют «React Testing Library», на самом деле представляет собой три отдельных библиотеки, каждая из которых выполняет свою функцию:

    • @testing-library/react отображает компонент на имитированной странице и предоставляет инструменты для поиска, такие как screen.getByText(...).
  • @testing-library/jest-dom предоставляет удобные функции сравнения, такие как toBeInTheDocument(), что позволяет избежать ручного сравнения результатов запроса с null.
  • @testing-library/user-event имитирует реалистичное поведение пользователя. Ввод текста генерирует полную последовательность событий фокусировки, нажатия клавиши, ввода и отпускания клавиши, как это происходит в браузере, вместо отправки одного синтетического события на элемент.
  • Короче говоря: отрисовка, проверка и взаимодействие. Три задачи, три пакета, и почти всегда они вам нужны все.

    Группа C: инструменты Babel, позволяющие Jest читать TSX

    Эта группа существует по одной причине: Jest сам по себе не может понимать файлы TypeScript или JSX.

    • babel-jest связывает эти два инструмента. Jest передает каждый файл через Babel перед его выполнением.
    • @babel/preset-typescript удаляет аннотации типов. Он не проверяет типы; он просто удаляет синтаксис вида : string и подобные.
    • @babel/preset-react компилирует JSX в обычные вызовы функций.
    • @babel/preset-env преобразует современный синтаксис в то, что поддерживает ваша версия Node.

    В отличие от группы B, эти пакеты устанавливаются вместе одним командным приказом. Все предустановки требуют совместимого @babel/core, и постепенная установка их в проекте, уже содержащем Jest (который подключает собственные зависимости Babel), может привести к тому, что npm будет пытаться согласовать несовместимые версии. Одним из проявлений этого является ошибка ERESOLVE unable to resolve dependency tree при следующей установке отдельного пакета. Установка всей группы сразу позволяет npm определить единый, согласованный набор версий.

    Вторая, более тонкая ловушка связана с копированием длинных команд из PDF-файлов или веб-страниц. Текст с автоматическим переносом может превратиться в настоящие разрывы строк при вставке, поэтому название пакета вроде @babel/preset-typescript разделяется на две части, и оболочка выполняет вторую часть как отдельную, бессмысленную команду. Явное продолжение строки позволяет размещать разрывы именно там, где вы этого хотите. Ниже приведена синтаксис командной строки Windows:

    npm install -D babel-jest ^
      @babel/core ^
      @babel/preset-env ^
      @babel/preset-react ^
      @babel/preset-typescript
    

    Знак ^ в конце указывает cmd.exe на то, что команда продолжается на следующей строке. В PowerShell символом для продолжения является обратный слеш, а в bash или zsh — обратная косая черта. Независимо от оболочки это по-прежнему одна команда npm install.

    Группа D: типы только для вашего редактора

    npm install -D @types/jest
    

    Этот пакет не влияет на процесс выполнения тестов; к тому моменту Babel уже удалил все типы. Он существует для того, чтобы TypeScript и ваш редактор распознавали глобальные функции вроде test(...) и expect(...), вместо того чтобы считать их ошибками.

    Добавление скриптов тестирования

    Установка Jest не предоставляет команды npm test, поэтому вам нужно самостоятельно добавить соответствующие скрипты в package.json:

    "scripts": {
      "dev": "vite",
      "build": "tsc -b && vite build",
      "lint": "eslint .",
      "test": "jest",
      "test:watch": "jest --watch"
    }
    

    npm test запускает весь набор тестов один раз. Команда npm run test:watch остается в активном состоянии и перезапускает только те тесты, которые были изменены в только что сохраненном файле; держите её открытой в другом терминале во время работы.

    Ещё две команды стоит запомнить на случай возникновения проблем:

    npx jest src/App.test.tsx   # run one file only
    npx jest --clearCache       # when Jest keeps showing an error
                                # you already fixed
    

    Команда кэширования имеет большее значение, чем кажется. Jest кэширует преобразованные файлы, поэтому после изменения файла babel.config.cjs или jest.config.cjs он может продолжать предоставлять старый результат и сообщать об ошибке, которую вы уже устранили. Если кажется, что исправление не работает, сначала очистите кэш, прежде чем делать вывод о том, что исправление неверно.

    Полная конфигурация тестов

    Ниже приведены все конфигурационные файлы в полном виде с пояснением того, за что отвечает каждый из них.

    babel.config.cjs

    Предустановки соответствуют группе C: они нацелены на текущую версию Node, используют автоматический движок JSX, благодаря чему файлам не нужно импортировать React, и удаляют TypeScript. Встроенный плагин обрабатывает то, чего не может Jest: import.meta, который используется в коде Vite для таких операций, как import.meta.env и горячая замена модулей, но который не допускается в формате CommonJS, на котором работает Jest.

    function stripImportMeta() {
      return {
        visitor: {
          MetaProperty(path) {
            path.replaceWithSourceString('({ url: "", hot: undefined })')
          },
        },
      }
    }
    
    module.exports = {
      presets: [
        ['@babel/preset-env', { targets: { node: 'current' } }],
        ['@babel/preset-react', { runtime: 'automatic' }],
        '@babel/preset-typescript',
      ],
      plugins: [stripImportMeta],
    }
    

    Это настоящий плагин Babel, написанный в виде встроенной функции вместо установленного пакета; Babel принимает обе формы. MetaProperty — это тип узла AST, который используется Babel для import.meta, и визитор заменяет каждое его вхождение на обычный объект с пустым полем url и неопределенным значением hot. Имейте в виду, что это также скрывает все значения import.meta.env от тестируемого кода, поэтому компонентам, которые читают переменные окружения, их потребуется имитировать отдельно.

    jest.config.cjs

    Этот файл связывает инструмент тестирования со всем остальным. Он выбирает среду jsdom, загружает файл настройки после готовности среды, пропускает каждый файл на JavaScript и TypeScript через babel-jest, а также преобразует импорты стилей и изображений в шаблонные модули.

    module.exports = {
      testEnvironment: 'jsdom',
      setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
      moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'mjs', 'json'],
      transform: {
        '^.+\\.(ts|tsx|js|jsx|mjs)
    : 'babel-jest', }, transformIgnorePatterns: ['node_modules/(?!(react-router|cookie-es)/)'], moduleNameMapper: { '\\.(css|less|scss|sass)
    : '<rootDir>/test/styleMock.js', '\\.(png|jpg|jpeg|gif|svg|webp)
    : '<rootDir>/test/fileMock.js', }, }

    Заслуживает внимания пункт transformIgnorePatterns. По умолчанию Jest не преобразует ничего внутри папки node_modules. Отрицательный lookahead делает исключение для react-router и cookie-es, поскольку они публикуются в виде ES-модулей, которые инструментарий CommonJS Jest не может загрузить без преобразования. Если позже вы добавите ещё один зависимый модуль, работающий только в режиме ESM, и увидите ошибку SyntaxError: Cannot use import statement outside a module, именно сюда следует добавить соответствующий шаблон.

    jest.setup.ts

    В файле настройки регистрируются мэтчеры jest-dom, а также добавляются объекты TextEncoder и TextDecoder в глобальную область видимости. Среда jsdom не предоставляет этих объектов, тогда как React Router ожидает их наличия, поэтому они берутся из модуля node:util в Node.js:

    import { TextEncoder, TextDecoder } from 'node:util'
    import '@testing-library/jest-dom'
    
    Object.assign(globalThis, { TextEncoder, TextDecoder })
    

    Шаблоны стилей и файлов

    Jest совершенно не знает, как импортировать файл CSS или PNG. Два приведённых ниже модуля-шаблона — это те места, куда moduleNameMapper перенаправляет такие импорты, поэтому компонент с кодом import './App.css' не приводит к сбою при выполнении теста:

    // test/styleMock.js
    module.exports = {}
    
    // test/fileMock.js
    module.exports = 'test-file-stub'
    

    src/App.test.tsx

    Наконец, тест, который проверяет всю конфигурацию. Он отображает компонент App внутри MemoryRouter, который хранит состояние маршрутизации в памяти вместо того, чтобы читать реальный URL браузера, и проверяет наличие текста-заменителя:

    import { render, screen } from '@testing-library/react'
    import { MemoryRouter } from 'react-router'
    import App from './App'
    
    test('renders the placeholder route content', () => {
      render(
        <MemoryRouter>
          <App />
        </MemoryRouter>,
      )
      expect(screen.getByText(/routes coming soon/i)).toBeInTheDocument()
    })
    

    Несмотря на свою небольшую размерность, этот тест охватывает все аспекты работы: компиляцию TSX, пакет маршрутизации ESM, полифилы для кодировки текста, обработку import.meta и сравнительный механизм jest-dom. Если тест проходит, значит настройка корректна.

    Prettier: окончание дискуссий о форматировании

    Пакеты: prettier, eslint-config-prettier

    npm install -D prettier eslint-config-prettier
    
    • prettier приводит ваш код к единому стилю, обычно при сохранении.
    • eslint-config-prettier имеет одну конкретную функцию: он отключает правила ESLint, противоречащие форматированию Prettier, такие как правила, касающиеся кавычек, точек с запятой и запятых в конце строк.

    Второй пакет не добавляет собственных правил; он лишь предотвращает конфликты между двумя инструментами. В файле eslint.config.js он должен быть последним элементом в списке настроек, поскольку более поздние элементы переопределяют более ранние, и ему необходимо переопределять их, а не подчиняться им.

    Зачем стоит использовать дополнительный пакет

    Без автоматической форматировки время ревью смещается в сторону использования табов вместо пробелов, а не в соответствии с логикой. Prettier намеренно предлагает лишь несколько опций, поэтому остаётся мало поводов для споров, и именно в этом заключается его суть.

    Связывание всего вместе

    Остаётся один недостаток. Команда tsc -b проверяет типы всего, что находится в папке src, включая файлы тестов, но по умолчанию она не знает ничего о пакетах test и expect. Необходимо добавить соответствующие пакеты типов в массив types в файле tsconfig.app.json:

    "types": ["vite/client", "jest", "@testing-library/jest-dom"]
    

    Затем запустите полную последовательность проверок, начиная с самого простого шага:

    npm run lint    # fast, catches obvious mistakes
    npm test        # fast, catches broken behaviour
    npm run build   # slower — real compile, real Tailwind output
    npm run dev     # slowest — but the only one that proves it renders
    

    Проверка кода с помощью инструментов типа Linting происходит быстро и позволяет выявить очевидные ошибки; тесты подтверждают правильность поведения приложения; процесс сборки выполняет реальную компиляцию и генерирует финальный вывод в формате Tailwind; а dev-сервер, являющийся самой медленной частью проверки, — единственный инструмент, который доказывает, что приложение отображается в браузере. Когда все четыре этапа пройдены, основа приложения готова, без единой строки кода с функциональным содержимым.

    Примечание об альтернативах

    Большая часть раздела Jest (предустановки Babel, плагин import.meta, исключения ESM) существует потому, что Jest не использует ту же систему сборки, что и Vite. Если вы предпочитаете меньше компонентов, Vitest переиспользует вашу конфигурацию Vite и работает с теми же пакетами из Testing Library. Чтобы узнать другие способы сокращения количества инструментов для тестирования, ознакомьтесь с нашим руководством по замене Jest на встроенный тестовый движок Node. Указанная выше настройка Jest остается хорошим выбором, если ваша команда уже знакома с Jest или использует его экосистему.

    Основные выводы

    • Рассматривайте каждую зависимость как решение: понимайте, зачем она нужна и что произойдет без нее.
    • Detailwind v4 требует только плагина Vite и одного импорта CSS; конфигурационных файлов не нужно.
  • Redux Toolkit хранит данные, а React Redux связывает их с компонентами; типизированные хуки обеспечивают чистоту кода, а локальное состояние интерфейса по-прежнему должно находиться в функции useState.
  • Установка роутера с самого начала позволяет изменять любую будущую страницу всего одной строкой кода.
  • Для работы Jest в проекте Vite требуется запускающий инструмент, среда DOM, три пакета из Testing Library, инструментарий Babel и несколько специфических настроек; если кажется, что какая-то настройка не применяется, очистите кэш Jest.
  • Устанавливайте взаимозависимые пакеты с помощью одной команды, а при необходимости продолжения строки используйте явные обозначения.
  • Связанные материалы