React - Complete guide to the library that changed frontend development
What is React and why did it dominate the market?
React is a JavaScript library for building user interfaces, created by Facebook (Meta) in 2013. Over the course of a decade, it has become the de facto standard in the world of frontend development, used by Netflix, Airbnb, Instagram, WhatsApp, and millions of other applications.
React introduced revolutionary concepts:
- Component-based thinking - UI as a tree of reusable components
- Virtual DOM - Efficient interface updates
- Unidirectional data flow - Predictable state management
- JSX - HTML in JavaScript
Why React in 2025?
Massive ecosystem
React has the largest ecosystem in the frontend world:
- Next.js, Remix - Full-stack frameworks
- React Native - Mobile applications
- Thousands of libraries - A solution for every problem
Stability and backward compatibility
Meta invests enormous resources in maintaining React. Breaking changes are rare and well communicated.
Demand on the job market
React is the most frequently required skill for frontend developers. Knowledge of React opens the door to thousands of job offers.
React fundamentals
First component
// Functional component - the standard in 2025
function Welcome({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>
}
// Usage
function App() {
return (
<div>
<Welcome name="Anna" />
<Welcome name="Bartek" />
</div>
)
}JSX - Syntax
function ProductCard({ product }: { product: Product }) {
return (
// JSX lets you write HTML-like syntax in JavaScript
<div className="card">
{/* JavaScript expressions in curly braces */}
<h2>{product.name}</h2>
<p>{product.description}</p>
<span>${product.price.toFixed(2)}</span>
{/* Conditional rendering */}
{product.inStock ? (
<button>Buy now</button>
) : (
<span>Unavailable</span>
)}
{/* Mapping lists */}
<ul>
{product.features.map((feature, index) => (
<li key={index}>{feature}</li>
))}
</ul>
</div>
)
}Props - Passing data
// Defining prop types
interface ButtonProps {
children: React.ReactNode
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
onClick?: () => void
}
// Component with props
function Button({
children,
variant = 'primary',
size = 'md',
disabled = false,
onClick
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
)
}
// Usage
<Button variant="primary" size="lg" onClick={() => alert('Clicked!')}>
Click me
</Button>React Hooks - The heart of modern React
useState - Local state
import { useState } from 'react'
function Counter() {
// [value, setter function] = useState(initial value)
const [count, setCount] = useState(0)
return (
<div>
<p>Counter: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
// State with an object
function UserForm() {
const [user, setUser] = useState({
name: '',
email: '',
age: 0
})
const updateField = (field: string, value: string | number) => {
setUser(prev => ({
...prev,
[field]: value
}))
}
return (
<form>
<input
value={user.name}
onChange={(e) => updateField('name', e.target.value)}
/>
<input
value={user.email}
onChange={(e) => updateField('email', e.target.value)}
/>
</form>
)
}useEffect - Side effects
import { useState, useEffect } from 'react'
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Run the effect when userId changes
useEffect(() => {
const fetchUser = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error('User not found')
const data = await response.json()
setUser(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Error')
} finally {
setLoading(false)
}
}
fetchUser()
}, [userId]) // Dependency array
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error}</p>
if (!user) return <p>No user found</p>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
// useEffect with cleanup
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
// Set initial size
handleResize()
// Add listener
window.addEventListener('resize', handleResize)
// Cleanup - remove listener on unmount
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // Empty array = run once on mount
return <p>Window size: {size.width} x {size.height}</p>
}useContext - Global state
import { createContext, useContext, useState, ReactNode } from 'react'
// 1. Create context
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType | null>(null)
// 2. Provider component
function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. Custom hook for easier usage
function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
// 4. Usage in components
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
)
}
function App() {
return (
<ThemeProvider>
<ThemeToggle />
<MainContent />
</ThemeProvider>
)
}useRef - References and non-mutating values
import { useRef, useEffect } from 'react'
// Reference to a DOM element
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} placeholder="Auto-focus" />
}
// Storing values without re-rendering
function Timer() {
const [count, setCount] = useState(0)
const intervalRef = useRef<number | null>(null)
const start = () => {
if (intervalRef.current) return
intervalRef.current = window.setInterval(() => {
setCount(c => c + 1)
}, 1000)
}
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
return (
<div>
<p>Time: {count}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}useMemo and useCallback - Optimization
import { useMemo, useCallback, useState } from 'react'
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
// useMemo - caches the result of calculations
const filteredItems = useMemo(() => {
console.log('Filtering...')
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
)
}, [items, filter]) // Recalculate only when items or filter changes
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
function Parent() {
const [count, setCount] = useState(0)
// useCallback - caches the function
const handleClick = useCallback(() => {
console.log('Clicked!')
}, []) // Function does not change
// Without useCallback - Child would re-render on every count change
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
)
}useReducer - Complex state
import { useReducer } from 'react'
// Types
interface State {
items: Item[]
loading: boolean
error: string | null
}
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: Item[] }
| { type: 'FETCH_ERROR'; payload: string }
| { type: 'ADD_ITEM'; payload: Item }
| { type: 'REMOVE_ITEM'; payload: string }
// Reducer
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null }
case 'FETCH_SUCCESS':
return { ...state, loading: false, items: action.payload }
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload }
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] }
case 'REMOVE_ITEM':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
}
default:
return state
}
}
// Component
function ItemList() {
const [state, dispatch] = useReducer(reducer, {
items: [],
loading: false,
error: null
})
const fetchItems = async () => {
dispatch({ type: 'FETCH_START' })
try {
const response = await fetch('/api/items')
const data = await response.json()
dispatch({ type: 'FETCH_SUCCESS', payload: data })
} catch (err) {
dispatch({ type: 'FETCH_ERROR', payload: 'Fetch error' })
}
}
return (
<div>
{state.loading && <p>Loading...</p>}
{state.error && <p>Error: {state.error}</p>}
<button onClick={fetchItems}>Fetch</button>
<ul>
{state.items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}>
Remove
</button>
</li>
))}
</ul>
</div>
)
}Custom Hooks - Building your own hooks
// useFetch - universal hook for data fetching
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const controller = new AbortController()
const fetchData = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) throw new Error('Network error')
const json = await response.json()
setData(json)
} catch (err) {
if (err instanceof Error && err.name !== 'AbortError') {
setError(err.message)
}
} finally {
setLoading(false)
}
}
fetchData()
return () => controller.abort()
}, [url])
return { data, loading, error }
}
// Usage
function UserList() {
const { data: users, loading, error } = useFetch<User[]>('/api/users')
if (loading) return <p>Loading...</p>
if (error) return <p>Error: {error}</p>
return (
<ul>
{users?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
// useLocalStorage - synchronization with localStorage
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error('Error saving to localStorage', error)
}
}
return [storedValue, setValue] as const
}
// useDebounce - delayed values
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// useDebounce usage
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) {
// Search only after 300ms since the last change
searchAPI(debouncedQuery)
}
}, [debouncedQuery])
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
)
}Design patterns
Compound Components
// Menu as a compound component
interface MenuProps {
children: React.ReactNode
}
interface MenuItemProps {
children: React.ReactNode
onClick?: () => void
}
function Menu({ children }: MenuProps) {
return <ul className="menu">{children}</ul>
}
function MenuItem({ children, onClick }: MenuItemProps) {
return (
<li className="menu-item" onClick={onClick}>
{children}
</li>
)
}
function MenuDivider() {
return <li className="menu-divider" />
}
// Assign subcomponents
Menu.Item = MenuItem
Menu.Divider = MenuDivider
// Usage
function App() {
return (
<Menu>
<Menu.Item onClick={() => console.log('Home')}>Home</Menu.Item>
<Menu.Item onClick={() => console.log('About')}>About</Menu.Item>
<Menu.Divider />
<Menu.Item onClick={() => console.log('Logout')}>Logout</Menu.Item>
</Menu>
)
}Render Props
interface MousePosition {
x: number
y: number
}
interface MouseTrackerProps {
render: (position: MousePosition) => React.ReactNode
}
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 })
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY })
}
window.addEventListener('mousemove', handleMouseMove)
return () => window.removeEventListener('mousemove', handleMouseMove)
}, [])
return <>{render(position)}</>
}
// Usage
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<p>Mouse position: {x}, {y}</p>
)}
/>
)
}Higher-Order Components (HOC)
// HOC for adding loading state
function withLoading<P extends object>(
Component: React.ComponentType<P>
) {
return function WithLoading({ isLoading, ...props }: P & { isLoading: boolean }) {
if (isLoading) {
return <div className="spinner">Loading...</div>
}
return <Component {...(props as P)} />
}
}
// Usage
const UserListWithLoading = withLoading(UserList)
function App() {
const [loading, setLoading] = useState(true)
return <UserListWithLoading isLoading={loading} users={users} />
}Forms
Controlled components
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
})
const [errors, setErrors] = useState<Record<string, string>>({})
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
// Clear error on change
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }))
}
}
const validate = () => {
const newErrors: Record<string, string> = {}
if (!formData.name) newErrors.name = 'Name is required'
if (!formData.email) newErrors.email = 'Email is required'
else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Invalid email'
}
if (!formData.message) newErrors.message = 'Message is required'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validate()) {
console.log('Sending:', formData)
// Send data
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
name="name"
value={formData.name}
onChange={handleChange}
/>
{errors.name && <span className="error">{errors.name}</span>}
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
/>
{errors.message && <span className="error">{errors.message}</span>}
</div>
<button type="submit">Send</button>
</form>
)
}React 19+ Features
use() hook
import { use, Suspense } from 'react'
// use() lets you read a Promise directly
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // Suspends the component
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
function App() {
const userPromise = fetchUser(1)
return (
<Suspense fallback={<p>Loading...</p>}>
<UserProfile userPromise={userPromise} />
</Suspense>
)
}Actions (form actions)
function SignupForm() {
const [error, setError] = useState<string | null>(null)
async function signup(formData: FormData) {
const email = formData.get('email') as string
const password = formData.get('password') as string
try {
await createAccount(email, password)
} catch (err) {
setError('Failed to create account')
}
}
return (
<form action={signup}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Sign up</button>
{error && <p className="error">{error}</p>}
</form>
)
}Testing React
React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
// Simple test
test('renders greeting', () => {
render(<Welcome name="Anna" />)
expect(screen.getByText('Hello, Anna!')).toBeInTheDocument()
})
// Interaction test
test('increments counter', async () => {
render(<Counter />)
const button = screen.getByRole('button', { name: /\+1/i })
await userEvent.click(button)
expect(screen.getByText(/Counter: 1/)).toBeInTheDocument()
})
// Async test
test('loads user data', async () => {
render(<UserProfile userId="1" />)
// Wait for loading to complete
await waitFor(() => {
expect(screen.getByText('Jan Kowalski')).toBeInTheDocument()
})
})
// Mock API
import { rest } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
rest.get('/api/users/1', (req, res, ctx) => {
return res(ctx.json({ id: '1', name: 'Jan Kowalski' }))
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())Best practices
Do's
✅ Use functional components and hooks
✅ Keep components small and focused
✅ Extract logic into custom hooks
✅ Use TypeScript for type safety
✅ Memoize expensive calculations (useMemo)
✅ Use keys when mapping lists
✅ Handle loading and error statesDon'ts
❌ Don't mutate state directly
❌ Don't use index as key (unless the list is static)
❌ Don't overuse useEffect
❌ Don't keep all state in a single useState
❌ Don't ignore the dependency array in useEffect
❌ Don't create functions inside JSX without useCallbackReact vs Alternatives
| Aspect | React | Vue | Svelte |
|---|---|---|---|
| Approach | Library | Framework | Compiler |
| Syntax | JSX | Templates | Templates |
| State | useState, Redux | Composition API | Stores |
| Learning curve | Medium | Low | Low |
| Ecosystem | Massive | Large | Growing |
| Performance | Good | Good | Excellent |
Summary
React is the foundation of modern frontend development:
- Components - Reusable, modular code
- Hooks - Elegant state and effect management
- Ecosystem - A solution for every problem
- Stability - Long-term support from Meta