React Hooks zrewolucjonizowały sposób pisania komponentów w React. W tym artykule poznasz zaawansowane techniki używania hooków, niestandardowe hooki i najlepsze praktyki, które pomogą Ci tworzyć wydajne i czyste aplikacje React.

useState - zarządzanie stanem lokalnym

Hook useState to podstawa zarządzania stanem w funkcyjnych komponentach React:

import React, { useState } from 'react';

// Podstawowe użycie
function Counter() {
    const [count, setCount] = useState(0);

    return (
        

Licznik: {count}

); } // Stan obiektowy function UserForm() { const [user, setUser] = useState({ name: '', email: '', age: 0 }); const updateUser = (field, value) => { setUser(prevUser => ({ ...prevUser, [field]: value })); }; return (
updateUser('name', e.target.value)} placeholder="Imię" /> updateUser('email', e.target.value)} placeholder="Email" />
); }

useEffect - efekty uboczne i cykl życia

useEffect zastępuje metody cyklu życia komponentów klasowych:

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

function DataFetcher({ userId }) {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    // Effect z dependency array
    useEffect(() => {
        const fetchUser = async () => {
            try {
                setLoading(true);
                setError(null);
                
                const response = await fetch(`/api/users/${userId}`);
                if (!response.ok) {
                    throw new Error('Nie można pobrać danych użytkownika');
                }
                
                const userData = await response.json();
                setUser(userData);
            } catch (err) {
                setError(err.message);
            } finally {
                setLoading(false);
            }
        };

        if (userId) {
            fetchUser();
        }
    }, [userId]); // Effect uruchomi się po zmianie userId

    // Cleanup effect
    useEffect(() => {
        const timer = setInterval(() => {
            console.log('Timer tick');
        }, 1000);

        // Cleanup function
        return () => {
            clearInterval(timer);
        };
    }, []);

    if (loading) return 
Ładowanie...
; if (error) return
Błąd: {error}
; if (!user) return
Nie znaleziono użytkownika
; return (

{user.name}

{user.email}

); }

useReducer - złożone zarządzanie stanem

useReducer jest idealny dla złożonej logiki stanu:

import React, { useReducer } from 'react';

// Definicja akcji
const ACTIONS = {
    ADD_TODO: 'add_todo',
    TOGGLE_TODO: 'toggle_todo',
    DELETE_TODO: 'delete_todo',
    SET_FILTER: 'set_filter'
};

// Reducer function
function todoReducer(state, action) {
    switch (action.type) {
        case ACTIONS.ADD_TODO:
            return {
                ...state,
                todos: [...state.todos, {
                    id: Date.now(),
                    text: action.payload.text,
                    completed: false
                }]
            };
        
        case ACTIONS.TOGGLE_TODO:
            return {
                ...state,
                todos: state.todos.map(todo =>
                    todo.id === action.payload.id
                        ? { ...todo, completed: !todo.completed }
                        : todo
                )
            };
        
        case ACTIONS.DELETE_TODO:
            return {
                ...state,
                todos: state.todos.filter(todo => todo.id !== action.payload.id)
            };
        
        case ACTIONS.SET_FILTER:
            return {
                ...state,
                filter: action.payload.filter
            };
        
        default:
            return state;
    }
}

// Komponent używający useReducer
function TodoApp() {
    const initialState = {
        todos: [],
        filter: 'all' // all, active, completed
    };

    const [state, dispatch] = useReducer(todoReducer, initialState);

    const addTodo = (text) => {
        dispatch({
            type: ACTIONS.ADD_TODO,
            payload: { text }
        });
    };

    const toggleTodo = (id) => {
        dispatch({
            type: ACTIONS.TOGGLE_TODO,
            payload: { id }
        });
    };

    const filteredTodos = state.todos.filter(todo => {
        if (state.filter === 'active') return !todo.completed;
        if (state.filter === 'completed') return todo.completed;
        return true;
    });

    return (
        

Lista zadań

{ if (e.key === 'Enter' && e.target.value.trim()) { addTodo(e.target.value.trim()); e.target.value = ''; } }} placeholder="Dodaj nowe zadanie..." />
{filteredTodos.map(todo => (
toggleTodo(todo.id)} /> {todo.text}
))}
); }

useContext - globalne zarządzanie stanem

useContext pozwala na dostęp do globalnego stanu bez prop drilling:

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

// Tworzenie context
const AuthContext = createContext();
const ThemeContext = createContext();

// Provider component
function AuthProvider({ children }) {
    const [user, setUser] = useState(null);
    const [isAuthenticated, setIsAuthenticated] = useState(false);

    const login = async (email, password) => {
        try {
            const response = await fetch('/api/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ email, password })
            });
            
            const data = await response.json();
            
            if (response.ok) {
                setUser(data.user);
                setIsAuthenticated(true);
                localStorage.setItem('token', data.token);
            } else {
                throw new Error(data.error);
            }
        } catch (error) {
            console.error('Login error:', error);
            throw error;
        }
    };

    const logout = () => {
        setUser(null);
        setIsAuthenticated(false);
        localStorage.removeItem('token');
    };

    const value = {
        user,
        isAuthenticated,
        login,
        logout
    };

    return (
        
            {children}
        
    );
}

// Custom hook do używania auth context
function useAuth() {
    const context = useContext(AuthContext);
    if (!context) {
        throw new Error('useAuth musi być używany wewnątrz AuthProvider');
    }
    return context;
}

// Komponent używający context
function LoginForm() {
    const { login, isAuthenticated } = useAuth();
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [error, setError] = useState('');

    const handleSubmit = async (e) => {
        e.preventDefault();
        try {
            await login(email, password);
        } catch (err) {
            setError(err.message);
        }
    };

    if (isAuthenticated) {
        return 
Jesteś zalogowany!
; } return (
setEmail(e.target.value)} placeholder="Email" required /> setPassword(e.target.value)} placeholder="Hasło" required /> {error &&
{error}
}
); }

Niestandardowe hooki - Custom Hooks

Custom hooki pozwalają na ponowne wykorzystanie logiki stanu:

// Custom hook do fetch danych
function useFetch(url, options = {}) {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const controller = new AbortController();
        
        const fetchData = async () => {
            try {
                setLoading(true);
                setError(null);
                
                const response = await fetch(url, {
                    ...options,
                    signal: controller.signal
                });
                
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const result = await response.json();
                setData(result);
            } catch (err) {
                if (err.name !== 'AbortError') {
                    setError(err.message);
                }
            } finally {
                setLoading(false);
            }
        };

        fetchData();

        return () => controller.abort();
    }, [url, JSON.stringify(options)]);

    return { data, loading, error };
}

// Custom hook do local storage
function useLocalStorage(key, initialValue) {
    const [storedValue, setStoredValue] = useState(() => {
        try {
            const item = window.localStorage.getItem(key);
            return item ? JSON.parse(item) : initialValue;
        } catch (error) {
            console.error(`Error reading localStorage key "${key}":`, error);
            return initialValue;
        }
    });

    const setValue = (value) => {
        try {
            const valueToStore = value instanceof Function ? value(storedValue) : value;
            setStoredValue(valueToStore);
            window.localStorage.setItem(key, JSON.stringify(valueToStore));
        } catch (error) {
            console.error(`Error setting localStorage key "${key}":`, error);
        }
    };

    return [storedValue, setValue];
}

// Custom hook do debouncing
function useDebounce(value, delay) {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
        const handler = setTimeout(() => {
            setDebouncedValue(value);
        }, delay);

        return () => {
            clearTimeout(handler);
        };
    }, [value, delay]);

    return debouncedValue;
}

// Użycie custom hooków
function SearchComponent() {
    const [searchTerm, setSearchTerm] = useLocalStorage('searchTerm', '');
    const debouncedSearchTerm = useDebounce(searchTerm, 500);
    const { data: searchResults, loading, error } = useFetch(
        debouncedSearchTerm ? `/api/search?q=${debouncedSearchTerm}` : null
    );

    return (
        
setSearchTerm(e.target.value)} placeholder="Wyszukaj..." /> {loading &&
Wyszukiwanie...
} {error &&
Błąd: {error}
} {searchResults && (
{searchResults.map(item => (
{item.name}
))}
)}
); }

useMemo i useCallback - optymalizacja wydajności

Te hooki pomagają w optymalizacji wydajności poprzez memoizację:

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

// Komponent potomny
const ExpensiveComponent = memo(({ items, onItemClick }) => {
    console.log('ExpensiveComponent rendered');
    
    const expensiveValue = useMemo(() => {
        console.log('Calculating expensive value...');
        return items.reduce((sum, item) => sum + item.value, 0);
    }, [items]);

    return (
        

Total: {expensiveValue}

{items.map(item => ( ))}
); }); // Komponent główny function OptimizedApp() { const [items, setItems] = useState([ { id: 1, name: 'Item 1', value: 10 }, { id: 2, name: 'Item 2', value: 20 }, { id: 3, name: 'Item 3', value: 30 } ]); const [count, setCount] = useState(0); // useCallback zapobiega niepotrzebnemu re-renderingowi const handleItemClick = useCallback((itemId) => { console.log('Item clicked:', itemId); setItems(prevItems => prevItems.map(item => item.id === itemId ? { ...item, value: item.value + 1 } : item ) ); }, []); // useMemo dla kosztownych obliczeń const filteredItems = useMemo(() => { console.log('Filtering items...'); return items.filter(item => item.value > 15); }, [items]); return (

Optimized App

Count: {count}

); }

useRef - dostęp do elementów DOM i wartości mutablenych

useRef pozwala na dostęp do elementów DOM i przechowywanie mutablenych wartości:

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

function FocusableInput() {
    const inputRef = useRef(null);
    const countRef = useRef(0);
    const [renders, setRenders] = useState(0);

    useEffect(() => {
        // Auto focus na input po mount
        if (inputRef.current) {
            inputRef.current.focus();
        }
    }, []);

    useEffect(() => {
        // Liczenie renderów bez powodowania re-render
        countRef.current += 1;
        console.log(`Component rendered ${countRef.current} times`);
    });

    const focusInput = () => {
        if (inputRef.current) {
            inputRef.current.focus();
        }
    };

    const scrollToBottom = () => {
        if (inputRef.current) {
            inputRef.current.scrollIntoView({ behavior: 'smooth' });
        }
    };

    return (
        

Renderów: {countRef.current}

); } // Hook do mierzenia wymiarów elementu function useElementSize() { const ref = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { const element = ref.current; if (!element) return; const resizeObserver = new ResizeObserver((entries) => { if (entries[0]) { const { width, height } = entries[0].contentRect; setSize({ width, height }); } }); resizeObserver.observe(element); return () => { resizeObserver.disconnect(); }; }, []); return [ref, size]; } // Użycie custom hook function ResizableDiv() { const [ref, size] = useElementSize(); return (
Zmień rozmiar tego elementu

Wymiary: {Math.round(size.width)} x {Math.round(size.height)}

); }

Najlepsze praktyki i częste błędy

✅ Dobre praktyki

  • Używaj dependency array - zawsze określaj zależności w useEffect
  • Wydzielaj custom hooki - dla powtarzalnej logiki
  • Optymalizuj re-rendery - używaj memo, useMemo, useCallback mądrze
  • Cleanup effects - zawsze sprzątaj po sobie

❌ Częste błędy

// ❌ Źle - nieskończona pętla
useEffect(() => {
    setData(fetchData());
}); // Brak dependency array!

// ✅ Dobrze
useEffect(() => {
    const loadData = async () => {
        const result = await fetchData();
        setData(result);
    };
    loadData();
}, []); // Pusta dependency array

// ❌ Źle - niepotrzebne re-rendery
const handleClick = () => {
    console.log('clicked');
};

// ✅ Dobrze
const handleClick = useCallback(() => {
    console.log('clicked');
}, []);

// ❌ Źle - mutowanie stanu bezpośrednio
const addItem = () => {
    items.push(newItem); // Mutacja!
    setItems(items);
};

// ✅ Dobrze - niemutacyjne aktualizacje
const addItem = () => {
    setItems(prevItems => [...prevItems, newItem]);
};

Testowanie hooków

Przykład testowania custom hooków z React Testing Library:

import { renderHook, act } from '@testing-library/react';
import { useLocalStorage } from './useLocalStorage';

describe('useLocalStorage', () => {
    beforeEach(() => {
        localStorage.clear();
    });

    test('should initialize with initial value', () => {
        const { result } = renderHook(() => 
            useLocalStorage('test-key', 'initial')
        );

        expect(result.current[0]).toBe('initial');
    });

    test('should update localStorage when value changes', () => {
        const { result } = renderHook(() => 
            useLocalStorage('test-key', 'initial')
        );

        act(() => {
            result.current[1]('updated');
        });

        expect(result.current[0]).toBe('updated');
        expect(localStorage.getItem('test-key')).toBe('"updated"');
    });
});

Podsumowanie

React Hooks to potężne narzędzie, które znacznie upraszcza tworzenie komponentów React. Kluczem do sukcesu jest:

  1. Zrozumienie fundamentów - useState, useEffect, useContext
  2. Tworzenie custom hooków - dla powtarzalnej logiki
  3. Optymalizacja wydajności - mądre używanie memo i callback
  4. Testowanie - pisanie testów dla hooków
  5. Śledzenie najnowszych wzorców - React ciągle się rozwija

Hooks zmieniły sposób myślenia o komponentach React - od klas do funkcji, od dziedziczenia do kompozycji. Opanowanie hooków jest kluczowe dla nowoczesnego rozwoju React.

Chcesz pogłębić swoją wiedzę o React? Sprawdź nasz kurs JavaScript i React, gdzie poznasz wszystkie zaawansowane techniki!