Skip to main content

Deploy a React SPA to CloudBase Hosting

In one sentence: Build your project with Vite + React to produce a dist folder, then run tcb hosting deploy dist -e <env-id> to upload it to CloudBase static website Hosting in one shot — you get CDN acceleration, HTTPS, and custom domain support automatically; when using React Router with BrowserRouter, set the error page to index.html in the Console to resolve refresh 404s.

Estimated time: 15 minutes | Difficulty: Beginner

Applicable Scenarios

This recipe covers the most common React frontend deployment scenario: a pure SPA with no SSR and no server-side logic, where the build output is a set of static files. CloudBase Hosting is backed by COS + CDN and billed by storage and traffic — no container instance fees.

  • Applicable: React 18+ + Vite or Create React App SPA
  • Applicable: China-hosted deployment that requires ICP filing + HTTPS
  • Applicable: cost-sensitive projects that do not need a server-side process
  • Not applicable: SSR / SSG / Next.js server-side rendering — use CloudBase Run instead, see deploy-nextjs-to-cloudbase-run
  • Not applicable: projects that need a BFF or backend API — use CloudBase Run or Cloud Functions

Hosting vs. CloudBase Run

One-line distinction: Hosting = static files + CDN distribution; CloudBase Run = containerized server-side process.

DimensionHostingCloudBase Run
Deployment artifactStatic files (HTML/CSS/JS/images)Container image (Dockerfile)
RuntimeNone (CDN serves directly from COS)Node.js / Python / custom runtime process
BillingStorage + trafficInstance uptime + traffic
Best forSPA / docs site / landing pageSSR / API / WebSocket / long connections

A React SPA produces exactly two kinds of artifacts — one HTML file + one JS bundle + other static assets — all of which are cheapest to serve via Hosting.

Prerequisites

DependencyVersion
Node.js≥ 18 (minimum requirement for Vite 5/6)
React18+
Vite or CRAVite 5+ / Create React App 5+
react-router-dom6+ (if using routing)
@cloudbase/clilatest (v3.x at time of writing)
A CloudBase environment with Hosting enabled

Install and log in to the CLI globally:

npm i -g @cloudbase/cli
tcb login

Step 1: Configure the React Project

Before deploying a SPA to Hosting, two settings must be confirmed, otherwise you will hit classic problems like refresh 404s and asset 404s in Step 4.

1.1 Vite base Configuration

vite.config.ts (or vite.config.js):

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
base: '/', // use '/' when deploying to the root of your domain
// base: '/my-app/', // use '/my-app/' if deploying to a sub-path (e.g. default domain /my-app/)
build: {
outDir: 'dist',
},
});

base controls the prefix added to asset references in the built HTML. If you deploy to the root of the default domain (https://<env-id>.tcloudbaseapp.com/), use /. If you deploy to a sub-path with tcb hosting deploy dist /my-app -e <env-id>, change it to /my-app/both leading and trailing slashes are required.

For Create React App projects, the equivalent field is homepage in package.json, with the same meaning (CRA build output goes to build/ instead of dist/; the rest of this recipe uses Vite as the example).

1.2 React Router basename

React Router 6+ supports two authoring styles. In both, basename must exactly match the base set in Vite.

Classic style (BrowserRouter):

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';

function App() {
return (
<BrowserRouter basename="/">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}

export default App;

Modern style (createBrowserRouter, recommended):

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';

const router = createBrowserRouter(
[
{ path: '/', element: <Home /> },
{ path: '/about', element: <About /> },
],
{ basename: '/' },
);

function App() {
return <RouterProvider router={router} />;
}

export default App;

Both styles are equivalent, but createBrowserRouter supports the data API (loader / action) and is what React Router recommends for new projects.

A mismatch between basename and Vite's base produces the confusing symptom where the home page loads but navigating to /about results in every asset returning 404 — the same trap as in the Vue Hosting recipe.

If you want to avoid the complexity of history mode entirely, you can use createHashRouter (or HashRouter) instead. URLs will look like /#/about, and no server-side fallback configuration is needed. However, hash-based URLs are not SEO-friendly and are generally not recommended for production.

Step 2: Build dist

npm install
npm run build

After the build completes, a dist/ folder appears in the project root:

dist/
├── index.html
├── assets/
│ ├── index-a1b2c3d4.js
│ ├── index-e5f6g7h8.css
│ └── ...(various chunks and static assets)
└── favicon.ico

Open dist/index.html and verify: all <script> and <link> src/href values should begin with your configured base. If base is /my-app/, the HTML should reference /my-app/assets/index-xxxxx.js, not /assets/index-xxxxx.js. If the paths are wrong, fix vite.config.ts and rebuild.

Step 3: Deploy to CloudBase Hosting

The CloudBase CLI offers two deployment paths depending on your workflow.

tcb app deploy runs the full pipeline — install dependencies → build → upload artifacts → configure routing — in a single command, with the build happening in the cloud:

tcb app deploy --framework react -e <env-id>

The CLI reads name from package.json as the application name and infers the build command (npm run build) and output directory (./dist) from --framework react. On the first run it prompts you to confirm these parameters interactively, then writes them back to cloudbaserc.json. Subsequent deployments only need tcb app deploy.

Common parameters:

ParameterDescription
--frameworkFramework type: vite / vue / react / next / nuxt / angular / static
-e, --env-id <envId>Target environment ID
--build-command <cmd>Custom build command, overrides the framework default
--output-dir <dir>Build output directory; Vite defaults to ./dist, CRA defaults to ./build
--deploy-path <path>Hosting mount path, defaults to /service-name

Full parameter reference: tcb app deploy command docs.

Path B: tcb hosting deploy (upload an existing dist directly)

If you want to keep the build local (e.g. CI has already run npm run build), you can upload dist/ directly:

# In the project root, after building locally
npm run build

# Deploy the entire dist directory to the cloud root
tcb hosting deploy dist -e <env-id>

# Or deploy to a sub-path
tcb hosting deploy dist /my-app -e <env-id>

tcb hosting deploy <localPath> [cloudPath] is a file-level tool: it does not run install or build, only upload. Full command reference:

CommandPurpose
tcb hosting deploy <localPath> [cloudPath] -e <env-id>Upload a file or folder to the specified path
tcb hosting list -e <env-id>List files on the cloud
tcb hosting detail -e <env-id>View service info (default domain / status)
tcb hosting delete <cloudPath> -e <env-id>Delete a specific file; add --dir to delete a folder, --force to skip confirmation, --dry-run to preview
tcb hosting delete -e <env-id>Clear all files in Hosting

Upload Limits

  • Maximum file size: 50 TB; no limit on the number of files.
  • If you see socket hang up / ECONNRESET during upload, the network middleware is likely terminating the SDK's keep-alive connection. Disable it and retry:
    export COS_SDK_KEEPALIVE=false
    tcb hosting deploy dist -e <env-id>

Full command documentation: CLI Hosting Commands.

Step 4: Configure the SPA Fallback

This step is required for any React Router BrowserRouter / createBrowserRouter deployment on Hosting. Skipping it produces this symptom: the home page loads, but any sub-route (/about, /users/123) returns 404 on direct access or refresh.

Why: history mode uses pushState to change the URL, but physically there is only one file on the cloud — index.html. When the CDN receives a request for /about, it cannot find /about.html and returns 404 by default.

Fix: configure the 4xx error page to serve index.html, which hands routing back to the frontend.

Steps:

  1. Go to CloudBase Console → Static Website Hosting
  2. Switch to the "Settings" tab
  3. Find the "Error Page" configuration
  4. Set the 4xx error page to index.html
  5. Save

After this change, accessing <default-domain>/about returns index.html with a 200 status code (note: 200, not a 404 with a modified response body). React Router then parses the URL and renders the correct route.

See the "Redirect Rules → Error Code Redirect" section in the Hosting Console Management documentation.

Step 5: Custom Domain + HTTPS

The default domain <env-id>.xxx.tcloudbaseapp.com works, but it has access rate limits. Production deployments should always use a custom domain.

Prerequisites

  1. ICP filing: Domains used for China-hosted access must complete ICP filing on Tencent Cloud first (individual filing takes roughly 7–20 days; enterprise is similar).
  2. SSL certificate: Apply for a free DV certificate in the SSL Certificate Console, or upload an existing certificate.

Configuration Steps

  1. Go to CloudBase Console → HTTP Access Service
  2. Click "Add Domain" and enter your custom domain (e.g. app.example.com)
  3. Upload an SSL certificate or select an existing one
  4. For CDN type, select "CloudBase CDN" (optimized for Hosting, configures CDN acceleration automatically)
  5. After adding, the Console provides a CNAME value — copy it
  6. Go to your DNS provider (Tencent Cloud DNS / Alibaba Cloud / Cloudflare, etc.) and add a CNAME record pointing to that value
  7. Wait 3–5 minutes for DNS to propagate globally; the Console status changes to "Active" once complete

The full process including a comparison of the three CDN types is covered in Custom Domains.

Cache Configuration (Optional)

Under Console "Settings → Cache Configuration", it is recommended to configure cache TTLs by resource type:

Resource TypeRecommended TTL
Images, fonts30 days
CSS / JS (with hash suffix)7 days
index.html1 hour

index.html is intentionally set short — otherwise, after a new deployment, users whose browsers have cached the old HTML will still reference old JS bundle filenames and not see the update.

Verification

# 1. Home page via default domain (should return 200 + HTML)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/

# 2. Sub-route direct access (with fallback configured, should return 200, not 404)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/about

# 3. Static asset (confirms base is configured correctly)
curl -I https://<env-id>.xxx.tcloudbaseapp.com/assets/index-xxxxx.js

Also verify in the browser:

  1. Open the default domain home page and navigate through all routes (/about/users/123) — everything should work.
  2. On the /about page, press F5 to refresh directly — this must not return 404. This is the key test for confirming the fallback is configured correctly.
  3. Once the custom domain DNS is active, repeat the tests with https://app.example.com.

Common Errors

SymptomCauseFix
tcb hosting deploy ./dist -e xxx reports ENOENT: no such file or directoryThe dist path is wrong, or the current directory is not the project root; CRA build output is in build/, not dist/cd to the project root and confirm ls dist (or ls build for CRA) shows index.html before deploying
Home page loads, but refreshing /about returns 404SPA fallback not configured (Step 4)In Console "Settings → Error Page", set 4xx to index.html
Home page opens but all CSS/JS return 404; Network tab shows wrong asset pathsvite.config.ts base does not match the actual cloudPath used during deploymentUse / for root deployment, /my-app/ for sub-path /my-app; rebuild and redeploy after changing
Sub-route refresh returns 200 but page is blank; Network shows JS 404BrowserRouter basename / createBrowserRouter basename does not match vite.config.ts baseSet both to exactly the same string (/ or /my-app/); rebuild and redeploy
CRA project assets return 404 after deploymentCRA homepage field not set; build output contains wrong asset pathsAdd "homepage": "." or "homepage": "/my-app" to package.json, then re-run npm run build
Custom domain shows ERR_CERT_COMMON_NAME_INVALIDThe SSL certificate is bound to a different domain, or the certificate has expiredRe-issue a matching certificate in the SSL Certificate Console and re-bind it
Custom domain DNS still resolves to old IPDNS has not switched to CloudBase's CNAME, or local DNS cache is staleRun nslookup app.example.com to confirm the CNAME points to Tencent Cloud's CDN domain; flush local cache on macOS with dscacheutil -flushcache
Custom domain returns 403Domain has not completed ICP filing, or the filing has expiredCheck filing status at the ICP filing system; file if not yet registered
socket hang up / ECONNRESET during bulk uploadCOS SDK keep-alive connection terminated by network middlewareRun export COS_SDK_KEEPALIVE=false and retry

Deployment error codes are listed in full at Error Code Reference.

Next Steps

Once your React SPA is live, the natural next step is connecting it to a backend. Put your LLM proxy in a Cloud Function to avoid exposing API keys in the browser — see connect-openai-api-cloud-function; add CloudBase authentication to your React site with add-auth-web-with-cloudbase-sdk, reusing the same environment; for a multi-environment (dev / staging / prod) secrets layering strategy that works with tcb hosting deploy in CI, see secure-secrets-in-cloud-function.