React - Kompletny przewodnik po bibliotece, która zmieniła frontend development
Czym jest React i dlaczego zdominował rynek?
React to biblioteka JavaScript do budowania interfejsów użytkownika, stworzona przez Facebook (Meta) w 2013 roku. W ciągu dekady stała się de facto standardem w świecie frontend development, używanym przez Netflix, Airbnb, Instagram, WhatsApp i miliony innych aplikacji.
React wprowadził rewolucyjne koncepcje:
- Komponentowe myślenie - UI jako drzewo reużywalnych komponentów
- Virtual DOM - Efektywne aktualizacje interfejsu
- Jednokierunkowy przepływ danych - Przewidywalny state management
- JSX - HTML w JavaScript
Dlaczego React w 2025?
Gigantyczny ekosystem
React ma największy ekosystem w świecie frontend:
- Next.js, Remix - Full-stack frameworks
- React Native - Aplikacje mobilne
- Thousands of libraries - Rozwiązanie na każdy problem
Stabilność i backward compatibility
Meta inwestuje ogromne zasoby w utrzymanie React. Breaking changes są rzadkie i dobrze komunikowane.
Demand na rynku pracy
React to najczęściej wymagana umiejętność dla frontend developerów. Znajomość React otwiera drzwi do tysięcy ofert pracy.
Podstawy React
Pierwszy komponent
// Funkcyjny komponent - standard w 2025
function Welcome({ name }: { name: string }) {
return <h1>Cześć, {name}!</h1>
}
// Użycie
function App() {
return (
<div>
<Welcome name="Anna" />
<Welcome name="Bartek" />
</div>
)
}JSX - Składnia
function ProductCard({ product }: { product: Product }) {
return (
// JSX pozwala pisać HTML-like syntax w JavaScript
<div className="card">
{/* Wyrażenia JavaScript w klamrach */}
<h2>{product.name}</h2>
<p>{product.description}</p>
<span>{product.price.toFixed(2)} zł</span>
{/* Warunkowe renderowanie */}
{product.inStock ? (
<button>Kup teraz</button>
) : (
<span>Niedostępny</span>
)}
{/* Mapowanie list */}
<ul>
{product.features.map((feature, index) => (
<li key={index}>{feature}</li>
))}
</ul>
</div>
)
}Props - Przekazywanie danych
// Definiowanie typów props
interface ButtonProps {
children: React.ReactNode
variant?: 'primary' | 'secondary' | 'danger'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
onClick?: () => void
}
// Komponent z 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>
)
}
// Użycie
<Button variant="primary" size="lg" onClick={() => alert('Kliknięto!')}>
Kliknij mnie
</Button>React Hooks - Serce nowoczesnego React
useState - Lokalny stan
import { useState } from 'react'
function Counter() {
// [wartość, funkcja do zmiany] = useState(wartość początkowa)
const [count, setCount] = useState(0)
return (
<div>
<p>Licznik: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
)
}
// Stan z obiektem
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)
// Uruchom efekt gdy userId się zmieni
useEffect(() => {
const fetchUser = async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) throw new Error('Nie znaleziono użytkownika')
const data = await response.json()
setUser(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Błąd')
} finally {
setLoading(false)
}
}
fetchUser()
}, [userId]) // Dependency array
if (loading) return <p>Ładowanie...</p>
if (error) return <p>Błąd: {error}</p>
if (!user) return <p>Brak użytkownika</p>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
// useEffect z cleanup
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
})
}
// Ustaw początkowy rozmiar
handleResize()
// Dodaj listener
window.addEventListener('resize', handleResize)
// Cleanup - usuń listener przy unmount
return () => {
window.removeEventListener('resize', handleResize)
}
}, []) // Pusta tablica = uruchom raz przy mount
return <p>Rozmiar okna: {size.width} x {size.height}</p>
}useContext - Globalny stan
import { createContext, useContext, useState, ReactNode } from 'react'
// 1. Stwórz 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 dla łatwiejszego użycia
function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
// 4. Użycie w komponentach
function ThemeToggle() {
const { theme, toggleTheme } = useTheme()
return (
<button onClick={toggleTheme}>
Aktualny motyw: {theme}
</button>
)
}
function App() {
return (
<ThemeProvider>
<ThemeToggle />
<MainContent />
</ThemeProvider>
)
}useRef - Referencje i wartości niemutujące
import { useRef, useEffect } from 'react'
// Referencja do elementu DOM
function AutoFocusInput() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <input ref={inputRef} placeholder="Auto-fokus" />
}
// Przechowywanie wartości bez re-renderowania
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>Czas: {count}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
)
}useMemo i useCallback - Optymalizacja
import { useMemo, useCallback, useState } from 'react'
function ExpensiveList({ items, filter }: { items: Item[]; filter: string }) {
// useMemo - cachuje wynik obliczeń
const filteredItems = useMemo(() => {
console.log('Filtrowanie...')
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
)
}, [items, filter]) // Przelicz tylko gdy items lub filter się zmieni
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
function Parent() {
const [count, setCount] = useState(0)
// useCallback - cachuje funkcję
const handleClick = useCallback(() => {
console.log('Kliknięto!')
}, []) // Funkcja się nie zmienia
// Bez useCallback - Child by się re-renderował przy każdej zmianie count
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
<Child onClick={handleClick} />
</div>
)
}useReducer - Złożony stan
import { useReducer } from 'react'
// Typy
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
}
}
// Komponent
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: 'Błąd pobierania' })
}
}
return (
<div>
{state.loading && <p>Ładowanie...</p>}
{state.error && <p>Błąd: {state.error}</p>}
<button onClick={fetchItems}>Pobierz</button>
<ul>
{state.items.map(item => (
<li key={item.id}>
{item.name}
<button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}>
Usuń
</button>
</li>
))}
</ul>
</div>
)
}Custom Hooks - Własne hooki
// useFetch - uniwersalny hook do pobierania danych
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('Błąd sieci')
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 }
}
// Użycie
function UserList() {
const { data: users, loading, error } = useFetch<User[]>('/api/users')
if (loading) return <p>Ładowanie...</p>
if (error) return <p>Błąd: {error}</p>
return (
<ul>
{users?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
)
}
// useLocalStorage - synchronizacja z 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 - opóźnione wartości
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
}
// Użycie useDebounce
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) {
// Szukaj dopiero po 300ms od ostatniej zmiany
searchAPI(debouncedQuery)
}
}, [debouncedQuery])
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Szukaj..."
/>
)
}Wzorce projektowe
Compound Components
// Menu jako 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" />
}
// Przypisz subkomponenty
Menu.Item = MenuItem
Menu.Divider = MenuDivider
// Użycie
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)}</>
}
// Użycie
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<p>Pozycja myszy: {x}, {y}</p>
)}
/>
)
}Higher-Order Components (HOC)
// HOC do dodawania 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">Ładowanie...</div>
}
return <Component {...(props as P)} />
}
}
// Użycie
const UserListWithLoading = withLoading(UserList)
function App() {
const [loading, setLoading] = useState(true)
return <UserListWithLoading isLoading={loading} users={users} />
}Formularze
Kontrolowane komponenty
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 }))
// Wyczyść błąd po zmianie
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }))
}
}
const validate = () => {
const newErrors: Record<string, string> = {}
if (!formData.name) newErrors.name = 'Imię jest wymagane'
if (!formData.email) newErrors.email = 'Email jest wymagany'
else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Niepoprawny email'
}
if (!formData.message) newErrors.message = 'Wiadomość jest wymagana'
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (validate()) {
console.log('Wysyłanie:', formData)
// Wyślij dane
}
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Imię</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">Wiadomość</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
/>
{errors.message && <span className="error">{errors.message}</span>}
</div>
<button type="submit">Wyślij</button>
</form>
)
}React 19+ Features
use() hook
import { use, Suspense } from 'react'
// use() pozwala czytać Promise bezpośrednio
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise) // Suspenduje komponent
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
function App() {
const userPromise = fetchUser(1)
return (
<Suspense fallback={<p>Ładowanie...</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('Nie udało się utworzyć konta')
}
}
return (
<form action={signup}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Zarejestruj</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'
// Prosty test
test('renders greeting', () => {
render(<Welcome name="Anna" />)
expect(screen.getByText('Cześć, Anna!')).toBeInTheDocument()
})
// Test interakcji
test('increments counter', async () => {
render(<Counter />)
const button = screen.getByRole('button', { name: /\+1/i })
await userEvent.click(button)
expect(screen.getByText(/Licznik: 1/)).toBeInTheDocument()
})
// Test async
test('loads user data', async () => {
render(<UserProfile userId="1" />)
// Czekaj na załadowanie
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
✅ Używaj funkcyjnych komponentów i hooks
✅ Trzymaj komponenty małe i skupione
✅ Wyciągaj logikę do custom hooks
✅ Używaj TypeScript dla type safety
✅ Memoizuj drogie obliczenia (useMemo)
✅ Używaj keys przy mapowaniu list
✅ Obsługuj loading i error statesDon'ts
❌ Nie mutuj state bezpośrednio
❌ Nie używaj index jako key (chyba że lista jest statyczna)
❌ Nie nadużywaj useEffect
❌ Nie trzymaj całego stanu w jednym useState
❌ Nie ignoruj dependency array w useEffect
❌ Nie twórz funkcji wewnątrz JSX bez useCallbackReact vs Alternatywy
| Aspekt | React | Vue | Svelte |
|---|---|---|---|
| Podejście | Library | Framework | Compiler |
| Składnia | JSX | Templates | Templates |
| State | useState, Redux | Composition API | Stores |
| Krzywa uczenia | Średnia | Niska | Niska |
| Ekosystem | Ogromny | Duży | Rosnący |
| Performance | Dobra | Dobra | Świetna |
Podsumowanie
React to fundament nowoczesnego frontend development:
- Komponenty - Reużywalny, modularny kod
- Hooks - Eleganckie zarządzanie stanem i efektami
- Ekosystem - Rozwiązanie na każdy problem
- Stabilność - Długoterminowe wsparcie od Meta