Головна / Статті / Базова структура 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: тут немає файлу 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 імітує реалістичну поведінку користувача. Набір тексту викликає повну фокусуваність, послідовність подій keydown, input та keyup, які б використовував браузер, замість однієї синтетичної події, надісланої до елемента.
  • Коротко кажучи: відображення, перевірка та взаємодія. Три завдання, три пакети, і майже завжди вам потрібні всі вони.

    Група 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:

    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
    

    Лінтинг виконується швидко та виявляє очевидні помилки, тести підтверджують поведінку програми, процес збірки здійснює справжню компіляцію та генерує фактичний вихід у форматі Tailwind, а сервер розробки, який є найповільнішим етапом перевірки, є єдиним, хто підтверджує, що додаток відображається у браузері. Коли всі чотири етапи пройдені, основа додатку готова, без жодної рядка коду функціоналу.

    Примітка щодо альтернатив

    Більша частина розділу про Jest (префікси Babel, плагін import.meta, винятки ESM) існує тому, що Jest не використовує ту саму систему збірки, що й Vite. Якщо ви віддаєте перевагу простоті, Vitest використовує вашу конфігурацію Vite та працює з тими самими пакетами Testing Library. Щоб дізнатися про інші способи скорочення кількості інструментів для тестування, перегляньте наш гайд про заміну Jest на вбудований тест-запускач Node. Вищенаведена налаштування Jest залишається гарним вибором, якщо ваша команда вже знає Jest або покладається на його екосистему.

    Основні висновки

    • Розглядайте кожну залежність як рішення: знайте, що вона робить та що не буде працювати без неї.
    • Tailwind v4 потребує лише плагіна Vite та одного імпорту CSS; конфігураційних файлів не потрібно.
  • Redux Toolkit зберігає дані, а React Redux під’єднує їх до компонентів; типовані хуки забезпечують чистий спосіб використання, а локальний стан UI все ще має знаходитися у useState.
  • Якщо рано додати маршрутизатор, кожну наступну сторінку можна буде змінити за один рядок коду.
  • Dest у проекті Vite потребує виконавця, середовища DOM, трьох пакетів Testing Library, інструментарію Babel та кількох специфічних корекцій конфігурації; якщо здається, що корекція проігнорована, потрібно очистити кеш Jest.
  • Встановлюйте взаємозалежні пакети за одним командним запитом, а при необхідності розтягнення команди на кілька рядків використовуйте явне продовження рядка.
  • Пов’язана література