React: A Complete Guide from Setup to Deployment
What is React?
React is a JavaScript library for building user interfaces, created and maintained by Meta (formerly Facebook). It's one of the most popular frontend frameworks in the world, used by companies like Netflix, Airbnb, Instagram, and countless others.
Core Concepts
Component-Based Architecture
React applications are built using components - reusable, self-contained pieces of UI. Think of components like LEGO blocks: each piece serves a specific purpose, and you combine them to build complex interfaces.
function Welcome() {
return <h1>Hello, World!</h1>;
}
Declarative UI
Instead of manually manipulating the DOM (like with vanilla JavaScript), you describe what the UI should look like based on the current state, and React handles the updates.
// You describe the UI based on state
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Virtual DOM
React maintains a virtual representation of the UI in memory. When state changes, React calculates the minimal set of changes needed and efficiently updates only what's necessary in the actual DOM.
JSX Syntax
React uses JSX, a syntax extension that looks like HTML but is actually JavaScript. It gets compiled to regular JavaScript function calls.
const element = <h1 className="title">Hello!</h1>;
// Compiles to:
const element = React.createElement('h1', {className: 'title'}, 'Hello!');
Why React?
- Massive Ecosystem: Thousands of libraries, tools, and resources
- Component Reusability: Write once, use everywhere
- Strong Community: Easy to find help and solutions
- Job Market: Highly demanded skill
- Performance: Virtual DOM makes updates efficient
- Developer Experience: Great tooling and debugging
Initializing a React Project
The Modern Way: React Router (Recommended)
For any React app that needs routing (which is most of them), I recommend starting with React Router from the beginning. It sets up everything you need with modern best practices.
npx create-react-router@latest my-react-router-app
This command:
- Creates a new directory called
my-react-router-app - Sets up a React project with Vite (fast build tool)
- Installs React Router v7 (the latest routing solution)
- Configures everything with sensible defaults
- Gives you a starter template to build from
During setup, you'll be asked:
- TypeScript or JavaScript? - Choose based on preference (TypeScript adds type safety)
- Install dependencies? - Say yes
cd my-react-router-app
npm run dev
Your app will be running at http://localhost:5173 (or another port if 5173 is taken).
Alternative: Create Vite (Manual Routing Setup)
If you want more control or aren't sure about routing yet:
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm install react-router-dom
npm run dev
This gives you a blank React app where you manually add React Router later.
Project Structure (React Router)
my-react-router-app/
├── node_modules/
├── public/
│ └── favicon.ico
├── app/
│ ├── routes/
│ │ ├── _index.tsx # Home page (/)
│ │ └── about.tsx # About page (/about)
│ ├── root.tsx # Root layout component
│ └── entry.client.tsx # Client entry point
├── package.json
├── vite.config.ts
└── tsconfig.json
The app/routes/ directory is where your pages live. React Router automatically creates routes based on the file structure.
Understanding Routing in React Router
What is Routing?
Routing is how your application responds to different URLs. When a user navigates to /about, you want to show the About page. When they go to /contact, show the Contact page. This all happens without page reloads - it's a Single Page Application (SPA).
Route Configuration with routes.ts
React Router v7 uses a centralized routes.ts file to define all your routes. This gives you a clear overview of your entire routing structure in one place.
Create app/routes.ts:
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/_index.tsx"),
route("about", "routes/about.tsx"),
route("blog", "routes/blog.tsx"),
route("blog/:slug", "routes/blog-post.tsx"),
route("contact", "routes/contact.tsx"),
] satisfies RouteConfig;
Route types:
index()- The index route for/route(path, file)- Regular route mapping a path to a component file:paramName- Dynamic parameter (e.g.,:slug,:userId)
Creating Your First Routes
1. Define routes in app/routes.ts:
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/_index.tsx"),
route("about", "routes/about.tsx"),
route("blog/:slug", "routes/blog-post.tsx"),
] satisfies RouteConfig;
2. Home Page (app/routes/_index.tsx):
export default function Index() {
return (
<div>
<h1>Welcome to My Website</h1>
<p>This is the home page</p>
</div>
);
}
3. About Page (app/routes/about.tsx):
export default function About() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company</p>
</div>
);
}
4. Blog with Dynamic Routes (app/routes/blog-post.tsx):
import { useParams } from 'react-router';
export default function BlogPost() {
const { slug } = useParams();
return (
<div>
<h1>Blog Post: {slug}</h1>
<p>This is the blog post for {slug}</p>
</div>
);
}
Now /blog/my-first-post and /blog/react-tutorial will both work, with different slug values.
Navigation Between Routes
React Router provides components for navigation:
Using <Link> (preferred):
import { Link } from 'react-router';
export default function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/blog">Blog</Link>
</nav>
);
}
import { useNavigate } from 'react-router';
export default function LoginForm() {
const navigate = useNavigate();
const handleSubmit = () => {
// After successful login
navigate('/dashboard');
};
return <button onClick={handleSubmit}>Login</button>;
}
Layouts and Nested Routes
The app/root.tsx file is your root layout - it wraps all other routes:
import { Links, Meta, Outlet, Scripts } from 'react-router';
export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
<footer>© 2024 My Website</footer>
<Scripts />
</body>
</html>
);
}
The <Outlet /> component is where child routes are rendered. This pattern lets you share layouts across multiple pages.
Creating nested routes with layouts:
First, create a layout component (app/routes/dashboard.tsx):
import { Outlet, Link } from 'react-router';
export default function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/settings">Settings</Link>
<Link to="/dashboard/profile">Profile</Link>
</aside>
<div className="content">
<Outlet /> {/* Nested routes render here */}
</div>
</div>
);
}
Then configure nested routes in app/routes.ts:
import { type RouteConfig, index, route, layout } from "@react-router/dev/routes";
export default [
index("routes/_index.tsx"),
// Dashboard with nested routes
layout("routes/dashboard.tsx", [
index("routes/dashboard-home.tsx"),
route("settings", "routes/dashboard-settings.tsx"),
route("profile", "routes/dashboard-profile.tsx"),
]),
] satisfies RouteConfig;
This creates:
/dashboard→ dashboard layout with dashboard-home content/dashboard/settings→ dashboard layout with settings content/dashboard/profile→ dashboard layout with profile content
Loading Data
React Router v7 provides a loader function for fetching data before rendering:
// app/routes.ts
export default [
route("blog/:slug", "routes/blog-post.tsx"),
] satisfies RouteConfig;
// app/routes/blog-post.tsx
import { useLoaderData } from 'react-router';
// This runs on the server or before navigation
export async function loader({ params }) {
const post = await fetch(`/api/posts/${params.slug}`).then(r => r.json());
return post;
}
export default function BlogPost() {
const post = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
The loader runs before the component renders, ensuring data is ready.
Error Handling
Add error boundaries to gracefully handle errors:
export function ErrorBoundary() {
const error = useRouteError();
return (
<div>
<h1>Oops! Something went wrong</h1>
<p>{error.message}</p>
</div>
);
}
Complete Routing Example
Here's a comprehensive example showing various routing patterns:
app/routes.ts:
import { type RouteConfig, index, route, layout } from "@react-router/dev/routes";
export default [
// Simple routes
index("routes/_index.tsx"),
route("about", "routes/about.tsx"),
route("contact", "routes/contact.tsx"),
// Dynamic routes
route("users/:userId", "routes/user.tsx"),
route("blog/:slug", "routes/blog-post.tsx"),
// Nested routes with layout
layout("routes/dashboard.tsx", [
index("routes/dashboard-home.tsx"),
route("settings", "routes/dashboard-settings.tsx"),
route("profile", "routes/dashboard-profile.tsx"),
]),
// Complex nested structure
route("admin", "routes/admin-layout.tsx", [
index("routes/admin-home.tsx"),
route("users", "routes/admin-users.tsx"),
route("posts", "routes/admin-posts.tsx"),
]),
] satisfies RouteConfig;
Simple route example (app/routes/about.tsx):
export default function About() {
return <h1>About</h1>;
}
Dynamic route example (app/routes/user.tsx):
import { useParams } from 'react-router';
export default function User() {
const { userId } = useParams();
return <h1>User {userId}</h1>;
}
Route with data loading (app/routes/blog-post.tsx):
import { useLoaderData } from 'react-router';
export async function loader({ params }) {
const post = await fetchPost(params.slug);
return post;
}
export default function BlogPost() {
const post = useLoaderData<typeof loader>();
return <article>{post.content}</article>;
}
Deploying Your React Router Project
Now let's deploy your React Router app to production using Docker and Caddy for automatic HTTPS.
Step 1: Build Configuration
React Router with Vite builds your app into static files. Your vite.config.ts should look like:
import { defineConfig } from 'vite';
import { reactRouter } from '@react-router/dev/vite';
export default defineConfig({
plugins: [reactRouter()],
});
When you run npm run build, it creates a build/ directory with optimized static files.
Step 2: Create a Dockerfile
Create a Dockerfile in your project root:
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build the app
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built files from builder stage
COPY --from=builder /app/build/client /usr/share/nginx/html
# Copy nginx config for SPA routing
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
This uses a multi-stage build:
- First stage builds your React app
- Second stage copies the built files into a lightweight Nginx container
- Final image only contains the production files (much smaller)
Step 3: Create Nginx Configuration
Create nginx.conf in your project root:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback: serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
}
The try_files directive is crucial for SPAs. It ensures that any route (like /about or /blog/my-post) serves index.html, letting React Router handle the routing.
Step 4: Create docker-compose.yml
Create docker-compose.yml in your project root:
version: '3.8'
services:
caddy:
image: caddy:2-alpine
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
depends_on:
- app
app:
build: .
container_name: react-app
restart: unless-stopped
networks:
- web
networks:
web:
driver: bridge
volumes:
caddy_data:
caddy_config:
Step 5: Create Caddyfile
Create a Caddyfile in your project root:
yourdomain.com {
reverse_proxy app:80
tls your-email@example.com
}
Replace:
yourdomain.comwith your actual domainyour-email@example.comwith your email (for Let's Encrypt notifications)
That's it! Caddy will automatically:
- Obtain SSL certificates
- Redirect HTTP to HTTPS
- Renew certificates before expiration
- Set proper security headers
Step 6: Project Structure
Your final project structure should look like:
my-react-router-app/
├── app/
│ ├── routes/
│ ├── root.tsx
│ └── entry.client.tsx
├── public/
├── Dockerfile
├── docker-compose.yml
├── Caddyfile
├── nginx.conf
├── package.json
├── vite.config.ts
└── tsconfig.json
Step 7: Deployment
On your local machine (test build):
# Test the Docker build locally
docker-compose build
# Run it locally
docker-compose up
Visit http://localhost to verify everything works.
On your production server:
-
Set up DNS:
A Record: yourdomain.com → Your.Server.IP.Address -
Push your code to GitHub:
git add . git commit -m "Add Docker deployment config" git push origin main -
On your server, clone and deploy:
# SSH into your server ssh user@your-server-ip # Clone your repository git clone https://github.com/yourusername/my-react-router-app.git cd my-react-router-app # Start the containers docker-compose up -d --build -
Check logs:
docker-compose logs -f
Within 30-60 seconds, Caddy will obtain an SSL certificate and your site will be live at https://yourdomain.com.
Step 8: Updates and Maintenance
To update your deployed app:
# Pull latest changes
git pull
# Rebuild and restart
docker-compose up -d --build
View logs:
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f app
docker-compose logs -f caddy
Restart services:
docker-compose restart
Stop everything:
docker-compose down
Deployment Variations
Multiple React Apps on One Server
You can host multiple React apps on the same server by adding more services:
Caddyfile:
portfolio.yourdomain.com {
reverse_proxy portfolio:80
tls your-email@example.com
}
blog.yourdomain.com {
reverse_proxy blog:80
tls your-email@example.com
}
app.yourdomain.com {
reverse_proxy mainapp:80
tls your-email@example.com
}
docker-compose.yml:
services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
portfolio:
build: ./portfolio
networks:
- web
blog:
build: ./blog
networks:
- web
mainapp:
build: ./mainapp
networks:
- web
networks:
web:
volumes:
caddy_data:
caddy_config:
Each subdomain gets its own SSL certificate and routes to a different React app.
Environment Variables
Pass environment variables to your React app:
docker-compose.yml:
services:
app:
build: .
environment:
- VITE_API_URL=https://api.yourdomain.com
- VITE_ENVIRONMENT=production
In your React code:
const API_URL = import.meta.env.VITE_API_URL;
fetch(`${API_URL}/data`)
.then(response => response.json())
.then(data => console.log(data));
Note: Vite requires environment variables to be prefixed with VITE_ to be exposed to client-side code.
Common Issues and Solutions
Issue: Routes return 404 on refresh
Cause: Nginx doesn't have the SPA fallback configuration.
Solution: Make sure your nginx.conf includes:
location / {
try_files $uri $uri/ /index.html;
}
Issue: Caddy can't obtain certificate
Causes:
- DNS not pointing to your server
- Ports 80/443 blocked by firewall
- Domain is too new (DNS not propagated)
Solutions:
# Check DNS
nslookup yourdomain.com
# Check firewall
sudo ufw status
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Check Caddy logs
docker-compose logs caddy
Issue: Build fails in Docker
Cause: Usually dependency issues or insufficient memory.
Solutions:
# Clear Docker cache
docker-compose build --no-cache
# Increase Docker memory (Docker Desktop)
# Settings → Resources → Memory → Increase to 4GB+
Issue: App works locally but not in Docker
Cause: Environment-specific code or missing environment variables.
Solution: Test your Docker build locally:
docker-compose up
# Visit http://localhost
# Check browser console for errors
Best Practices
-
Always use
.dockerignore:node_modules npm-debug.log .git .env dist build -
Keep secrets out of your repo:
# Use .env file (add to .gitignore) echo "VITE_API_KEY=secret123" > .env -
Use health checks:
app: build: . healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 30s timeout: 10s retries: 3 -
Monitor your logs:
# Set up log rotation docker-compose logs --tail=100 -f -
Backup your Caddy data volume:
docker run --rm -v caddy_data:/data -v $(pwd):/backup alpine tar czf /backup/caddy-backup.tar.gz /data
Conclusion
You now have:
- A solid understanding of React and its core concepts
- A React Router project initialized with modern best practices
- File-based routing with layouts and data loading
- A production-ready deployment using Docker and Caddy
- Automatic HTTPS with zero configuration
- The ability to host multiple apps on one server
This setup scales from personal projects to professional applications. The combination of React Router's developer experience and Caddy's simplicity makes deployment painless and maintainable.
Happy building! 🚀