React Hooks - Praktyczny przewodnik od podstaw do zaawansowanych
React Hooks Praktyczny
poradnikiReact 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!
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.