React is the world's most-used UI library — over 40 % of frontend developers use it daily. This roadmap shows exactly what to learn, in what order, with realistic timelines and the tools that actually matter in 2025.
At a glance
| Phase |
Topic |
Timeline |
| 1 |
JavaScript prerequisites |
Weeks 1–4 |
| 2 |
React fundamentals |
Weeks 5–8 |
| 3 |
Hooks in depth |
Weeks 9–11 |
| 4 |
State management |
Weeks 12–14 |
| 5 |
Routing & data fetching |
Weeks 15–17 |
| 6 |
Styling & component libraries |
Weeks 18–19 |
| 7 |
Testing |
Weeks 20–22 |
| 8 |
Next.js & SSR |
Weeks 23–26 |
| 9 |
Performance & accessibility |
Weeks 27–29 |
| 10 |
Portfolio & job prep |
Weeks 30–36 |
Phase 1 — JavaScript prerequisites (Weeks 1–4)
React is just JavaScript. These are non-negotiable before touching React:
// Arrow functions
const double = x => x * 2;
// Destructuring
const { name, age } = user;
const [first, ...rest] = items;
// Spread
const updated = { ...user, age: 31 };
// Array methods
items.map(x => x * 2).filter(x => x > 5).reduce((acc, x) => acc + x, 0);
// Modules
import { useState } from "react";
export default function App() {}
// Optional chaining & nullish coalescing
const city = user?.address?.city ?? "Unknown";
// Async/await
const data = await fetch("/api/users").then(r => r.json());
| Concept |
Why it matters in React |
this binding |
Class components (still exists in legacy code) |
| Closures |
Hook state captures, event handlers |
| Array methods |
Rendering lists, state transformations |
| ES modules |
Every React file uses import/export |
| Promises / async |
Data fetching in useEffect |
| Spread operator |
Immutable state updates |
Checkpoint: Build a small vanilla JS app (todo list, weather app) before starting React.
Phase 2 — React fundamentals (Weeks 5–8)
JSX
// JSX compiles to React.createElement calls
function Welcome({ name }) {
return (
<div className="card"> {/* class → className */}
<h1>Hello, {name}!</h1>
{name && <p>Welcome back.</p>} {/* conditional rendering */}
</div>
);
}
Components & props
// Functional component (the only kind you need in 2025)
function Button({ label, onClick, variant = "primary" }) {
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
</button>
);
}
// Usage
<Button label="Save" onClick={handleSave} variant="secondary" />
Rendering lists
function UserList({ users }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li> // key required for reconciliation
))}
</ul>
);
}
Component types
| Pattern |
When to use |
| Presentational component |
Pure UI — takes props, returns JSX |
| Container component |
Fetches data, passes to presentational |
| Compound component |
Flexible API (<Select> + <Select.Option>) |
| Higher-order component |
Wraps component to add behaviour (legacy — prefer hooks) |
Phase 3 — Hooks in depth (Weeks 9–11)
useState
function Counter() {
const [count, setCount] = useState(0);
// Always use functional update when next state depends on prev
const increment = () => setCount(prev => prev + 1);
return <button onClick={increment}>{count}</button>;
}
useEffect
function Profile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => { if (!cancelled) setUser(data); });
return () => { cancelled = true; }; // cleanup — prevents stale updates
}, [userId]); // re-runs when userId changes
return user ? <div>{user.name}</div> : <Spinner />;
}
Common useEffect mistakes:
| Mistake |
Fix |
| Missing dependency |
Add to deps array or use useCallback |
| No cleanup for subscriptions |
Return cleanup function |
| Fetch with no cancellation |
Use AbortController or flag |
Setting state in useEffect for derived values |
Compute during render instead |
useRef
function FocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
// Also use useRef to persist mutable values across renders without triggering re-render
const timerRef = useRef(null);
return <input ref={inputRef} />;
}
useMemo and useCallback
// useMemo — memoize expensive computation
const sorted = useMemo(
() => [...items].sort((a, b) => a.name.localeCompare(b.name)),
[items]
);
// useCallback — stable function reference (for child component optimization)
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
Rule of thumb: Don't add useMemo/useCallback everywhere — profile first, then optimise.
Custom hooks
// Extract stateful logic into reusable hooks
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
return JSON.parse(localStorage.getItem(key)) ?? initialValue;
} catch {
return initialValue;
}
});
const set = useCallback(newValue => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
}, [key]);
return [value, set];
}
// Usage
const [theme, setTheme] = useLocalStorage("theme", "light");
Core hooks quick reference
| Hook |
Purpose |
useState |
Local component state |
useEffect |
Side effects (fetch, subscriptions, DOM) |
useRef |
DOM ref or mutable value without re-render |
useMemo |
Memoize expensive computation |
useCallback |
Stable callback reference |
useContext |
Read from context |
useReducer |
Complex state with actions |
useId |
Stable unique ID for accessibility |
useTransition |
Mark updates as non-urgent |
useDeferredValue |
Defer slow-rendering value |
Phase 4 — State management (Weeks 12–14)
When to use what
| Solution |
Best for |
useState |
Local, simple state |
useReducer |
Complex local state with multiple actions |
Context API |
Low-frequency global state (theme, auth user) |
| Zustand |
Medium apps — simple API, no boilerplate |
| TanStack Query |
Server state (fetching, caching, sync) |
| Jotai |
Atomic state, fine-grained reactivity |
| Redux Toolkit |
Large apps with complex state logic, existing Redux |
Zustand — recommended for client state
// store.js
import { create } from "zustand";
const useCartStore = create(set => ({
items: [],
addItem: item => set(state => ({ items: [...state.items, item] })),
removeItem: id => set(state => ({ items: state.items.filter(i => i.id !== id) })),
total: 0,
}));
// Component
function Cart() {
const { items, removeItem } = useCartStore();
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => removeItem(item.id)}>Remove</button>
</li>
))}
</ul>
);
}
TanStack Query — recommended for server state
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
function UserList() {
const qc = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
staleTime: 60_000, // treat as fresh for 1 min
});
const deleteMutation = useMutation({
mutationFn: id => fetch(`/api/users/${id}`, { method: "DELETE" }),
onSuccess: () => qc.invalidateQueries({ queryKey: ["users"] }),
});
if (isLoading) return <Spinner />;
if (error) return <Error message={error.message} />;
return data.map(u => (
<div key={u.id}>
{u.name}
<button onClick={() => deleteMutation.mutate(u.id)}>Delete</button>
</div>
));
}
Phase 5 — Routing & data fetching (Weeks 15–17)
React Router v6
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link>
<Link to="/users">Users</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
function UserDetail() {
const { id } = useParams();
const navigate = useNavigate();
// ...
}
Protected routes
function ProtectedRoute({ children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
return children;
}
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
Phase 6 — Styling & component libraries (Weeks 18–19)
| Approach |
Library |
When to use |
| Utility-first CSS |
Tailwind CSS |
Fast prototyping, consistent design system |
| CSS Modules |
Built-in (Vite/CRA) |
Scoped styles, no naming conflicts |
| CSS-in-JS |
styled-components, Emotion |
Dynamic styles based on props |
| Component library |
shadcn/ui, MUI, Radix |
Pre-built accessible components |
Tailwind + shadcn/ui (2025 standard)
// shadcn/ui button example
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
function LoginForm() {
return (
<form className="flex flex-col gap-4 p-6 max-w-sm mx-auto">
<Input type="email" placeholder="Email" />
<Input type="password" placeholder="Password" />
<Button type="submit">Sign In</Button>
</form>
);
}
Phase 7 — Testing (Weeks 20–22)
Testing pyramid for React
| Level |
Tool |
What to test |
| Unit |
Vitest |
Pure functions, hooks |
| Component |
React Testing Library |
Component behaviour from user's POV |
| E2E |
Playwright |
Full user journeys |
React Testing Library
// Don't test implementation details — test what the user sees
import { render, screen, userEvent } from "@testing-library/react";
import { Counter } from "./Counter";
test("increments count on click", async () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /increment/i }));
expect(screen.getByText("1")).toBeInTheDocument();
});
Testing hooks with renderHook
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "./useCounter";
test("useCounter increments", () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
Phase 8 — Next.js & SSR (Weeks 23–26)
When to move from React SPA to Next.js
| Need |
Solution |
| SEO |
Next.js (SSR/SSG) |
| Fast initial load |
Next.js (streaming SSR) |
| API routes in same repo |
Next.js API routes / Server Actions |
| Static site |
Next.js SSG or Astro |
| Pure client-side SPA |
Vite + React |
Next.js App Router basics
app/
├── layout.tsx # shared layout (Server Component)
├── page.tsx # route "/"
├── (marketing)/ # route group — no URL segment
│ └── about/
│ └── page.tsx # route "/about"
└── users/
├── page.tsx # "/users"
└── [id]/
└── page.tsx # "/users/:id"
// Server Component — runs on server, no hooks
async function UserPage({ params }: { params: { id: string } }) {
const user = await db.users.findById(params.id); // direct DB call
return <UserProfile user={user} />;
}
// Client Component — needs interactivity
"use client";
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(l => !l)}>{liked ? "♥" : "♡"}</button>;
}
Phase 9 — Performance & accessibility (Weeks 27–29)
React performance
// React.memo — skip re-render when props unchanged
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
return <div>{/* expensive render */}</div>;
});
// Code splitting — load component only when needed
const HeavyModal = lazy(() => import("./HeavyModal"));
function App() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Open</button>
<Suspense fallback={<Spinner />}>
{open && <HeavyModal />}
</Suspense>
</>
);
}
// Virtual lists for long lists (react-window or TanStack Virtual)
import { FixedSizeList } from "react-window";
<FixedSizeList height={500} itemCount={10000} itemSize={40} width="100%">
{({ index, style }) => <div style={style}>Row {index}</div>}
</FixedSizeList>
Core Web Vitals targets
| Metric |
Target |
React impact |
| LCP (Largest Contentful Paint) |
< 2.5s |
Lazy load images, SSR |
| INP (Interaction to Next Paint) |
< 200ms |
Reduce re-renders, useTransition |
| CLS (Cumulative Layout Shift) |
< 0.1 |
Reserve image dimensions |
Accessibility in React
// Use semantic HTML and ARIA correctly
<button onClick={handleDelete} aria-label="Delete item">
<TrashIcon aria-hidden="true" />
</button>
// Associate form labels
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Manage focus after modal opens
useEffect(() => {
if (isOpen) closeButtonRef.current?.focus();
}, [isOpen]);
// Announce dynamic content
<div role="status" aria-live="polite">{statusMessage}</div>
Phase 10 — Portfolio & job prep (Weeks 30–36)
Portfolio projects to build
| Project |
Skills demonstrated |
| Kanban board |
Drag & drop, complex state, optimistic updates |
| GitHub Explorer |
API integration, search, pagination |
| Real-time chat |
WebSockets, Zustand, notifications |
| E-commerce product page |
Cart state, images, React Query |
| Dashboard with charts |
Recharts/Nivo, data fetching, filters |
| Full-stack app with Next.js |
Auth, database, Server Actions, deployment |
What React interviews test
| Topic |
What to know |
| Rendering |
When does React re-render? (state/props change) |
| Reconciliation |
Virtual DOM, key prop, diffing algorithm |
| Hooks rules |
Only in function components, not in conditions |
| Closure in hooks |
Stale closures in useEffect |
| State immutability |
Never mutate — always return new object/array |
| Lifting state |
When to move state up to common parent |
| Context API limits |
Avoid for high-frequency updates |
Common interview questions
- What is the difference between
useEffect and useLayoutEffect?
- Why must keys be stable and unique?
- How do you prevent unnecessary re-renders?
- What is the difference between controlled and uncontrolled components?
- How does React 18 Concurrent Mode work?
- When would you reach for Redux vs Zustand vs Context?
- How do Server Components differ from Client Components?
Full React technology map
Foundation
├── HTML, CSS, JavaScript (ES2022+)
├── TypeScript (strongly recommended)
└── Git
React Core
├── JSX & rendering
├── Components & props
├── Hooks (useState, useEffect, useRef, useMemo, useCallback)
└── Context API
Ecosystem
├── Routing — React Router v6 / Next.js App Router
├── State — Zustand, Jotai
├── Server state — TanStack Query
├── Forms — React Hook Form + Zod
├── Styling — Tailwind CSS + shadcn/ui
└── Animation — Framer Motion
Testing
├── Unit/Component — Vitest + React Testing Library
└── E2E — Playwright
Build & Deploy
├── Vite (SPA) / Next.js (SSR/SSG)
├── TypeScript + ESLint + Prettier
└── Vercel / Netlify / Railway
Realistic timeline
| Month |
Focus |
Output |
| 1 |
JavaScript prerequisites |
Solid JS fundamentals, 1 vanilla JS project |
| 2 |
React fundamentals + hooks |
Todo app, weather app in React |
| 3 |
State management + routing |
Multi-page app with Zustand + React Router |
| 4 |
Data fetching + testing |
App with TanStack Query + RTL tests |
| 5 |
Next.js + styling |
SSR project with Tailwind + shadcn |
| 6 |
Portfolio + job prep |
3 polished projects, 50+ leetcode mediums |
React developer roles & salary
| Role |
React usage |
Avg salary (US, 2025) |
| Junior Frontend Developer |
Building features |
$60k – $85k |
| Mid Frontend Developer |
Architecture decisions |
$90k – $120k |
| Senior Frontend Developer |
Tech lead, mentoring |
$130k – $160k |
| Full Stack Developer (React+Node) |
Frontend + backend |
$110k – $150k |
| React Native Developer |
Mobile with React |
$100k – $140k |
Common mistakes
| Mistake |
What goes wrong |
Fix |
| Mutating state directly |
React doesn't detect change, no re-render |
Always return new object: { ...prev, key: value } |
| Missing key in lists |
Incorrect reconciliation, performance bugs |
Stable unique key, never array index |
useEffect for derived state |
Over-complicated, stale data |
Compute during render |
| Everything in global state |
Hard to maintain, over-fetching |
Co-locate state; server state → TanStack Query |
| No error boundaries |
White screen on JS error |
Wrap with <ErrorBoundary> |
| Prop drilling 3+ levels |
Painful to refactor |
Context, Zustand, or component composition |
Premature useMemo/useCallback |
Complexity with no gain |
Profile first |
Ignoring useEffect deps warning |
Stale closures, subtle bugs |
Fix deps — don't silence with // eslint-disable |
FAQ
Q: Do I need to learn class components?
Not for new projects — function components and hooks are the standard since React 16.8 (2019). However, most large codebases have some class components, so understanding componentDidMount/componentDidUpdate/componentWillUnmount is useful for maintenance work.
Q: Should I learn Redux?
Redux is still used heavily in large enterprise apps. But for new projects, Zustand or TanStack Query handles 95 % of use cases with far less boilerplate. Learn Redux concepts (actions, reducers, selectors) but start with Zustand for practice.
Q: React or Next.js for my first project?
Start with Vite + React (no SSR, simpler mental model). Move to Next.js once you're comfortable with React itself. Next.js adds routing, SSR, and deployment conventions — all valuable, but confusing when you're still learning JSX.
Q: How important is TypeScript for React jobs?
Essentially mandatory for jobs in 2025. Nearly all React job listings mention TypeScript. Start typing your React projects as soon as you understand JavaScript basics — React.FC, useState<Type>, prop interfaces.
Q: Is React declining with the rise of Next.js and alternatives?
Next.js is React — it's a framework built on React. React remains the dominant UI library. Alternatives like Vue, Svelte, and Solid are gaining traction but React has the most jobs by a large margin.
Q: What is the fastest way to get hired as a React developer?
Three portfolio projects (deployed, with GitHub repos), TypeScript throughout, at least one Next.js project, React Testing Library tests, and practising 50–100 LeetCode problems (easy + medium). Open source contributions help significantly for standing out.