React Hooks - zaawansowane techniki i najlepsze praktyki
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 (
);
}
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 (
);
}
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:
- Zrozumienie fundamentów - useState, useEffect, useContext
- Tworzenie custom hooków - dla powtarzalnej logiki
- Optymalizacja wydajności - mądre używanie memo i callback
- Testowanie - pisanie testów dla hooków
- Ś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!