Skip to main content

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:

  1. Creates a new directory called my-react-router-app
  2. Sets up a React project with Vite (fast build tool)
  3. Installs React Router v7 (the latest routing solution)
  4. Configures everything with sensible defaults
  5. 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

After creation, navigate to your project:

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):

Using useNavigate (programmatic navigation):

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:

  1. First stage builds your React app
  2. Second stage copies the built files into a lightweight Nginx container
  3. 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.com with your actual domain
  • your-email@example.com with 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:

  1. Set up DNS:

    A Record: yourdomain.com → Your.Server.IP.Address
    
  2. Push your code to GitHub:

    git add .
    git commit -m "Add Docker deployment config"
    git push origin main
    
  3. 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
    
  4. 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

  1. Always use .dockerignore:

    node_modules
    npm-debug.log
    .git
    .env
    dist
    build
    
  2. Keep secrets out of your repo:

    # Use .env file (add to .gitignore)
    echo "VITE_API_KEY=secret123" > .env
    
  3. Use health checks:

    app:
      build: .
      healthcheck:
        test: ["CMD", "curl", "-f", "http://localhost"]
        interval: 30s
        timeout: 10s
        retries: 3
    
  4. Monitor your logs:

    # Set up log rotation
    docker-compose logs --tail=100 -f
    
  5. 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! 🚀