React is the most popular JavaScript library for building user interfaces. In this guide we will build a production-quality web application from scratch using the modern React ecosystem. With up-to-date tools such as Vite, React Router, Zustand and TanStack Query we will create a foundation you can use in real-world projects.

Starting a Project: Vite vs Create React App

Related guides: Software development processes · PostgreSQL optimization · Advanced Git commands · What is Redis, and how to use it · Deploying with Docker

Create React App (CRA) is no longer officially recommended. With its ESBuild-based build system, Vite offers a far faster development experience. Hot Module Replacement (HMR) happens within milliseconds.

FeatureCreate React AppVite
Dev server startup~10-30 seconds~300ms
HMR time1-5 seconds~50ms
Build toolsWebpackESBuild + Rollup
ConfigurationHidden (requires eject)Open and simple
Official support (2026)DiscontinuedActively developed
TypeScriptExtra setupBuilt-in support
bash
# Create a React + TypeScript project with Vite
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev   # opens http://localhost:5173
text
my-app/
├── public/
│   └── vite.svg
├── src/
│   ├── assets/
│   ├── components/
│   │   ├── ui/           # Generic UI components
│   │   └── layout/       # Header, Footer, Sidebar
│   ├── pages/            # Page components
│   ├── hooks/            # Custom hooks
│   ├── services/         # API calls
│   ├── store/            # State management
│   ├── types/            # TypeScript types
│   ├── utils/            # Helper functions
│   ├── App.tsx
│   └── main.tsx
├── .env
├── vite.config.ts
└── package.json

Component Architecture

In React everything is a component. A good component structure follows the single responsibility principle and should be reusable and testable. It receives data through props and manages its own internal state.

tsx
// src/components/ui/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from 'react';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  loading?: boolean;
  children: ReactNode;
}

export function Button({
  variant = 'primary',
  size = 'md',
  loading = false,
  children,
  className = '',
  disabled,
  ...props
}: ButtonProps) {
  const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors';
  const variantClasses = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
    danger: 'bg-red-600 text-white hover:bg-red-700'
  };
  const sizeClasses = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  };

  return (
    <button
      className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}
      disabled={disabled || loading}
      {...props}
    >
      {loading && <span className="animate-spin mr-2">⟳</span>}
      {children}
    </button>
  );
}

React Hooks in Depth

Hooks let you use React features such as state, side effects and context in functional components. The most widely used hooks are useState, useEffect, useMemo, useCallback and useRef.

Writing a Custom Hook

By moving repeated logic into custom hooks you can keep your code DRY (Don't Repeat Yourself). Below is a handy useFetch hook for API calls:

tsx
// src/hooks/useFetch.ts
import { useState, useEffect } from 'react';

interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
  refetch: () => void;
}

export function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [trigger, setTrigger] = useState(0);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);
    setError(null);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(json => setData(json))
      .catch(err => {
        if (err.name !== 'AbortError') setError(err.message);
      })
      .finally(() => setLoading(false));

    return () => controller.abort();
  }, [url, trigger]);

  const refetch = () => setTrigger(t => t + 1);
  return { data, loading, error, refetch };
}

State Management: Zustand

For global state management, Zustand requires far less boilerplate than Redux. It is an ideal choice for small and medium-sized projects, and TypeScript support is built in.

bash
npm install zustand
tsx
// src/store/authStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: number;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  login: (user: User, token: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      token: null,
      isAuthenticated: false,

      login: (user, token) => set({
        user,
        token,
        isAuthenticated: true
      }),

      logout: () => set({
        user: null,
        token: null,
        isAuthenticated: false
      })
    }),
    { name: 'auth-storage' } // saves to localStorage
  )
);

// Usage:
// const { user, login, logout } = useAuthStore();
// login({ id: 1, name: 'Jane', email: 'jane@test.com' }, 'jwt-token');

API Integration

Collecting API calls in a central service layer makes maintenance easier. You can create an API client with Axios or native fetch and manage all your endpoints from there.

tsx
// src/services/api.ts
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';

async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
  const token = localStorage.getItem('auth-storage');
  const parsed = token ? JSON.parse(token) : null;

  const res = await fetch(`${API_URL}${endpoint}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(parsed?.state?.token && {
        Authorization: `Bearer ${parsed.state.token}`
      }),
      ...options.headers
    }
  });

  if (!res.ok) {
    const error = await res.json().catch(() => ({ error: 'Server error' }));
    throw new Error(error.error || `HTTP ${res.status}`);
  }

  return res.json();
}

// API endpoints
export const api = {
  auth: {
    login: (data: { email: string; password: string }) =>
      request('/auth/login', { method: 'POST', body: JSON.stringify(data) }),
    register: (data: { name: string; email: string; password: string }) =>
      request('/auth/register', { method: 'POST', body: JSON.stringify(data) })
  },
  users: {
    list: (page = 1) => request(`/users?page=${page}`),
    get: (id: number) => request(`/users/${id}`),
    update: (id: number, data: any) =>
      request(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) })
  }
};

Page Routing with React Router

bash
npm install react-router-dom
tsx
// src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { useAuthStore } from './store/authStore';
import Layout from './components/layout/Layout';
import Home from './pages/Home';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import NotFound from './pages/NotFound';

// Protected route component
function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const isAuthenticated = useAuthStore(s => s.isAuthenticated);
  return isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
}

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route element={<Layout />}>
          <Route path="/" element={<Home />} />
          <Route path="/login" element={<Login />} />
          <Route
            path="/dashboard"
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            }
          />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

Build and Deploy

Vite creates the production build with Rollup. It applies automatic code splitting, tree shaking and minification. The output is written to the dist/ folder and can be hosted on any static file server.

bash
# Production build
npm run build

# Test the build output
npm run preview  # http://localhost:4173

# Analyse the build file sizes
npx vite-bundle-visualizer
typescript
// vite.config.ts — production optimizations
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    target: 'es2020',
    minify: 'terser',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom']
        }
      }
    }
  },
  server: {
    proxy: {
      '/api': 'http://localhost:3000'
    }
  }
});

Performance Tips

  • Apply code splitting with React.lazy() and Suspense — each page is loaded as a separate chunk
  • Prevent unnecessary re-renders with useMemo and useCallback
  • Serve images in WebP/AVIF format and with lazy loading
  • Optimize long lists with virtualization (react-window/tanstack-virtual)
  • Analyse render performance with the React DevTools Profiler
  • Watch the bundle size — aim to keep the vendor chunk under 200KB
tsx
// Lazy loading example
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div className="p-8 text-center">Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Modern Software Development and DevOps Practices

A professional software development process rests on three pillars: source control (Git + a GitHub/GitLab pull request flow with mandatory code review), a CI/CD pipeline (automated test + lint + build + deploy), and observability (collecting logs, metrics and traces with Sentry/Datadog/Grafana). Guaranteeing code quality with the test pyramid (unit > integration > e2e), using Docker containers and Kubernetes orchestration in a microservice architecture, and keeping an OpenAPI/GraphQL Schema contract when designing a REST or GraphQL API are the modern standards. Throughout the software development life cycle (requirements → design → implementation → test → deploy → maintenance), Agile/Scrum sprints run 1-2 weeks and DevOps teams work on the principle of continuous delivery.