Localhost — це не продакшн: діагностика React-додатків, які не працюють під час розгортання
Дізнайтеся, чому додаток на React, який працює без проблем на вашому комп’ютері, зазнає невдач після розгортання, та як відстежити API-адреси, змінні середовища, CORS, маршрутизацію, ресурси та механізми автентифікації до причини збою.
Джерела: Why Your React App Works Locally but Breaks After Deployment: 10 Common Problems and How to Fix Them
Додаток на React, який працює ідеально на localhost, але раптово перестає функціонувати після розгортання, є одним із найпоширеніших джерел розчарувань у роботі з фронтендом. Запити перестають повертати дані, оновлення сторінки призводить до помилки 404, зображення зникають, процес входу мовчки провалюється, або платформа хостингу відмовляється компілювати код, який без проблем скомпілювався на вашому ноутбуці. Майже жодна з цих проблем не походить від самого React. Вони виникають через розбіжності між середовищем, у якому ви розробляєте, та тим, з яким стикаються ваші користувачі. У цьому посібнику розглядаються десять найпоширеніших причин, описується, як кожна з них проявляється, а також наводиться метод дебаггінгу та чек-лист перед розгортанням, які можна використовувати при кожному випуску.
Чому один і той самий код поводиться по-різному у продакшені
Фронтенд на React рідко є всією програмою. Він взаємодіє з API, а API — із базою даних, причому кожен етап цього ланцюга залежить від налаштувань: змінних середовища, імен доменів, правил мережі, параметрів автентифікації та того, що робить платформа хостингу з надходячими запитами. На вашому комп’ютері ви неявно контролюєте все це: ви знаєте, на якому порту працює бекенд, який файл .env завантажується та які пакети знаходяться у директорії node_modules. У продакшені кожна з цих припущень замінюється на щось конкретне, і будь-яка неузгодженість може змусити правильний код виглядати несправним.
Пам’ятайте про це під час читання наступних розділів. Більшість виправлень — це зміни налаштувань, а не зміни коду.
1. URL API все ще вказує на localhost
Коли розгорнена додаток не відображає жодних даних, перше, що варто перевірити, — це куди надсилаються його запити. Під час розробки природно запускати бекенд локально та вказувати його адресу як константу у фронтенді:
const API_URL = "http://localhost:8080/api";
Це працює тому, що ваш браузер та бекенд працюють на одному комп’ютері. Як тільки фронтенд розгортається на адресу на кшталт https://my-app.example.com, та сама строка продовжує надсилати запити до http://localhost:8080/api. Проблема полягає у тому, що localhost розшифровується залежно від того, який браузер виконує код. Для відвідувача це означає його власний комп’ютер, а не ваш розробницький сервер. Таким чином шлях запиту фактично стає:
User's Browser
↓
localhost:8080
↓
User's computer
Якщо тільки у відвідувача випадково не працює ваш бекенд на порту 8080, кожен запит зазнає невдачі.
Рішення полягає у тому, щоб припинити жорстке введення адреси та зчитувати її з змінної середовища. У проекті Vite значення оголошується у файлі env:
VITE_API_URL=https://your-backend.example.com/api
а зчитується у коді через import.meta.env:
const API_URL = import.meta.env.VITE_API_URL;
Тепер під час розробки можна використовувати адресу http://localhost:8080/api, тоді як у продакшн-версії — щось на кшталт https://api.example.com/api, причому код не потрібно змінювати. Це незначна оптимізація, але вона усуває одну з найпоширеніших помилок під час розгортання: спробу змусити браузер сторонньої людини взаємодіяти з вашим ноутбуком.
Ще одним захистом є гучне повідомлення про помилку у разі відсутності змінної. Якщо API_URL має значення undefined, запити надсилаються за адресами на кшталт undefined/users, що ускладнює діагностику. Однорядкова перевірка під час запуску, яка записує логи або кидає помилку при відсутності значення, допомагає заощадити час згодом.
2. Змінні середовища у продакшн-середовищі так і не були налаштовані
Змінні середовища є наступним поширеним джерелом проблем. Локально у вас, ймовірно, є файл .env, який містить усе, що потрібно додатку:
VITE_API_URL=http://localhost:8080/api
VITE_APP_NAME=MyApp
Ці значення існують лише на вашому комп’ютері. Платформа хостингу не читає ваш локальний файл .env (і не повинна цього робити, адже такий файл зазвичай виключається з контролю версій). Ваша версія додатку для продакшну мусить отримувати власні значення, налаштовані у параметрах змінних середовища провайдера:
VITE_API_URL=https://your-backend.example.com/api
Після додавання чи зміни значення там необхідно ініціювати нову розгортку. З Vite цей крок є обов’язковим: лише змінні, попереджені VITE_, стають доступними для клієнтського коду, і Vite підставляє їх у бандл під час збирання. Зміна змінної в панелі керування нічого не впливає на бандл, який вже був створений зі старим значенням.
Змінні фронтенду — це не секрети
У цьому криється урок безпеки. Розміщення значення в .env не робить його приватним. Усе, що стає доступним для клієнта, компілюється у JavaScript, який завантажує та може читати кожен відвідувач. Паролі до баз даних, приватні ключі API та інші облікові дані серверної частини ніколи не повинні знаходитися у змінних VITE_*. Зберігайте їх на бекенді або за функцією серверної частини, і нехай фронтенд викликає ендпоінт, який буде їх використовувати замість нього.
3. Правила CORS дозволяють лише ваш вихідний джерело розробки
Помилки спільного використання ресурсів між доменами є ще одним класичним сюрпризом після розгортання. Уявіть фронтенд, який обслуговується з:
https://myapp.example.com
і бекенд, який обслуговується з:
https://api.example.com
Це різні вихідні джерела, адже вихідне джерело — це поєднання схеми, хоста та порту. Браузери дотримуються політики однакового вихідного джерела, тому щоб фронтенд міг отримувати відповіді від API, бекенд повинен надсилати заголовки CORS, які прямо дозволяють вихідне джерело фронтенда.
Під час розробки бекенд, ймовірно, був налаштований так, щоб приймати запити від сервера розробки Vite:
http://localhost:5173
Усе працювало, тож легко припустити, що проблема CORS вирішена. Однак після розгортання вихідне джерело фронтенда є:
https://myapp.example.com
Для браузера це абсолютно новий початковий адресний простір. Якщо на бекенді все ще вказаний лише http://localhost:5173, розгорнутий фронтенд буде заблокований. Для бекенду Spring Boot дозволити використання продакшн-адресного простору у контролері може виглядати так (фрагмент коду на Java):
@CrossOrigin(origins = "https://myapp.example.com")
@RestController
public class UserController {
// ...
}
Анотування кожного контролера підходить для невеликих сервісів, але у більшому кодовому базисі простіше підтримувати єдину централізовану конфігурацію CORS, і її легше не забути під час додавання нових кінцевих точок. Незалежно від технологічного стеку правило залишається таким самим: дозволені початкові адресні простори мають точно відповідати справжньому продакшн-фронтенду, включаючи https та будь-які піддомени. Поширеним та ефективним підходом є зчитування списку дозволених початкових адресних просторів з власної конфігурації середовища бекенду, щоб режими розробки, тестування та продакшн дозволяли використання відповідного фронтенду.
Пам’ятайте, що CORS — це механізм браузера. Запит, який зазнає невдачі через помилку CORS у браузері, може успішно виконатися за допомогою curl або Postman, тому тестування API поза браузером може дати оманливо позитивний результат.
4. Оновлення клієнтського маршруту повертає 404
Ця проблема змушує розробників детально переглядати код свого маршрутизатора, навіть якщо він працює належним чином. Припустімо, у додатку є такий маршрут:
/dashboard
Ви потрапляєте на головну сторінку, натискаєте посилання на панель керування, і вона відображається ідеально. Ви можете навігувати без проблем. Потім ви натискаєте «Оновити», і сервер відповідає:
404 Not Found
Пояснення полягає у тому, як працюють односторінкові додатки. Коли ви натискаєте на посилання, React Router оновлює URL та повністю відображає відповідний компонент у браузері; жоден запит на /dashboard не надходить на сервер. Коли ви оновлюєте сторінку або відкриваєте цей URL безпосередньо, браузер запитує хостинговий сервер про /dashboard. Якщо сервер знає лише про реальні файли, він шукає ресурс з таким шляхом, не знаходить його та повертає код 404.
Рішенням є правило резервного варіанту на хостингу: будь-який запит до маршруту фронтенду, який не відповідає реальному файлу, слід переписати або перенаправити на index.html. Потім браузер завантажує додаток, а React Router читає URL та відображає відповідну сторінку. У документації з розгортання React Router описано цю вимогу, і більшість статичних хостингів пропонують відповідне налаштування або невеликий конфігураційний файл для цього. Тож, якщо клік функціонує, а оновлення — ні, не чіпайте свої маршрути та спочатку перевірте налаштування переписування SPA хостингом. Якщо ви хочете ознайомитися з тим, як спочатку оголошуються маршрути, перегляньте Основи React Router.
5. Шляхи імпорту з неправильною величиною літер
Деякі баги залишаються непомітними, поки процес збірки не виконується на іншій операційній системі. Уявіть файл компонента з назвою:
Header.jsx
Імпортується ось так:
import Header from "./components/header";
Єдина різниця — це одна велика літера, яку легко пропустити під час перевірки. Файлові системи не узгоджуються щодо того, чи має це значення. За замовчуванням у macOS та Windows імпорт є незалежним від регістру, тож імпорт вдається, і додаток запускається. Більшість серверів збірки та контейнерів працюють під Linux, чиї файлові системи є чутливими до регістру, тому Header.jsx та header.jsx є двома різними іменами, і процес збірки завершується помилкою «модуль не знайдено».
Рішення полягає просто у тому, щоб кожен імпорт точно відповідав файлу на диску за кожною літерою. З огляду на це:
components/Header.jsx
Напишіть імпорт ось так:
import Header from "./components/Header";
Приділяйте таку саму увагу назвам каталогів. Ще одна підступність, про яку варто знати: зміна назви файлу лише через різницю у великості та малості літер може залишитися непоміченою Git на файловій системі, яка не розрізняє великі та малі літери, тож репозиторій може зберігати стару назву, навіть якщо ваш редактор відображає нову. Перевірка назви, яку насправді відстежує Git, швидко розв’язує цю проблему.
6. Зображення та статичні ресурси зникають
Іноді сам додаток завантажується, але його зображення, шрифти чи іконки — ні. Це зазвичай означає, що до цих ресурсів звертаються за шляхами, які працювали на сервері розробки, але відсутні у фінальній версії додатку.
Vite пропонує два різні механізми, і корисно знати, який з них ви використовуєте. Перший — це імпорт ресурсу з вихідного коду:
import logo from "./assets/logo.png";
а також використання імпортованого значення як джерела зображення:
function App() {
return <img src={logo} alt="Logo" />;
}
Оскільки цей ресурс проходить через граф модулів, Vite обробляє його під час збирання, копіює його до результату з генерованою назвою файлу та надає вам правильну URL. Вам ніколи не доводиться здогадуватися про кінцевий шлях.
Другим механізмом є каталог public для файлів, які мають надаватися у такому вигляді без додаткової обробки:
public/
└── logo.png
Файли з цього каталогу копіюються до кореня результату збирання, тому ви посилаєтесь на них за допомогою абсолютного URL-шляху:
<img src="/logo.png" alt="Logo" />
src/assets/logo.png, але браузер розуміє лише ті URL, які насправді надає розгорнутий сервер. Якщо додаток розміщений під підшляхом замість кореня домену, абсолютні шляхи на кшталт /logo.png також можуть не спрацювати, і саме для вирішення цієї проблеми існує опція base у Vite.
7. Розробчий сервер — це не продакшн-версія
npm run dev
Запускає сервер розробки з можливістю гарячої заміни модулів, детальними попередженнями та перетворенням модулів за потреби. Те, що деплоюється, — це щось інше: збірка оптимізованих статичних файлів. Для проекту Vite його створюють за допомогою:
npm run build
а ваш хост подає отриманий результат. Оскільки ці два шляхи обробляють код по-різному, цілком можливо, що додаток буде працювати під:
npm run dev
але зазнає невдачі під:
npm run build
Ця невдача є хорошою новиною, якщо ви її помітили самостійно, адже можете виправити її до того, як щось дістанеться користувачам. Vite також дозволяє локально подавати готовий результат за допомогою:
npm run preview
Це набагато ближче до того, що отримають відвідувачі, ніж на сервері розробників. Станьте звичайними до створення та перегляду проекту локально перед кожним розгортанням, замість того щоб дозволяти платформі хостингу бути першим місцем, де запускається фінальна версія проекту. Підключення команди npm run build до процесу CI забезпечує вам таку саму гарантію автоматично при кожному pull request.
8. Ваші локальні node_modules приховують відсутні залежності
Директорія node_modules, яка існує протягом тривалого часу, може створювати враження, що проект у кращому стані, ніж насправді. Протягом тижнів розробки там накопичуються пакети: ті, які ви інсталювали раз для тестування, транзитивні залежності інших пакетів, залишки з відміненої гілки. Ваш код може імпортувати один з цих пакетів та працювати лише через те, що він випадково є на місці.
Середовище розгортання починається з чистого стану. Воно встановлює саме те, що описано у package.json та файлі lockfile, і нічого більше. Тож якщо ваш код містить:
import axios from "axios";
тоді Axios потрібно оголосити як справжню залежність проекту, наприклад:
{
"dependencies": {
"axios": "^1.0.0",
"react": "^19.0.0",
"react-router-dom": "^7.0.0"
}
}
Номери версій у цьому фрагменті є ілюстративними; використовуйте ті, які дійсно потрібні вашому проекту. Мета полягає у тому, щоб після створення нової копії та її встановлення у ній було все необхідне для складання та запуску додатку. Зберігайте як package.json, так і файл lockfile та підтримуйте їх синхронність, щоб кожне встановлення використовувало однакові версії.
Щоб відтворити умови розгортання локально, видаліть node_modules та встановіть проект знову, бажано за допомогою npm ci, який встановлює компоненти суворо з файлу lockfile та призводить до помилки, якщо цей файл та package.json не збігаються. Якщо після цього процес будування зламається, ви виявили незаявлену залежність раніше, ніж це зробив хост.
9. Фронтенд працює, а бекенд не функціонує
Хочеться звинувачувати React щоразу, коли екран виглядає неправильно, але проблема часто полягає далі у ланцюзі обробки. Припустимо, розгорнутий фронтенд завантажується успішно за адресою:
https://myapp.example.com
Ви відкриваєте панель керування — вона порожня. У вкладці Network ви бачите:
GET https://api.example.com/api/dashboard
500 Internal Server Error
Цей рядок багато чого пояснює. Додаток React виконав свою роботу та надіслав запит; бекенд його отримав, але зазнав невдачі під час обробки. Ймовірними причинами є пошкоджене з’єднання з базою даних, відсутність змінної середовища на стороні сервера, проблеми з автентифікацією, некерована виняток чи конфігурація, яка відрізняється від вашої локальної налаштування.
Типовий запит повноцінного фреймворку проходить через ці шари:
React Frontend
↓
REST API
↓
Backend
↓
Database
Невдача на нижньому рівні впливає на верхній. Якщо до бази даних неможливо дістатися, панель керування виглядає несправною, навіть якщо кожен рядок коду фронтенду є правильним. Саме тому діагностика не повинна закінчуватися на браузері. Коли виклик API зазнає невдачі, перегляньте вкладку «Мережа», а потім журнали бекенду. Якщо браузер повідомляє про код 500, а журнал сервера містить:
Database connection failed
Ви маєте справу з проблемою бекенду чи інфраструктури, а не з помилкою відображення. Фронтенд, який показує чіткий стан помилки для невдалих запитів замість порожньої сторінки, робить цю відмінність видимою як для користувачів, так і для розробників.
10. Аутентифікація працює локально та зазнає невдач після розгортання
Процеси входу особливо чутливі до відмінностей середовищ. Локально послідовність може бути такою:
localhost → Login → Dashboard
У продакшені вона стає такою:
Production → Login → Dashboard doesn't load
Кілька попередніх проблем можуть самостійно спричинити це. Фронтенд все ще може викликати неправильну URL API, правила CORS бекенду можуть не включати джерело з продакшену, або токени можуть бути неправильно налаштовані для нового середовища. Однак найчастішою причиною є куки.
Чому куки поводяться інакше у продакшені
Чи буде браузер зберігати та надсилати кукі, залежить від його атрибутів: Secure, HttpOnly, SameSite, Domain та Path. У локальному режимі все зазвичай використовує одного хоста, тому ці атрибути рідко мають значення. У продакшн-середовищі фронтенд та API можуть знаходитися на різних доменах через HTTPS, і тоді браузер починає дотримуватися правил. Наприклад, кукі, позначена як SameSite=None, приймається лише тоді, коли вона також позначена як Secure.
Запити між різними доменами, які передають облікові дані, також вимагають співпраці з обох сторін. Клієнт має дозволити надсилання облікових даних, а сервер має прямо дозволити їх використання для конкретного домену; домен-шаблон не приймається для запитів із обліковими даними. Якщо хтось із учасників не виконає свою частину, запит на вход може здатися успішним, проте куки сесії ніколи не надійдуть під час наступних викликів.
Висновок: процес входу, який працює на localhost, був протестований лише у найпростішому можливому середовищі. Протестуйте його знову у середовищі, схожому на продакшн, з використанням HTTPS, доменів, налаштувань кук та правил CORS.
Метод поетапної діагностики
Коли виникають проблеми під час розгортання, найменш ефективною реакцією є редагування випадкових компонентів у надії на покращення. Ефективною є діагностика того, який саме етап не працює, ще до того, як почнеться редагування коду.
Почніть з консолі
Відкрийте інструменти розробника браузера (зазвичай F12) та перегляньте вікно Console. Повідомлення на кшталт Failed to fetch, порушення правил CORS, неконтрольовані винятки JavaScript та помилки 404 для скриптів чи ресурсів вказують на різні причини проблеми.
Перегляньте вкладку Network
Перейдіть на вкладку Network та перезавантажте сторінку. Для кожного запиту ви можете побачити точну URL, метод HTTP, код стану, тіло відповіді та часто заголовки. Для проблем з API це найінформативніший перегляд. Ви можете побачити:
GET /api/users
Status: 404
або:
GET /api/users
Status: 401
або:
GET /api/users
Status: 500
Кожен код стану допомагає звузити пошук:
404: запитуваний ресурс чи маршрут відсутні за цією URL.401: потрібна автентифікація, або надіслані облікові дані відсутні чи недійсні.
403: сервер зрозумів, хто ви, але відмовляється дозволити виконання дії.500: сервер зазнав внутрішньої помилки під час обробки запиту.Перевірте URL, який насправді використовує браузер
Не покладайтеся на те, що ви вважаєте діями вихідного коду; подивіться на запит, який справді надіслав браузер. Якщо в продакшені викликається:
http://localhost:8080/api/users
тоді як насправді бекенд знаходиться за адресою:
https://api.example.com/api/users
ви майже напевно знайшли проблему — це проблема з URL API з розділу 1.
Слідкуйте за помилками сервера в журналах бекенду
500 означає, що відповідь знаходиться на сервері. Журнали бекенду можуть виявити невдалий з’єднання з базою даних, відсутню змінну чи виняток, про який фронтенд не може повідомити. Саме ця звичка переходити до наступного рівня, коли докази цього вказують, дозволяє швидко відлагоджувати розгортання.
Розбір реальної несправності
Розглянемо аплікацію повного стеку, яка розгортається без помилок, проте панель керування відображається порожньою. Інстинктивною реакцією є припущення, що у компоненті панелі керування є баг:
"Щось мусить бути не так із компонентом панелі керування."
Замість того, щоб відкривати цей компонент, відкрийте вкладку Network. Там ви побачите:
GET https://api.example.com/api/dashboard
Status: 500
Це вже виключає фронтенд як основну причину: запит було надіслано. Далі журнали бекенду показують:
Database connection failed
З обома доказами вся ланцюгова структура стає зрозумілою:
React Frontend
↓
API request
↓
Backend
↓
Database connection failed
↓
500 response
Компонент панелі керування ніколи не був причиною проблем. Десять хвилин на аналіз інструментів браузера та журналів сервера замінили б години роботи над переробкою коду. Здатність одночасно аналізувати помилки браузера, мережевий трафік та журнали сервера є однією з найцінніших навичок у повсякденній роботі з програмним забезпеченням.
Чек-лист перед розгортанням
Найнадійніший спосіб уникнути несподіванок під час розгортання — це перед відправкою простежити кожен етап проходження запиту від браузера до бази даних. Практичний чек-лист:
- Запустіть
npm run buildлокально та переконайтеся, що це вдалося, потім перевірте результат за допомогоюnpm run preview. - Переконайтеся, що встановлена URL продакшн-API та що жоден запит не спрямований на
localhost.
Жоден чек-лист не гарантує бездоганної розробки, але цей допомагає виявити більшість поширених проблем, поки їх ще легко вирішити. Щоб дізнатися більше про те, що має включати надійна налаштованість проекту з самого початку, базові налаштування React для продакшну охоплюють аспекти інструментарію.
Чому твердження „це працює локально“ мало що говорить
Ваш комп’ютер — це контрольоване, сприятливе середовище. Ви знаєте, де працює бекенд, які змінні визначені, що встановлено у node_modules та яку базу даних використовує додаток. У продакшні майже все змінюється: інший домен, інша адреса API, інші змінні середовища, інша база даних, суворіші правила CORS та кукі, сервер, якому потрібно пояснити, як обробляти маршрути з боку клієнта, а також часто інша операційна система. Код може бути абсолютно правильним, тоді як конфігурація навколо нього — хибною.
Саме тому розгортання варто розглядати як ще одне середовище для тестування, а не як остаточний крок після завершення розробки. Середовища стейджингу, процеси CI та попередні розгортання існують для того, щоб перенести це тестування на ранній етап.
Ключові висновки
- Якщо щось зламується після розгортання, спочатку з’ясуйте, який шар не працює, перш ніж запитувати, у чому проблема з React.
- Якщо нічого не завантажується, перегляньте результати компіляції та ресурси; якщо сторінка завантажується, але даних немає, перегляньте вкладку «Мережа» та URL API.
- Код
500веде до журналів серверної частини; помилка CORS веде до списку дозволених джерел серверної частини. - Маршрут, який працює при кліку, але повертає помилку 404 після оновлення, потребує резервного варіанту SPA на хості, а не змін маршрутизатора.
- Відсутні значення зазвичай означають, що змінні середовища продакшену так і не були встановлені або додаток не був перекомпільований після їх налаштування.
- Помилки автентифікації часто пов’язані з URL API, параметрами CORS та атрибутами cookie, тому перевірте всі три елементи.
Зустріч з такими проблемами — це не ознака слабких навичок програмування; це спосіб, яким усі дізнаються, з чого складаються справжні системи. Мета не в тому, щоб ніколи не бачити помилок, а в тому, щоб швидко знаходити місце їх виникнення.