Базова структура React для продакшну: що насправді робить кожен пакет.
Налаштуйте Vite, Tailwind v4, Redux Toolkit, React Router, Jest та Prettier для React-додатку та зрозумійте, чому потрібен кожен пакет та кожен рядок конфігурації.
Виконання команди npm create vite створює React-додаток, який відображається, але це не той додаток, який можна було б передати справжнім користувачам: тут немає системи стилізації, спільного стану, маршрутизації, тестів та уніфікованого формату коду. Цей посібник крок за кроком створює цю відсутню основу за допомогою Tailwind CSS, Redux Toolkit, React Router, Jest з React Testing Library та Prettier. Для кожного пакета він відповідає на два запитання: що він насправді робить та що зламається, якщо його пропустити? До кінця ви отримаєте функціональну основу для розробки нових функцій, а ще важливіше — зможете читати власний файл package.json та пояснювати кожен рядок.
Огляд технологій:
- Tailwind CSS для стилізації
- Redux Toolkit для спільних даних додатку
- React Router для навігації між сторінками
Деякі з цих інструментів можна встановити за одну команду. Інші приховують несподівані деталі; наприклад, 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— для даних, які важливі лише для одного екрана чи компонента: чи відкрито модальне вікно, поточне значення поля введення, обраний варіант у списку.
Якщо певний стан інакше мусив би передаватися крізь кілька рівнів або дублюватися між екранами, це є хорошим ознакою того, що він належить до сховища даних.
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(...).
toBeInTheDocument(), тож вам не доводиться вручну порівнювати результати запиту з null.Коротко кажучи: відображення, перевірка та взаємодія. Три завдання, три пакети, і майже завжди вам потрібні всі вони.
Група C: інструментарій Babel, який дозволяє Jest читати TSX
Ця група існує з однієї причини: Jest сам по собі не може розуміти файли TypeScript чи JSX.
- babel-jest поєднує їх. Jest перед виконанням кожного файлу пропускає його через Babel.
: string та подібну синтаксис.На відміну від Групи 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)