Gdy użytkownik wpisuje w search, nie chcesz wysyłać API request przy każdej literze. Debouncing rozwiązuje ten problem!
Debouncing to opóźnienie wykonania funkcji do momentu, gdy użytkownik przestanie działać.
1function SearchPirates() {
2 const [query, setQuery] = useState("");
3 const [results, setResults] = useState<Pirate[]>([]);
4
5 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
6 const value = e.target.value;
7 setQuery(value);
8
9 // ❌ API call przy KAŻDEJ literze!
10 fetchPirates(value).then(setResults);
11 };
12
13 return <input value={query} onChange={handleChange} />;
14}Problem:
1import { useState, useEffect } from 'react';
2
3function SearchPirates() {
4 const [query, setQuery] = useState("");
5 const [debouncedQuery, setDebouncedQuery] = useState("");
6 const [results, setResults] = useState<Pirate[]>([]);
7
8 // Debounce - czekaj 500ms po ostatniej zmianie
9 useEffect(() => {
10 const timer = setTimeout(() => {
11 setDebouncedQuery(query);
12 }, 500);
13
14 return () => clearTimeout(timer);
15 }, [query]);
16
17 // Fetch tylko gdy debouncedQuery się zmieni
18 useEffect(() => {
19 if (debouncedQuery) {
20 fetchPirates(debouncedQuery).then(setResults);
21 }
22 }, [debouncedQuery]);
23
24 return (
25 <div>
26 <input
27 value={query}
28 onChange={(e) => setQuery(e.target.value)}
29 placeholder="Szukaj piratów..."
30 />
31 {results.map(pirate => <div key={pirate.id}>{pirate.name}</div>)}
32 </div>
33 );
34}Efekt:
Zrób reużywalny hook:
1function useDebounce<T>(value: T, delay: number): T {
2 const [debouncedValue, setDebouncedValue] = useState<T>(value);
3
4 useEffect(() => {
5 const timer = setTimeout(() => {
6 setDebouncedValue(value);
7 }, delay);
8
9 return () => clearTimeout(timer);
10 }, [value, delay]);
11
12 return debouncedValue;
13}
14
15// Użycie - super proste!
16function SearchPirates() {
17 const [query, setQuery] = useState("");
18 const debouncedQuery = useDebounce(query, 500);
19 const [results, setResults] = useState<Pirate[]>([]);
20
21 useEffect(() => {
22 if (debouncedQuery) {
23 fetchPirates(debouncedQuery).then(setResults);
24 }
25 }, [debouncedQuery]);
26
27 return (
28 <input
29 value={query}
30 onChange={(e) => setQuery(e.target.value)}
31 placeholder="Szukaj piratów..."
32 />
33 );
34}Throttling to wykonywanie funkcji max raz na X ms (niezależnie od ilości wywołań).
| | Debounce | Throttle | |---|----------|----------| | Kiedy wykonuje | Po X ms ciszy | Co X ms | | Użycie | Search, resize, typing | Scroll, mouse move |
1function useThrottle<T>(value: T, limit: number): T {
2 const [throttledValue, setThrottledValue] = useState<T>(value);
3 const lastRan = useRef(Date.now());
4
5 useEffect(() => {
6 const handler = setTimeout(() => {
7 if (Date.now() - lastRan.current >= limit) {
8 setThrottledValue(value);
9 lastRan.current = Date.now();
10 }
11 }, limit - (Date.now() - lastRan.current));
12
13 return () => clearTimeout(handler);
14 }, [value, limit]);
15
16 return throttledValue;
17}
18
19// Użycie - scroll position
20function ScrollTracker() {
21 const [scrollY, setScrollY] = useState(0);
22 const throttledScrollY = useThrottle(scrollY, 200);
23
24 useEffect(() => {
25 const handleScroll = () => setScrollY(window.scrollY);
26 window.addEventListener('scroll', handleScroll);
27 return () => window.removeEventListener('scroll', handleScroll);
28 }, []);
29
30 return <div>Scroll: {throttledScrollY}px</div>;
31}React.memo zapobiega niepotrzebnemu re-renderowaniu komponentów.1function PostCard({ post }: { post: Post }) {
2 console.log("Rendering PostCard:", post.id);
3 return <div>{post.content}</div>;
4}
5
6// Parent re-renderuje → WSZYSTKIE PostCard re-renderują!1import { memo } from 'react';
2
3const PostCard = memo(function PostCard({ post }: { post: Post }) {
4 console.log("Rendering PostCard:", post.id);
5 return <div>{post.content}</div>;
6});
7
8// Parent re-renderuje → PostCard re-renderuje TYLKO jeśli post się zmienił!useMemo cache'uje wynik kosztownej operacji.1function PirateStats({ pirates }: { pirates: Pirate[] }) {
2 // ❌ Bez memo - liczy przy KAŻDYM renderze
3 const totalLevel = pirates.reduce((sum, p) => sum + p.level, 0);
4
5 // ✅ Z memo - liczy tylko gdy pirates się zmienią
6 const totalLevel = useMemo(() => {
7 console.log("Calculating total level...");
8 return pirates.reduce((sum, p) => sum + p.level, 0);
9 }, [pirates]);
10
11 return <div>Total Level: {totalLevel}</div>;
12}Kiedy używać useMemo:
Kiedy NIE używać:
useCallback cache'uje funkcję między renderami.1function Parent() {
2 const [count, setCount] = useState(0);
3
4 // ❌ Nowa funkcja przy każdym renderze!
5 const handleClick = () => console.log("Clicked");
6
7 return <MemoizedChild onClick={handleClick} />;
8 // Child re-renderuje mimo memo, bo handleClick jest zawsze "nowy"
9}1import { useCallback } from 'react';
2
3function Parent() {
4 const [count, setCount] = useState(0);
5
6 // ✅ Ta sama funkcja między renderami
7 const handleClick = useCallback(() => {
8 console.log("Clicked");
9 }, []); // Pusty array - funkcja nigdy się nie zmienia
10
11 return <MemoizedChild onClick={handleClick} />;
12 // Child NIE re-renderuje (handleClick jest ten sam)
13}Kiedy używać useCallback:
React.memo)useEffectkey prop jest KRYTYCZNY dla wydajności list!
1// ❌ BAD - używanie index
2{pirates.map((pirate, index) => (
3 <PirateCard key={index} pirate={pirate} />
4))}
5
6// ✅ GOOD - używanie unique ID
7{pirates.map((pirate) => (
8 <PirateCard key={pirate.id} pirate={pirate} />
9))}Dlaczego index jest zły:
✅ Debouncing - czekaj X ms po ostatniej akcji (search) ✅ Throttling - wykonuj max raz na X ms (scroll) ✅ React.memo - zapobiega re-renderom komponentów ✅ useMemo - cache'uje wyniki kosztownych operacji ✅ useCallback - cache'uje funkcje ✅ key prop - używaj unique ID, nie index
Performance Rule: Optymalizuj tylko gdy jest problem - przedwczesna optymalizacja to zło!
Do zobaczenia! 🚀