React is a JavaScript library for building user interfaces through reusable components. Built by Meta, it uses a virtual DOM to efficiently update the UI when data changes.
Next.js is a React meta-framework built by Vercel that adds server-side rendering, file-based routing, API routes, image optimization, and production-ready tooling on top of React.
UI library · Component-based · Virtual DOM · Hooks API · 200k+ stars on GitHub · Powers millions of apps
Full-stack framework · SSR/SSG/ISR · File routing · API routes · Edge runtime · App Router (v13+)
TypeScript adds static type checking to JavaScript. In React projects it catches prop mismatches, API shape errors, and refactoring regressions at compile time — not in production.
| JavaScript | TypeScript |
|---|---|
| Errors at runtime | Errors at compile time |
| No IntelliSense for props | Full autocomplete & docs |
| Refactoring is risky | Safe, automated refactoring |
| API shapes are implicit | API shapes are explicit interfaces |
JS renders everything in browser. Fast navigation after load. Poor SEO. Used for dashboards, apps behind auth.
HTML generated per request on server. Great SEO, fresh data. Slower TTFB. Used for e-commerce, social feeds.
HTML pre-built at deploy time. Blazing fast, CDN-cached. Used for blogs, docs, marketing pages.
Static pages revalidated in background. Best of SSG + SSR. Used for news sites, product pages.
- 1Install Node.js: Download Node.js 20 LTS from nodejs.org. This includes npm. Verify with
node -vandnpm -v. - 2Install pnpm (recommended):
npm install -g pnpm— faster installs, better monorepo support than npm. - 3Install VS Code: Use the ESLint, Prettier, and TypeScript extensions for the best DX.
# Create a Vite + React + TypeScript project pnpm create vite my-react-app --template react-ts cd my-react-app pnpm install pnpm dev # starts at http://localhost:5173
# Create Next.js 14 with TypeScript, Tailwind, App Router npx create-next-app@latest my-next-app \ --typescript \ --tailwind \ --app \ --eslint \ --src-dir cd my-next-app pnpm dev # starts at http://localhost:3000
src/ ├── app/ # App Router pages │ ├── (auth)/ # Route group (no URL segment) │ │ ├── login/page.tsx │ │ └── register/page.tsx │ ├── dashboard/ │ │ ├── layout.tsx # Nested layout │ │ └── page.tsx │ ├── api/ # API routes │ │ └── students/route.ts │ ├── globals.css │ ├── layout.tsx # Root layout │ └── page.tsx # Home page (/) ├── components/ │ ├── ui/ # Reusable UI primitives │ │ ├── Button.tsx │ │ └── Input.tsx │ └── features/ # Feature-specific components │ └── StudentCard.tsx ├── hooks/ # Custom hooks │ └── useStudents.ts ├── lib/ # Utilities, API clients │ ├── api.ts │ └── db.ts ├── types/ # Shared TypeScript types │ └── index.ts └── store/ # State management └── studentStore.ts
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 90,
"plugins": ["prettier-plugin-tailwindcss"]
}
JSX is JavaScript XML — a syntax extension that lets you write HTML-like markup inside JavaScript. React compiles it to React.createElement() calls.
// JSX rules: // 1. Return a single root element (or Fragment) // 2. Use className instead of class // 3. All tags must be closed // 4. JavaScript expressions go in { } const name = 'Alice'; const isActive = true; function WelcomeBanner() { return ( <div className="banner"> <h1>Hello, {name}!</h1> <p className={isActive ? 'active' : 'inactive'}> Status: {isActive ? 'Online' : 'Offline'} </p> <img src="/avatar.png" alt="User avatar" /> </div> ); }
Components are the building blocks of React UIs. Functional components are plain functions that return JSX. They're the modern standard (class components are legacy).
import React from 'react'; // Type definition for props interface StudentCardProps { name: string; grade: string; score: number; isEnrolled?: boolean; // optional prop } // Functional component — destructure props directly function StudentCard({ name, grade, score, isEnrolled = true }: StudentCardProps) { return ( <div className="student-card"> <h2>{name}</h2> <span>Grade: {grade} · Score: {score}/100</span> {isEnrolled && <span className="badge">Enrolled</span>} </div> ); } // Usage function App() { return <StudentCard name="Alice" grade="A" score={95} />; } export default StudentCard;
Props flow down from parent to child — they are read-only. State is data owned by a component that can change over time, triggering a re-render.
import { useState } from 'react'; // Props: passed from parent, immutable interface CounterProps { initialCount?: number; label: string; } function Counter({ initialCount = 0, label }: CounterProps) { // State: owned by this component, triggers re-render on change const [count, setCount] = useState<number>(initialCount); const increment = () => setCount(prev => prev + 1); const decrement = () => setCount(prev => prev - 1); const reset = () => setCount(initialCount); return ( <div> <p>{label}: <strong>{count}</strong></p> <button onClick={decrement}>−</button> <button onClick={reset}>Reset</button> <button onClick={increment}>+</button> </div> ); }
import { useState } from 'react'; function SearchBox() { const [query, setQuery] = useState(''); // TypeScript knows this is a React input change event const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { setQuery(e.target.value); }; const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); // stop page reload alert(`Searching for: ${query}`); }; const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Escape') setQuery(''); }; return ( <form onSubmit={handleSubmit}> <input type="text" value={query} onChange={handleChange} onKeyDown={handleKeyDown} placeholder="Search students..." /> <button type="submit">Search</button> </form> ); }
interface StatusBadgeProps { status: 'active' | 'inactive' | 'pending'; } function StatusBadge({ status }: StatusBadgeProps) { // Pattern 1: ternary — for two outcomes const color = status === 'active' ? 'green' : 'gray'; // Pattern 2: early return — guard clause if (status === 'inactive') return null; // render nothing // Pattern 3: object map — clean multi-case rendering const labels: Record<string, string> = { active: '✓ Active', pending: '⏳ Pending', }; return ( <span style={{ color }}> {/* Pattern 4: && short-circuit */} {status === 'pending' && <span>Review needed: </span>} {labels[status]} </span> ); }
interface Student { id: number; name: string; grade: string; } function StudentList({ students }: { students: Student[] }) { if (students.length === 0) { return <p>No students found.</p>; } return ( <ul> {students.map(student => ( // key must be a stable, unique value — never use array index! <li key={student.id}> <strong>{student.name}</strong> — {student.grade} </li> ))} </ul> ); }
import { useState } from 'react'; interface FormData { name: string; email: string; grade: 'A' | 'B' | 'C' | 'D' | 'F'; } function StudentForm({ onSubmit }: { onSubmit: (data: FormData) => void }) { const [form, setForm] = useState<FormData>({ name: '', email: '', grade: 'A' }); const [errors, setErrors] = useState<Partial<FormData>>({}); // Generic handler — updates any field by name const handleChange = ( e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> ) => { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); setErrors(prev => ({ ...prev, [name]: undefined })); // clear error }; const validate = (): boolean => { const newErrors: Partial<FormData> = {}; if (!form.name.trim()) newErrors.name = 'Name required'; if (!form.email.includes('@')) newErrors.email = 'Valid email required'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (validate()) onSubmit(form); }; return ( <form onSubmit={handleSubmit}> <input name="name" value={form.name} onChange={handleChange} /> {errors.name && <span className="error">{errors.name}</span>} <input name="email" value={form.email} onChange={handleChange} /> <select name="grade" value={form.grade} onChange={handleChange}> {['A','B','C','D','F'].map(g => ( <option key={g} value={g}>{g}</option> ))} </select> <button type="submit">Add Student</button> </form> ); }
import { useState, useEffect, useRef, useCallback } from 'react'; // ── useEffect: side effects & lifecycle ────────────── function StudentFetcher({ classId }: { classId: number }) { const [students, setStudents] = useState<Student[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { let cancelled = false; // prevent state update after unmount async function fetchStudents() { try { setLoading(true); const res = await fetch(`/api/students?classId=${classId}`); const data = await res.json(); if (!cancelled) setStudents(data); } catch (err) { if (!cancelled) setError('Failed to load students'); } finally { if (!cancelled) setLoading(false); } } fetchStudents(); return () => { cancelled = true; }; // cleanup }, [classId]); // re-run when classId changes if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error}</div>; return <StudentList students={students} />; } // ── useRef: access DOM elements or persist values ──── function AutoFocusInput() { const inputRef = useRef<HTMLInputElement>(null); useEffect(() => { inputRef.current?.focus(); // auto-focus on mount }, []); return <input ref={inputRef} placeholder="I'm focused automatically" />; } // ── Custom hook — reusable stateful logic ──────────── function useLocalStorage<T>(key: string, initial: T) { const [value, setValue] = useState<T>(() => { try { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : initial; } catch { return initial; } }); const setStored = useCallback((newValue: T) => { setValue(newValue); localStorage.setItem(key, JSON.stringify(newValue)); }, [key]); return [value, setStored] as const; } // Usage of custom hook function ThemeToggle() { const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Current: {theme} </button> ); }
// Shared types — import across your whole app export interface Student { id: number; name: string; email: string; age: number; grade: 'A' | 'B' | 'C' | 'D' | 'F'; createdAt: string; } export type CreateStudentDto = Omit<Student, 'id' | 'createdAt'>; export type UpdateStudentDto = Partial<CreateStudentDto>; // API response wrapper export interface ApiResponse<T> { data: T; message: string; success: boolean; pagination?: { page: number; total: number; limit: number; }; } // Component prop patterns export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'primary' | 'secondary' | 'danger'; loading?: boolean; children: React.ReactNode; } // Callback props export interface StudentListProps { students: Student[]; onSelect: (student: Student) => void; onDelete: (id: number) => Promise<void>; renderEmpty?: () => React.ReactNode; // render prop }
// INTERFACE — best for object shapes (can extend, implement) interface User { id: number; name: string; } // Declaration merging — can add fields later interface User { email: string; } // TYPE — best for unions, intersections, primitives type Role = 'admin' | 'teacher' | 'student'; type ID = number | string; // Intersection type (like extends) type AdminUser = User & { role: 'admin'; permissions: string[] }; // Utility types — use these constantly type PartialUser = Partial<User>; // all fields optional type RequiredUser = Required<User>; // all fields required type ReadonlyUser = Readonly<User>; // no mutations type UserWithoutId = Omit<User, 'id'>; // remove field type OnlyName = Pick<User, 'name'>;// keep field // Record type — typed dictionary type GradeMap = Record<string, 'A' | 'B' | 'C'>;
// Generic component — works with ANY data type interface ListProps<T> { items: T[]; renderItem: (item: T, index: number) => React.ReactNode; keyExtractor: (item: T) => string | number; emptyMessage?: string; } function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) { if (items.length === 0) { return <p>{emptyMessage ?? 'No items'}</p>; } return ( <ul> {items.map((item, i) => ( <li key={keyExtractor(item)}>{renderItem(item, i)}</li> ))} </ul> ); } // TypeScript infers T from usage — fully type-safe <List items={students} keyExtractor={s => s.id} renderItem={s => <span>{s.name}</span>} />
// Full reference of typed React event handlers const handlers = { onChange: (e: React.ChangeEvent<HTMLInputElement>) => {}, onSubmit: (e: React.FormEvent<HTMLFormElement>) => {}, onClick: (e: React.MouseEvent<HTMLButtonElement>) => {}, onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => {}, onFocus: (e: React.FocusEvent<HTMLInputElement>) => {}, onDragStart: (e: React.DragEvent<HTMLDivElement>) => {}, }; // Typed ref example const inputRef = useRef<HTMLInputElement>(null); const divRef = useRef<HTMLDivElement>(null); const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
Context provides a way to pass data through the component tree without prop drilling. Use it for global state like auth, theme, or language.
import { createContext, useContext, useState, useEffect } from 'react'; interface AuthContextType { user: User | null; isLoading: boolean; login: (email: string, password: string) => Promise<void>; logout: () => void; } // 1. Create the context const AuthContext = createContext<AuthContextType | null>(null); // 2. Create the provider export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { // Restore session on mount const token = localStorage.getItem('token'); if (token) fetchCurrentUser(token).then(setUser).finally(() => setIsLoading(false)); else setIsLoading(false); }, []); const login = async (email: string, password: string) => { const { user, token } = await loginApi(email, password); localStorage.setItem('token', token); setUser(user); }; const logout = () => { localStorage.removeItem('token'); setUser(null); }; return ( <AuthContext.Provider value={{ user, isLoading, login, logout }}> {children} </AuthContext.Provider> ); } // 3. Custom hook for consuming context export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error('useAuth must be inside AuthProvider'); return ctx; } // Usage in any component function NavBar() { const { user, logout } = useAuth(); return ( <nav> {user ? `Hello, ${user.name}` : 'Not logged in'} {user && <button onClick={logout}>Logout</button>} </nav> ); }
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom'; import { useAuth } from './context/AuthContext'; // Protected route wrapper function ProtectedRoute() { const { user, isLoading } = useAuth(); if (isLoading) return <div>Loading...</div>; if (!user) return <Navigate to="/login" replace />; return <Outlet />; // render child route } export default function App() { return ( <BrowserRouter> <Routes> <{/* Public routes */} <Route path="/" element={<Home />} /> <Route path="/login" element={<Login />} /> <{/* Protected routes */} <Route element={<ProtectedRoute />}> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/students" element={<Students />} /> <Route path="/students/:id" element={<StudentDetail />} /> </Route> <Route path="*" element={<NotFound />} /> </Routes> </BrowserRouter> ); }
// Define all possible actions as a discriminated union type StudentAction = | { type: 'FETCH_START' } | { type: 'FETCH_SUCCESS'; payload: Student[] } | { type: 'FETCH_ERROR'; payload: string } | { type: 'ADD_STUDENT'; payload: Student } | { type: 'UPDATE_STUDENT';payload: Student } | { type: 'DELETE_STUDENT';payload: number }; interface StudentState { students: Student[]; loading: boolean; error: string | null; } const initialState: StudentState = { students: [], loading: false, error: null }; function studentReducer(state: StudentState, action: StudentAction): StudentState { switch (action.type) { case 'FETCH_START': return { ...state, loading: true, error: null }; case 'FETCH_SUCCESS': return { ...state, loading: false, students: action.payload }; case 'FETCH_ERROR': return { ...state, loading: false, error: action.payload }; case 'ADD_STUDENT': return { ...state, students: [...state.students, action.payload] }; case 'UPDATE_STUDENT': return { ...state, students: state.students.map( s => s.id === action.payload.id ? action.payload : s )}; case 'DELETE_STUDENT': return { ...state, students: state.students.filter(s => s.id !== action.payload) }; default: return state; } } // Usage in component const [state, dispatch] = useReducer(studentReducer, initialState); dispatch({ type: 'ADD_STUDENT', payload: newStudent });
import { memo, useCallback, useMemo, lazy, Suspense } from 'react'; // React.memo — skip re-render if props unchanged const ExpensiveCard = memo(function ExpensiveCard({ student }: { student: Student }) { return <div>{student.name}</div>; }); function StudentDashboard() { const [students, setStudents] = useState<Student[]>([]); const [filter, setFilter] = useState('A'); // useMemo — recalculate only when students or filter changes const filteredStudents = useMemo( () => students.filter(s => s.grade === filter), [students, filter] ); // useCallback — stable function reference for child components const handleDelete = useCallback(async (id: number) => { await deleteStudent(id); setStudents(prev => prev.filter(s => s.id !== id)); }, []); // empty dep array = stable forever return ( <div> {filteredStudents.map(s => ( <ExpensiveCard key={s.id} student={s} /> ))} </div> ); } // Lazy loading — code split by route/component const Analytics = lazy(() => import('./Analytics')); function App() { return ( <Suspense fallback={<div>Loading analytics...</div>}> <Analytics /> </Suspense> ); }
Next.js 13+ App Router uses the filesystem as the router. Every folder inside app/ is a route segment; a page.tsx file makes it accessible.
// URL: /students/42 // This file is a Server Component by default interface PageProps { params: { id: string }; // dynamic segment searchParams: Record<string, string>; // ?query=value } // Server Component — runs on server, can fetch directly export default async function StudentPage({ params }: PageProps) { // Direct DB/API call — no useEffect needed! const student = await fetchStudent(parseInt(params.id)); if (!student) notFound(); // renders 404 page return ( <main> <h1>{student.name}</h1> <p>Grade: {student.grade}</p> </main> ); } // Generate page metadata (SEO) export async function generateMetadata({ params }: PageProps) { const student = await fetchStudent(parseInt(params.id)); return { title: student?.name ?? 'Student' }; }
// ── SERVER COMPONENT (default) ─────────────────────── // No "use client" directive // Can: async/await, access DB, no useState/useEffect export default async function StudentsPage() { // Runs on server at request time const students = await db.student.findMany({ orderBy: { name: 'asc' } }); return ( <div> <h1>Students ({students.length})</h1> {/* Client component for interactivity */} <ClientFilter students={students} /> </div> ); } // ── CLIENT COMPONENT ───────────────────────────────── 'use client'; // marks as client component import { useState } from 'react'; export function ClientFilter({ students }: { students: Student[] }) { const [query, setQuery] = useState(''); const filtered = students.filter(s => s.name.toLowerCase().includes(query)); return ( <> <input value={query} onChange={e => setQuery(e.target.value)} /> <StudentList students={filtered} /> </> ); }
Client Component: useState, useEffect, event listeners, browser APIs, animations.
import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; // GET /api/students?grade=A export async function GET(req: NextRequest) { try { const grade = req.nextUrl.searchParams.get('grade'); const students = await db.student.findMany({ where: grade ? { grade } : undefined }); return NextResponse.json({ success: true, data: students }); } catch (error) { return NextResponse.json({ success: false, error: 'Server error' }, { status: 500 }); } } // POST /api/students export async function POST(req: NextRequest) { try { const body: CreateStudentDto = await req.json(); // Validate with Zod const validated = createStudentSchema.parse(body); const student = await db.student.create({ data: validated }); return NextResponse.json({ success: true, data: student }, { status: 201 }); } catch (error) { if (error instanceof ZodError) { return NextResponse.json({ success: false, errors: error.errors }, { status: 400 }); } return NextResponse.json({ error: 'Server error' }, { status: 500 }); } } // app/api/students/[id]/route.ts export async function DELETE( req: NextRequest, { params }: { params: { id: string } } ) { await db.student.delete({ where: { id: parseInt(params.id) } }); return NextResponse.json({ success: true }); }
import { redirect } from 'next/navigation'; import { getServerSession } from 'next-auth'; export default async function DashboardLayout({ children, }: { children: React.ReactNode; }) { // Server-side auth check — runs before any page renders const session = await getServerSession(); if (!session) redirect('/login'); return ( <div className="flex h-screen"> <Sidebar /> <main className="flex-1 overflow-auto"> <TopNav user={session.user} /> <div className="p-6"> {children} {/* page.tsx renders here */} </div> </main> </div> ); }