Przejdź do treści
Poradniki

React Hooks - Praktyczny przewodnik od podstaw do zaawansowanych

Opublikowano:
·3 min czytania·Autor: MDS Software Solutions Group

React Hooks Praktyczny

poradniki

React Hooks - Praktyczny Przewodnik od Podstaw

React Hooks zrewolucjonizowały sposób, w jaki budujemy komponenty w React. Od momentu wprowadzenia w wersji 16.8, hooki stały się preferowanym podejściem do zarządzania stanem i efektami ubocznymi, wypierając klasyczne komponenty klasowe. W tym przewodniku omówimy wszystkie kluczowe hooki, pokażemy praktyczne wzorce z TypeScript i przedstawimy nowości z React 19.

Czym są React Hooks?#

Hooki to funkcje, które pozwalają "zaczepiać się" o mechanizmy React z poziomu komponentów funkcyjnych. Dzięki nim możesz używać stanu, efektów ubocznych, kontekstu i wielu innych funkcji bez konieczności pisania komponentów klasowych.

// Przed hookami - komponent klasowy
class Counter extends React.Component<{}, { count: number }> {
  state = { count: 0 };

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Kliknięcia: {this.state.count}
      </button>
    );
  }
}

// Z hookami - komponent funkcyjny
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Kliknięcia: {count}
    </button>
  );
}

useState - Zarządzanie Stanem Lokalnym#

useState to najbardziej podstawowy hook. Pozwala dodać stan do komponentu funkcyjnego. Przyjmuje wartość początkową i zwraca parę: aktualną wartość stanu oraz funkcję do jej aktualizacji.

import { useState } from 'react';

interface FormData {
  name: string;
  email: string;
  age: number;
}

function RegistrationForm() {
  const [formData, setFormData] = useState<FormData>({
    name: '',
    email: '',
    age: 0,
  });
  const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (field: keyof FormData, value: string | number) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    // Wyczyść błąd po zmianie wartości
    setErrors(prev => ({ ...prev, [field]: undefined }));
  };

  const validate = (): boolean => {
    const newErrors: Partial<Record<keyof FormData, string>> = {};

    if (!formData.name.trim()) {
      newErrors.name = 'Imię jest wymagane';
    }
    if (!formData.email.includes('@')) {
      newErrors.email = 'Nieprawidłowy adres email';
    }
    if (formData.age < 18) {
      newErrors.age = 'Musisz mieć co najmniej 18 lat';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!validate()) return;

    setIsSubmitting(true);
    try {
      await submitForm(formData);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.name}
        onChange={e => handleChange('name', e.target.value)}
        placeholder="Imię"
      />
      {errors.name && <span className="error">{errors.name}</span>}

      <input
        value={formData.email}
        onChange={e => handleChange('email', e.target.value)}
        placeholder="Email"
      />
      {errors.email && <span className="error">{errors.email}</span>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Wysyłanie...' : 'Zarejestruj się'}
      </button>
    </form>
  );
}

Leniwa inicjalizacja stanu#

Gdy wartość początkowa wymaga kosztownych obliczeń, przekaż funkcję zamiast wartości:

// Kosztowna inicjalizacja - wykonuje się przy KAŻDYM renderze
const [items, setItems] = useState(parseExpensiveData(rawData));

// Leniwa inicjalizacja - wykonuje się TYLKO raz
const [items, setItems] = useState(() => parseExpensiveData(rawData));

Aktualizacja stanu na podstawie poprzedniej wartości#

Zawsze używaj formy funkcyjnej, gdy nowa wartość zależy od poprzedniej:

// Potencjalny problem z batched updates
const handleDoubleIncrement = () => {
  setCount(count + 1); // count = 0, ustawia 1
  setCount(count + 1); // count nadal = 0, ustawia 1
};

// Poprawne rozwiązanie
const handleDoubleIncrement = () => {
  setCount(prev => prev + 1); // 0 -> 1
  setCount(prev => prev + 1); // 1 -> 2
};

useEffect - Efekty Uboczne#

useEffect pozwala synchronizować komponent z zewnętrznymi systemami: API, subskrypcjami, timerem czy DOM.

import { useState, useEffect } from 'react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const controller = new AbortController();

    async function fetchUser() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }

        const data: User = await response.json();
        setUser(data);
      } catch (err) {
        if (err instanceof Error && err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    }

    fetchUser();

    // Cleanup - anuluj request przy unmount lub zmianie userId
    return () => controller.abort();
  }, [userId]);

  if (loading) return <div>Ładowanie...</div>;
  if (error) return <div>Błąd: {error}</div>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Typowe wzorce useEffect#

// Wykonaj raz po zamontowaniu
useEffect(() => {
  initializeAnalytics();
}, []);

// Reaguj na zmiany zależności
useEffect(() => {
  document.title = `Masz ${count} nowych wiadomości`;
}, [count]);

// Subskrypcja z cleanup
useEffect(() => {
  const ws = new WebSocket('wss://api.example.com/feed');

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    setMessages(prev => [...prev, data]);
  };

  return () => ws.close();
}, []);

// Event listener
useEffect(() => {
  const handleResize = () => {
    setWindowWidth(window.innerWidth);
  };

  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);

useContext - Globalny Stan bez Prop Drilling#

useContext eliminuje problem przekazywania propsów przez wiele poziomów komponentów.

import { createContext, useContext, useState, ReactNode } from 'react';

interface Theme {
  mode: 'light' | 'dark';
  primaryColor: string;
}

interface ThemeContextType {
  theme: Theme;
  toggleMode: () => void;
  setPrimaryColor: (color: string) => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

// Custom hook z walidacją
function useTheme(): ThemeContextType {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme musi być użyty wewnątrz ThemeProvider');
  }
  return context;
}

function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<Theme>({
    mode: 'light',
    primaryColor: '#3b82f6',
  });

  const toggleMode = () => {
    setTheme(prev => ({
      ...prev,
      mode: prev.mode === 'light' ? 'dark' : 'light',
    }));
  };

  const setPrimaryColor = (color: string) => {
    setTheme(prev => ({ ...prev, primaryColor: color }));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleMode, setPrimaryColor }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Komponent konsumujący kontekst
function ThemeToggle() {
  const { theme, toggleMode } = useTheme();

  return (
    <button onClick={toggleMode}>
      Aktualny motyw: {theme.mode === 'light' ? 'Jasny' : 'Ciemny'}
    </button>
  );
}

useReducer - Zaawansowane Zarządzanie Stanem#

useReducer jest lepszym wyborem niż useState, gdy logika stanu jest złożona lub obejmuje wiele powiązanych wartości.

import { useReducer } from 'react';

interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  discount: number;
  isCheckingOut: boolean;
}

type CartAction =
  | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
  | { type: 'REMOVE_ITEM'; payload: { id: string } }
  | { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
  | { type: 'APPLY_DISCOUNT'; payload: { discount: number } }
  | { type: 'CHECKOUT_START' }
  | { type: 'CHECKOUT_COMPLETE' }
  | { type: 'CLEAR_CART' };

function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existingItem = state.items.find(item => item.id === action.payload.id);

      if (existingItem) {
        return {
          ...state,
          items: state.items.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        };
      }

      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }],
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload.id),
      };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: Math.max(0, action.payload.quantity) }
            : item
        ).filter(item => item.quantity > 0),
      };

    case 'APPLY_DISCOUNT':
      return { ...state, discount: action.payload.discount };

    case 'CHECKOUT_START':
      return { ...state, isCheckingOut: true };

    case 'CHECKOUT_COMPLETE':
      return { items: [], discount: 0, isCheckingOut: false };

    case 'CLEAR_CART':
      return { items: [], discount: 0, isCheckingOut: false };

    default:
      return state;
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, {
    items: [],
    discount: 0,
    isCheckingOut: false,
  });

  const total = cart.items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
  const discountedTotal = total * (1 - cart.discount);

  return (
    <div>
      {cart.items.map(item => (
        <div key={item.id}>
          <span>{item.name} - {item.price} PLN x {item.quantity}</span>
          <button onClick={() => dispatch({
            type: 'UPDATE_QUANTITY',
            payload: { id: item.id, quantity: item.quantity + 1 }
          })}>+</button>
          <button onClick={() => dispatch({
            type: 'UPDATE_QUANTITY',
            payload: { id: item.id, quantity: item.quantity - 1 }
          })}>-</button>
          <button onClick={() => dispatch({
            type: 'REMOVE_ITEM',
            payload: { id: item.id }
          })}>Usuń</button>
        </div>
      ))}
      <p>Suma: {discountedTotal.toFixed(2)} PLN</p>
    </div>
  );
}

useMemo i useCallback - Optymalizacja Wydajności#

Te hooki pomagają uniknąć niepotrzebnych obliczeń i re-renderów.

useMemo - Memoizacja wartości#

import { useMemo, useState } from 'react';

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  rating: number;
}

function ProductList({ products }: { products: Product[] }) {
  const [sortBy, setSortBy] = useState<'price' | 'rating'>('price');
  const [filterCategory, setFilterCategory] = useState<string>('all');
  const [searchQuery, setSearchQuery] = useState('');

  // Memoizacja kosztownego filtrowania i sortowania
  const filteredAndSorted = useMemo(() => {
    let result = products;

    // Filtruj po kategorii
    if (filterCategory !== 'all') {
      result = result.filter(p => p.category === filterCategory);
    }

    // Filtruj po wyszukiwaniu
    if (searchQuery) {
      const query = searchQuery.toLowerCase();
      result = result.filter(p =>
        p.name.toLowerCase().includes(query)
      );
    }

    // Sortuj
    return [...result].sort((a, b) =>
      sortBy === 'price' ? a.price - b.price : b.rating - a.rating
    );
  }, [products, sortBy, filterCategory, searchQuery]);

  // Memoizacja statystyk
  const stats = useMemo(() => ({
    count: filteredAndSorted.length,
    avgPrice: filteredAndSorted.reduce((s, p) => s + p.price, 0) / filteredAndSorted.length || 0,
    avgRating: filteredAndSorted.reduce((s, p) => s + p.rating, 0) / filteredAndSorted.length || 0,
  }), [filteredAndSorted]);

  return (
    <div>
      <input
        value={searchQuery}
        onChange={e => setSearchQuery(e.target.value)}
        placeholder="Szukaj produktów..."
      />
      <p>Znaleziono: {stats.count} | Średnia cena: {stats.avgPrice.toFixed(2)} PLN</p>
      {filteredAndSorted.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

useCallback - Memoizacja funkcji#

import { useCallback, memo } from 'react';

// Komponent dziecka z React.memo
const TodoItem = memo(function TodoItem({
  todo,
  onToggle,
  onDelete,
}: {
  todo: { id: string; text: string; done: boolean };
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}) {
  console.log(`Render TodoItem: ${todo.text}`);

  return (
    <li>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Usuń</button>
    </li>
  );
});

function TodoList() {
  const [todos, setTodos] = useState<{ id: string; text: string; done: boolean }[]>([]);
  const [input, setInput] = useState('');

  // Bez useCallback - nowa referencja przy każdym renderze
  // = każdy TodoItem się re-renderuje
  const handleToggle = useCallback((id: string) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, done: !todo.done } : todo
      )
    );
  }, []);

  const handleDelete = useCallback((id: string) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <ul>
        {todos.map(todo => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={handleToggle}
            onDelete={handleDelete}
          />
        ))}
      </ul>
    </div>
  );
}

useRef - Referencje i Wartości Mutowalne#

useRef przechowuje mutowalną wartość, która nie powoduje re-renderu przy zmianie. Jest idealny do przechowywania referencji do elementów DOM i wartości persystentnych między renderami.

import { useRef, useEffect, useState } from 'react';

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  const renderCount = useRef(0);
  const previousValue = useRef<string>('');
  const [value, setValue] = useState('');

  // Licznik renderów (nie powoduje dodatkowych renderów)
  renderCount.current += 1;

  useEffect(() => {
    // Auto-focus po zamontowaniu
    inputRef.current?.focus();
  }, []);

  useEffect(() => {
    previousValue.current = value;
  }, [value]);

  return (
    <div>
      <input
        ref={inputRef}
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Auto-fokus na tym polu"
      />
      <p>Liczba renderów: {renderCount.current}</p>
      <p>Poprzednia wartość: {previousValue.current}</p>
    </div>
  );
}

useRef z timerem#

function Stopwatch() {
  const [time, setTime] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);

  const start = () => {
    if (isRunning) return;
    setIsRunning(true);
    intervalRef.current = setInterval(() => {
      setTime(prev => prev + 10);
    }, 10);
  };

  const stop = () => {
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
      intervalRef.current = null;
    }
    setIsRunning(false);
  };

  const reset = () => {
    stop();
    setTime(0);
  };

  // Cleanup przy odmontowaniu
  useEffect(() => {
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
  }, []);

  const formatTime = (ms: number) => {
    const minutes = Math.floor(ms / 60000);
    const seconds = Math.floor((ms % 60000) / 1000);
    const centiseconds = Math.floor((ms % 1000) / 10);
    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${centiseconds.toString().padStart(2, '0')}`;
  };

  return (
    <div>
      <span>{formatTime(time)}</span>
      <button onClick={start} disabled={isRunning}>Start</button>
      <button onClick={stop} disabled={!isRunning}>Stop</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
}

Custom Hooks - Tworzenie Własnych Hooków#

Custom hooks to potężny mechanizm wydzielania i ponownego wykorzystywania logiki stanowej.

// useLocalStorage - persystentny stan w localStorage
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value: T | ((prev: T) => T)) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [storedValue, setValue];
}

// useDebounce - opóźniona wartość
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;
}

// useFetch - generyczny 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();

    async function fetchData() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const json: T = 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 SearchPage() {
  const [query, setQuery] = useLocalStorage('searchQuery', '');
  const debouncedQuery = useDebounce(query, 300);
  const { data, loading, error } = useFetch<Product[]>(
    `/api/search?q=${encodeURIComponent(debouncedQuery)}`
  );

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading && <p>Ładowanie...</p>}
      {error && <p>Błąd: {error}</p>}
      {data?.map(product => <ProductCard key={product.id} product={product} />)}
    </div>
  );
}

Zasady Hooków (Rules of Hooks)#

React wymaga przestrzegania dwóch fundamentalnych zasad:

1. Wywołuj hooki tylko na najwyższym poziomie#

// Nigdy nie używaj hooków w warunkach, pętlach ani zagnieżdżonych funkcjach

function BadComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
  // NIGDY tak nie rób
  if (isLoggedIn) {
    const [user, setUser] = useState(null); // Zła praktyka
  }

  // Poprawnie - hook zawsze na najwyższym poziomie
  const [user, setUser] = useState(null);

  // Warunek wewnątrz hooka lub po nim
  useEffect(() => {
    if (isLoggedIn) {
      fetchUser().then(setUser);
    }
  }, [isLoggedIn]);
}

2. Wywołuj hooki tylko z komponentów React lub custom hooków#

// Nie wywołuj hooków w zwykłych funkcjach JavaScript
function regularFunction() {
  const [state, setState] = useState(0); // Błąd
}

// Poprawnie - custom hook (nazwa zaczyna się od "use")
function useCounter(initial: number = 0) {
  const [count, setCount] = useState(initial);
  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  return { count, increment, decrement };
}

Nowości w React 19#

React 19 wprowadza nowe hooki, które znacząco upraszczają typowe wzorce.

useFormStatus#

Pozwala śledzić stan formularza bez przekazywania propsów:

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending, data, method } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Wysyłanie...' : 'Wyślij formularz'}
    </button>
  );
}

function ContactForm() {
  async function handleSubmit(formData: FormData) {
    'use server';
    const name = formData.get('name');
    const email = formData.get('email');
    await saveContact({ name, email });
  }

  return (
    <form action={handleSubmit}>
      <input name="name" placeholder="Imię" required />
      <input name="email" type="email" placeholder="Email" required />
      <SubmitButton />
    </form>
  );
}

useOptimistic#

Pozwala na optymistyczne aktualizacje UI - pokazanie oczekiwanego rezultatu zanim serwer potwierdzi operację:

import { useOptimistic, useState } from 'react';

interface Message {
  id: string;
  text: string;
  sending?: boolean;
}

function Chat() {
  const [messages, setMessages] = useState<Message[]>([]);

  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    messages,
    (state: Message[], newMessage: Message) => [
      ...state,
      { ...newMessage, sending: true },
    ]
  );

  async function sendMessage(formData: FormData) {
    const text = formData.get('message') as string;
    const optimisticMsg: Message = {
      id: crypto.randomUUID(),
      text,
      sending: true,
    };

    addOptimisticMessage(optimisticMsg);

    const savedMessage = await saveMessageToServer(text);
    setMessages(prev => [...prev, savedMessage]);
  }

  return (
    <div>
      {optimisticMessages.map(msg => (
        <div key={msg.id} style={{ opacity: msg.sending ? 0.6 : 1 }}>
          {msg.text}
          {msg.sending && <span> (wysyłanie...)</span>}
        </div>
      ))}
      <form action={sendMessage}>
        <input name="message" placeholder="Wpisz wiadomość..." />
        <button type="submit">Wyślij</button>
      </form>
    </div>
  );
}

use - Nowy sposób odczytu zasobów#

Hook use pozwala odczytywać wartości z Promise i Context w sposób, w jaki żaden inny hook nie może - nawet warunkowo:

import { use, Suspense } from 'react';

// Odczyt Promise
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise);

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

// Warunkowy odczyt kontekstu
function StatusMessage({ isAdmin }: { isAdmin: boolean }) {
  if (isAdmin) {
    const adminContext = use(AdminContext);
    return <p>Panel admina: {adminContext.dashboardUrl}</p>;
  }
  return <p>Widok użytkownika</p>;
}

// Użycie z Suspense
function App() {
  const userPromise = fetchUser(1);

  return (
    <Suspense fallback={<div>Ładowanie profilu...</div>}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  );
}

Wzorce Wydajności#

Unikanie niepotrzebnych re-renderów#

import { memo, useMemo, useCallback } from 'react';

// 1. Podział komponentów - izolacja stanu
function SearchPage() {
  return (
    <div>
      <SearchInput /> {/* Stan wyszukiwania tutaj */}
      <ExpensiveStaticContent /> {/* Nie re-renderuje się */}
    </div>
  );
}

// 2. Composition pattern - children nie re-renderują się
function Layout({ children }: { children: ReactNode }) {
  const [sidebarOpen, setSidebarOpen] = useState(false);

  return (
    <div>
      <Sidebar open={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
      <main>{children}</main> {/* children nie re-renderują się */}
    </div>
  );
}

// 3. Memoizacja kontekstu
function OptimizedProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const value = useMemo(
    () => ({ user, setUser }),
    [user]
  );

  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

Najczęstsze Błędy#

1. Brakujące zależności w useEffect#

// Problem - stale closure
function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1); // Zawsze odczytuje count = 0
    }, 1000);
    return () => clearInterval(id);
  }, []); // Brak count w zależnościach

  // Rozwiązanie - użyj formy funkcyjnej
  useEffect(() => {
    const id = setInterval(() => {
      setCount(prev => prev + 1); // Zawsze aktualna wartość
    }, 1000);
    return () => clearInterval(id);
  }, []);
}

2. Nieskończona pętla w useEffect#

// Problem - obiekt jako zależność
function UserList() {
  const [users, setUsers] = useState<User[]>([]);

  const filters = { role: 'admin', active: true }; // Nowy obiekt przy każdym renderze

  useEffect(() => {
    fetchUsers(filters).then(setUsers); // Nieskończona pętla
  }, [filters]); // filters to zawsze nowa referencja

  // Rozwiązanie - useMemo
  const filters = useMemo(() => ({ role: 'admin', active: true }), []);

  useEffect(() => {
    fetchUsers(filters).then(setUsers);
  }, [filters]);
}

3. Nadmierne użycie useEffect#

// Antypattern - useEffect do transformacji danych
function FilteredList({ items, filter }: Props) {
  const [filtered, setFiltered] = useState(items);

  useEffect(() => {
    setFiltered(items.filter(item => item.category === filter));
  }, [items, filter]);

  // Poprawnie - oblicz podczas renderowania
  const filtered = useMemo(
    () => items.filter(item => item.category === filter),
    [items, filter]
  );
}

Podsumowanie#

React Hooks to fundament nowoczesnego React:

  • useState - stan lokalny komponentu
  • useEffect - synchronizacja z zewnętrznymi systemami
  • useContext - globalny stan bez prop drilling
  • useReducer - złożona logika stanu
  • useMemo / useCallback - optymalizacja wydajności
  • useRef - referencje DOM i wartości mutowalne
  • Custom hooks - reużywalna logika stanowa
  • React 19 - useFormStatus, useOptimistic, use

Kluczem jest zrozumienie, kiedy i dlaczego używać każdego hooka, a nie stosowanie ich na ślepo. Pamiętaj o zasadach hooków i mierz wydajność przed optymalizacją.

Potrzebujesz wsparcia?#

W MDS Software Solutions Group pomagamy w:

  • Budowie nowoczesnych aplikacji React z TypeScript
  • Migracji z komponentów klasowych do hooków
  • Optymalizacji wydajności aplikacji frontendowych
  • Wdrażaniu najlepszych praktyk i wzorców architektonicznych
  • Code review i audytach jakości kodu

Skontaktuj się z nami, aby omówić Twój projekt!

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

React Hooks - Praktyczny przewodnik od podstaw do zaawansowanych | MDS Software Solutions Group | MDS Software Solutions Group