<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Leo Brown</title>
    <description>The latest articles on DEV Community by Leo Brown (@leobrown).</description>
    <link>https://dev.to/leobrown</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4100697%2Fe7c9f4fa-d597-4688-9177-2705cd70ff72.png</url>
      <title>DEV Community: Leo Brown</title>
      <link>https://dev.to/leobrown</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leobrown"/>
    <language>en</language>
    <item>
      <title>7 Things I Check When a React App Works Locally but Breaks in Production</title>
      <dc:creator>Leo Brown</dc:creator>
      <pubDate>Fri, 04 Sep 2026 19:28:13 +0000</pubDate>
      <link>https://dev.to/leobrown/7-things-i-check-when-a-react-app-works-locally-but-breaks-in-production-83o</link>
      <guid>https://dev.to/leobrown/7-things-i-check-when-a-react-app-works-locally-but-breaks-in-production-83o</guid>
      <description>&lt;h1&gt;
  
  
  7 Things I Check When a React App Works Locally but Breaks in Production
&lt;/h1&gt;

&lt;p&gt;One of the most annoying frontend problems is this:&lt;/p&gt;

&lt;p&gt;Your app works perfectly on localhost.&lt;/p&gt;

&lt;p&gt;Then you deploy it.&lt;/p&gt;

&lt;p&gt;And suddenly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API requests fail&lt;/li&gt;
&lt;li&gt;routes return 404&lt;/li&gt;
&lt;li&gt;environment variables are missing&lt;/li&gt;
&lt;li&gt;images disappear&lt;/li&gt;
&lt;li&gt;authentication redirects break&lt;/li&gt;
&lt;li&gt;everything works on your machine, but nowhere else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This happens often enough that I now go through roughly the same checklist every time.&lt;/p&gt;

&lt;p&gt;Here are the things I usually check first.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Environment variables
&lt;/h2&gt;

&lt;p&gt;Local development makes environment variables feel simple because &lt;code&gt;.env&lt;/code&gt; files are always nearby.&lt;/p&gt;

&lt;p&gt;Production is different.&lt;/p&gt;

&lt;p&gt;A variable may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not exist on the server&lt;/li&gt;
&lt;li&gt;use a different name&lt;/li&gt;
&lt;li&gt;contain an outdated URL&lt;/li&gt;
&lt;li&gt;not be exposed to the browser&lt;/li&gt;
&lt;li&gt;have been changed without rebuilding the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, in Vite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VITE_API_URL=https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you accidentally use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API_URL=https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the variable will not automatically be exposed to client-side code.&lt;/p&gt;

&lt;p&gt;Next.js has a similar distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEXT_PUBLIC_API_URL=https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is available in the browser, while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API_URL=https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is normally server-side only.&lt;/p&gt;

&lt;p&gt;When something works locally but fails after deployment, I always inspect the actual production environment before changing application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hardcoded localhost URLs
&lt;/h2&gt;

&lt;p&gt;This one is extremely common.&lt;/p&gt;

&lt;p&gt;During development, code slowly accumulates things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:3001/api/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws://localhost:4000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything works locally because all services are running on the same machine.&lt;/p&gt;

&lt;p&gt;In production, &lt;code&gt;localhost&lt;/code&gt; means the user's own machine or the production server, depending on where the code executes.&lt;/p&gt;

&lt;p&gt;Neither is usually what you want.&lt;/p&gt;

&lt;p&gt;A better approach is to keep URLs in configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VITE_API_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/users`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also search the whole project for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;localhost
127.0.0.1
http://
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before deploying.&lt;/p&gt;

&lt;p&gt;It catches more issues than you might expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. CORS
&lt;/h2&gt;

&lt;p&gt;A request that succeeds locally can fail immediately when the frontend and backend are hosted on different origins.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend:
https://app.example.com

API:
https://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend needs to allow requests from the frontend origin.&lt;/p&gt;

&lt;p&gt;A typical Express configuration might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During debugging, I open the browser network panel and inspect the request directly.&lt;/p&gt;

&lt;p&gt;If the browser says something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blocked by CORS policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then changing React code usually will not fix the problem.&lt;/p&gt;

&lt;p&gt;The fix belongs on the API side.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Client-side routing
&lt;/h2&gt;

&lt;p&gt;Single-page applications often have routes like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/
/dashboard
/settings
/users/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigation inside the app works because React Router handles it.&lt;/p&gt;

&lt;p&gt;But refreshing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;404 Not Found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because the web server tries to find an actual &lt;code&gt;/dashboard&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;For a client-side SPA, the server usually needs to return &lt;code&gt;index.html&lt;/code&gt; for unknown application routes.&lt;/p&gt;

&lt;p&gt;For Nginx, that may look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Platforms like Vercel, Netlify, Cloudflare Pages, and others have their own rewrite rules.&lt;/p&gt;

&lt;p&gt;If navigation works but refreshing a route does not, routing configuration is one of the first things I check.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. HTTP vs HTTPS
&lt;/h2&gt;

&lt;p&gt;Browsers are much stricter once your site is served over HTTPS.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;calling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://api.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can be blocked as mixed content.&lt;/p&gt;

&lt;p&gt;WebSockets have the same issue.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you may need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wss://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A page can appear completely normal while some background functionality silently fails because the browser blocks insecure requests.&lt;/p&gt;

&lt;p&gt;The console usually reveals this quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. File name case sensitivity
&lt;/h2&gt;

&lt;p&gt;This bug can be surprisingly confusing.&lt;/p&gt;

&lt;p&gt;On some local development systems, this may appear to work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even if the real file is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Header.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a Linux production server may treat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Header.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;header.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as different files.&lt;/p&gt;

&lt;p&gt;The deployment build then fails, or the module cannot be resolved.&lt;/p&gt;

&lt;p&gt;I try to keep import paths exactly aligned with filenames:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Header&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./components/Header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This problem is especially easy to introduce when renaming files only by changing capitalization.&lt;/p&gt;

&lt;p&gt;Git may not always detect the change the way you expect.&lt;/p&gt;

&lt;p&gt;One workaround is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nb"&gt;mv &lt;/span&gt;Header.jsx HeaderTemp.jsx
git &lt;span class="nb"&gt;mv &lt;/span&gt;HeaderTemp.jsx header.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Production build behavior
&lt;/h2&gt;

&lt;p&gt;Development mode is not production mode.&lt;/p&gt;

&lt;p&gt;React, Vite, Next.js, and bundlers can behave differently after optimization.&lt;/p&gt;

&lt;p&gt;Before blaming the hosting provider, I build the application locally.&lt;/p&gt;

&lt;p&gt;For Vite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npm run preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Next.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches problems such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing imports&lt;/li&gt;
&lt;li&gt;invalid environment variables&lt;/li&gt;
&lt;li&gt;SSR-only errors&lt;/li&gt;
&lt;li&gt;browser-only APIs used on the server&lt;/li&gt;
&lt;li&gt;build-time data fetching failures&lt;/li&gt;
&lt;li&gt;TypeScript errors&lt;/li&gt;
&lt;li&gt;incorrect asset paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the production build already fails locally, deployment is not the real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  My debugging order
&lt;/h2&gt;

&lt;p&gt;When a deployment behaves differently from localhost, I usually check things in this order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Browser console
2. Network requests
3. Production environment variables
4. API URLs
5. CORS
6. Routing/rewrite rules
7. HTTPS / mixed content
8. Production build logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This order saves me from randomly changing code.&lt;/p&gt;

&lt;p&gt;The browser console and network panel alone often reveal the issue within a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small habit that helps
&lt;/h2&gt;

&lt;p&gt;I try to avoid treating production configuration as something I think about only during deployment.&lt;/p&gt;

&lt;p&gt;Instead, I keep local development reasonably close to production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use environment variables from the start&lt;/li&gt;
&lt;li&gt;avoid hardcoded URLs&lt;/li&gt;
&lt;li&gt;test production builds locally&lt;/li&gt;
&lt;li&gt;keep frontend and backend configuration separate&lt;/li&gt;
&lt;li&gt;use HTTPS-compatible URLs&lt;/li&gt;
&lt;li&gt;check routes with direct page loads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The closer the environments are, the fewer surprises appear later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;When an app works locally but fails in production, the React code itself is often fine.&lt;/p&gt;

&lt;p&gt;The problem is usually somewhere around it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;configuration
networking
routing
security
environment
build process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How I test my React app on real devices without deploying it</title>
      <dc:creator>Leo Brown</dc:creator>
      <pubDate>Sat, 29 Aug 2026 20:37:23 +0000</pubDate>
      <link>https://dev.to/leobrown/how-i-test-my-react-app-on-real-devices-without-deploying-it-1ko</link>
      <guid>https://dev.to/leobrown/how-i-test-my-react-app-on-real-devices-without-deploying-it-1ko</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjpoqy4xff8hhcf7wcqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffjpoqy4xff8hhcf7wcqz.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  How I test my React app on real devices without deploying it
&lt;/h1&gt;

&lt;p&gt;I recently needed to test a React app outside my own computer. Opening another browser tab wasn't enough. I wanted to use the app on my phone, try it on another device, and sometimes share the current build without deploying it first.&lt;/p&gt;

&lt;p&gt;The app was running locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the project, that usually gives me an address like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;localhost&lt;/code&gt; only exists on my machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tried
&lt;/h2&gt;

&lt;p&gt;There are several ways to expose a local development server to the internet. I tried a few tunneling services and approaches, and they worked, but I wanted something simpler for this workflow.&lt;/p&gt;

&lt;p&gt;My requirements were straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  one command to start a tunnel;&lt;/li&gt;
&lt;li&gt;  HTTPS without configuring certificates;&lt;/li&gt;
&lt;li&gt;  no deployment;&lt;/li&gt;
&lt;li&gt;  a URL I could open from my phone;&lt;/li&gt;
&lt;li&gt;  something convenient for React and Vite development;&lt;/li&gt;
&lt;li&gt;  preferably a hostname I could keep instead of receiving a new URL every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up using &lt;a href="https://portpreview.com/" rel="noopener noreferrer"&gt;PortPreview&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting a tunnel
&lt;/h2&gt;

&lt;p&gt;There isn't much to configure. With the React development server already running, I can expose its port from the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx portpreview 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My local address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is then available through a public HTTPS URL. I can open that URL on my phone and test the app there without making a temporary deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I use it for React development
&lt;/h2&gt;

&lt;p&gt;Chrome DevTools responsive mode is useful, but it doesn't cover everything. Some things need a real device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  touch interactions;&lt;/li&gt;
&lt;li&gt;  mobile Safari and Chrome behavior;&lt;/li&gt;
&lt;li&gt;  viewport quirks;&lt;/li&gt;
&lt;li&gt;  authentication redirects;&lt;/li&gt;
&lt;li&gt;  camera or browser APIs;&lt;/li&gt;
&lt;li&gt;  network behavior;&lt;/li&gt;
&lt;li&gt;  links opened from messaging apps;&lt;/li&gt;
&lt;li&gt;  sharing a work-in-progress build with someone else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of pushing a branch and waiting for a preview deployment, I leave the React dev server running and expose it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Edit React code
      ↓
Local dev server
      ↓
PortPreview tunnel
      ↓
Real phone / remote browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changes from the local server appear through the tunnel immediately, so the workflow still feels like local development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I kept using it
&lt;/h2&gt;

&lt;p&gt;PortPreview lets me reserve one domain for free. That matters more than I expected because temporary URLs get annoying once you use tunnels regularly.&lt;/p&gt;

&lt;p&gt;A tunnel URL may be configured in several places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  OAuth callbacks;&lt;/li&gt;
&lt;li&gt;  webhook endpoints;&lt;/li&gt;
&lt;li&gt;  API dashboards;&lt;/li&gt;
&lt;li&gt;  test integrations;&lt;/li&gt;
&lt;li&gt;  mobile devices;&lt;/li&gt;
&lt;li&gt;  bookmarks;&lt;/li&gt;
&lt;li&gt;  configuration files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a reserved hostname, I don't have to update those places whenever I restart my development environment. The free plan lets me reserve one domain permanently, which covers many personal development and testing projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  It isn't limited to React
&lt;/h2&gt;

&lt;p&gt;I started using PortPreview for a React app, but it works with anything listening on a local port, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  React / Vite&lt;/li&gt;
&lt;li&gt;  Next.js&lt;/li&gt;
&lt;li&gt;  Vue&lt;/li&gt;
&lt;li&gt;  Angular&lt;/li&gt;
&lt;li&gt;  Node.js&lt;/li&gt;
&lt;li&gt;  Express&lt;/li&gt;
&lt;li&gt;  NestJS&lt;/li&gt;
&lt;li&gt;  local APIs&lt;/li&gt;
&lt;li&gt;  webhook handlers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, I can expose an API running on port &lt;code&gt;3000&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx portpreview 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API can then receive external requests without being deployed. This is particularly handy for services that need to send requests back to my machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tunnels and preview deployments serve different jobs
&lt;/h2&gt;

&lt;p&gt;I still use preview deployments when a team needs a stable environment or when the app must stay online independently of my computer.&lt;/p&gt;

&lt;p&gt;A preview deployment looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code → commit → push → build → deploy → test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A local tunnel is shorter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code → localhost → test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a quick test, I usually prefer the second workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a permanent subdomain
&lt;/h2&gt;

&lt;p&gt;After trying the random tunnel URL, I wanted a stable address for the project. PortPreview lets me reserve a subdomain and use it when starting the tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx portpreview 3000 &lt;span class="nt"&gt;--subdomain&lt;/span&gt; my-project &lt;span class="nt"&gt;--token&lt;/span&gt; pp_live_...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the project a predictable URL instead of a new random address each time. I can also save the token instead of passing it with every command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx portpreview config set-token pp_live_...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, starting a tunnel takes one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx portpreview 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My normal workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run the React app
      ↓
npx portpreview 3000
      ↓
Open the public HTTPS URL
      ↓
Test on a real device
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a project needs a stable URL, I use the reserved subdomain. Once the initial setup is done, exposing a local app is a one-command job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;There are plenty of established tunneling tools, and no single option will suit everyone. For my React workflow, I wanted as little friction as possible: start the app, run one command, get an HTTPS URL, and open it on a real device.&lt;/p&gt;

&lt;p&gt;The permanent free hostname also solves one of the more irritating parts of using temporary tunnels regularly.&lt;/p&gt;

&lt;p&gt;If you're dealing with the same problem, take a look at &lt;a href="https://portpreview.com/" rel="noopener noreferrer"&gt;PortPreview&lt;/a&gt;. I'm also curious what other developers use for local React testing and webhook development.&lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>tunnel</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
