Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik13 min czytania

React

React to najpopularniejsza biblioteka JavaScript do budowania interfejsów użytkownika. Poznaj komponenty, hooks, state management i najlepsze praktyki w 2025 roku.

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

Code
TypeScript
// 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

Code
TypeScript
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)}</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

Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
// 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

Code
TypeScript
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)

Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
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)

Code
TypeScript
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

Code
TypeScript
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

Code
TEXT
✅ 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 states

Don'ts

Code
TEXT
❌ 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 useCallback

React vs Alternatywy

AspektReactVueSvelte
PodejścieLibraryFrameworkCompiler
SkładniaJSXTemplatesTemplates
StateuseState, ReduxComposition APIStores
Krzywa uczeniaŚredniaNiskaNiska
EkosystemOgromnyDużyRosnący
PerformanceDobraDobraŚ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