7.1 useRef, useMemo ו useCallback הרצאה
הוקים מתקדמים - useRef, useMemo ו-useCallback¶
בשיעור הזה נלמד על שלושה הוקים מתקדמים שמאפשרים לנו שליטה טובה יותר בביצועים ובהתנהגות של קומפוננטות ריאקט. הוקים אלה הם כלים חיוניים בארגז הכלים של כל מפתח ריאקט.
הוק useRef¶
מה זה Ref?¶
- הוק useRef מחזיר אובייקט עם שדה
currentשנשמר בין רנדרים - שינוי הערך של
currentלא גורם לרנדר מחדש - שימושים עיקריים: גישה לאלמנטי DOM, ושמירת ערכים שלא צריכים לגרום לרנדר
גישה לאלמנטי DOM - DOM References¶
השימוש הנפוץ ביותר של useRef הוא גישה ישירה לאלמנט DOM:
import { useRef } from "react";
function TextInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleFocus = () => {
inputRef.current?.focus();
};
return (
<div>
<input ref={inputRef} type="text" placeholder="הקלד כאן..." />
<button onClick={handleFocus}>התמקד בשדה</button>
</div>
);
}
- אנחנו יוצרים ref עם
useRef<HTMLInputElement>(null) - מחברים אותו לאלמנט דרך הפרופ
ref - ניגשים לאלמנט דרך
inputRef.current
דוגמה - גלילה לאלמנט¶
import { useRef } from "react";
function ScrollToSection() {
const sectionRef = useRef<HTMLDivElement>(null);
const scrollToSection = () => {
sectionRef.current?.scrollIntoView({ behavior: "smooth" });
};
return (
<div>
<button onClick={scrollToSection}>גלול לסקציה</button>
<div style={{ height: "100vh" }} />
<div ref={sectionRef}>
<h2>הסקציה שאנחנו רוצים להגיע אליה</h2>
</div>
</div>
);
}
שמירת ערכים ללא רנדר - Mutable Values¶
הuseRef מתאים גם לשמירת ערכים שצריכים להישמר בין רנדרים, אבל שינוי שלהם לא צריך לגרום לרנדר מחדש:
import { useRef, useState, useEffect } from "react";
function StopWatch() {
const [time, setTime] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const intervalRef = useRef<number | null>(null);
const start = () => {
if (isRunning) return;
setIsRunning(true);
intervalRef.current = window.setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
};
const stop = () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setIsRunning(false);
};
const reset = () => {
stop();
setTime(0);
};
useEffect(() => {
return () => {
if (intervalRef.current !== null) {
clearInterval(intervalRef.current);
}
};
}, []);
return (
<div>
<p>זמן: {time} שניות</p>
<button onClick={start}>התחל</button>
<button onClick={stop}>עצור</button>
<button onClick={reset}>אפס</button>
</div>
);
}
- שמירת ה-interval ID ב-ref ולא ב-state, כי שינוי שלו לא צריך לגרום לרנדר
- ה-ref נשמר בין רנדרים, כך שנוכל לנקות את ה-interval בכל רגע
שמירת הערך הקודם של state¶
import { useRef, useState, useEffect } from "react";
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>ערך נוכחי: {count}</p>
<p>ערך קודם: {previousCount}</p>
<button onClick={() => setCount(count + 1)}>הגדל</button>
</div>
);
}
העברת Ref לקומפוננטת ילד - forwardRef¶
כאשר רוצים להעביר ref לקומפוננטת ילד, צריך להשתמש ב-forwardRef:
import { forwardRef, useRef } from "react";
interface CustomInputProps {
label: string;
placeholder?: string;
}
const CustomInput = forwardRef<HTMLInputElement, CustomInputProps>(
({ label, placeholder }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
</div>
);
}
);
function Form() {
const nameRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log("Name:", nameRef.current?.value);
nameRef.current?.focus();
};
return (
<div>
<CustomInput ref={nameRef} label="שם" placeholder="הכנס שם..." />
<button onClick={handleSubmit}>שלח</button>
</div>
);
}
- בלי forwardRef, העברת ref לקומפוננטה מותאמת לא תעבוד
- ב-React 19 אפשר לקבל ref כפרופ רגיל בלי forwardRef
הוק useMemo¶
מה זה useMemo?¶
- הוק useMemo מאפשר לשמור תוצאה של חישוב יקר ולהשתמש בה שוב, כל עוד התלויות לא השתנו
- זהו מנגנון של Memoization - שמירת תוצאות קודמות
תחביר בסיסי¶
- הפרמטר הראשון הוא פונקציה שמחזירה את הערך המחושב
- הפרמטר השני הוא מערך תלויות - החישוב יתבצע מחדש רק כשאחד מהערכים משתנה
דוגמה - סינון רשימה¶
import { useState, useMemo } from "react";
interface Product {
id: number;
name: string;
price: number;
category: string;
}
function ProductList({ products }: { products: Product[] }) {
const [search, setSearch] = useState("");
const [sortBy, setSortBy] = useState<"name" | "price">("name");
const filteredAndSorted = useMemo(() => {
console.log("מחשב רשימה מסוננת...");
const filtered = products.filter((product) =>
product.name.toLowerCase().includes(search.toLowerCase())
);
return filtered.sort((a, b) => {
if (sortBy === "name") return a.name.localeCompare(b.name);
return a.price - b.price;
});
}, [products, search, sortBy]);
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="חפש מוצר..."
/>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as "name" | "price")}
>
<option value="name">מיין לפי שם</option>
<option value="price">מיין לפי מחיר</option>
</select>
<ul>
{filteredAndSorted.map((product) => (
<li key={product.id}>
{product.name} - {product.price} ש"ח
</li>
))}
</ul>
</div>
);
}
- ללא useMemo, הסינון והמיון יתבצעו בכל רנדר, גם אם products, search ו-sortBy לא השתנו
- עם useMemo, החישוב יתבצע רק כשאחד מהערכים במערך התלויות משתנה
דוגמה - חישוב מתמטי כבד¶
import { useState, useMemo } from "react";
function FibonacciCalculator() {
const [num, setNum] = useState(10);
const [theme, setTheme] = useState("light");
const fibonacci = useMemo(() => {
const fib = (n: number): number => {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
};
return fib(num);
}, [num]);
return (
<div className={theme}>
<input
type="number"
value={num}
onChange={(e) => setNum(Number(e.target.value))}
/>
<p>התוצאה: {fibonacci}</p>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
החלף ערכת נושא
</button>
</div>
);
}
- כשנלחץ על כפתור החלפת ערכת הנושא, החישוב של פיבונאצ'י לא יתבצע מחדש כי num לא השתנה
מתי לא להשתמש ב-useMemo¶
- חישובים פשוטים - ה-overhead של useMemo עצמו יהיה גדול מהחיסכון
- כשהתלויות משתנות בכל רנדר - ה-memoization לא יעזור
- כשאין בעיית ביצועים אמיתית - אופטימיזציה מוקדמת מוסיפה מורכבות מיותרת
הוק useCallback¶
מה זה useCallback?¶
- הוק useCallback שומר הפניה יציבה לפונקציה בין רנדרים
- הפונקציה תיווצר מחדש רק כשאחת מהתלויות משתנה
- בעצם,
useCallback(fn, deps)שווה ל-useMemo(() => fn, deps)
למה צריך הפניה יציבה?¶
בריאקט, כל רנדר יוצר פונקציות חדשות:
function Parent() {
const [count, setCount] = useState(0);
// נוצרת מחדש בכל רנדר
const handleClick = () => {
console.log("clicked");
};
return <Child onClick={handleClick} />;
}
- בכל רנדר של Parent, נוצרת פונקציה חדשה של handleClick
- זה אומר שגם אם Child עטוף ב-React.memo, הוא עדיין ירנדר מחדש כי ה-props השתנו
דוגמה בסיסית¶
import { useState, useCallback, memo } from "react";
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
}
const ExpensiveButton = memo(({ onClick, children }: ButtonProps) => {
console.log("ExpensiveButton rendered");
return <button onClick={onClick}>{children}</button>;
});
function App() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleIncrement = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
return (
<div>
<p>מונה: {count}</p>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ExpensiveButton onClick={handleIncrement}>הגדל</ExpensiveButton>
</div>
);
}
- בלי useCallback, כתיבה בשדה הטקסט תגרום גם ל-ExpensiveButton לרנדר מחדש
- עם useCallback, הפונקציה handleIncrement שומרת על אותה הפניה, וה-memo של ExpensiveButton עובד
useCallback עם תלויות¶
import { useState, useCallback } from "react";
function SearchComponent() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<string[]>([]);
const handleSearch = useCallback(
async (searchTerm: string) => {
const response = await fetch(`/api/search?q=${searchTerm}&query=${query}`);
const data = await response.json();
setResults(data);
},
[query]
);
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button onClick={() => handleSearch(query)}>חפש</button>
<ul>
{results.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
</div>
);
}
useCallback ב-useEffect¶
import { useState, useCallback, useEffect } from "react";
function DataFetcher({ userId }: { userId: string }) {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
const result = await response.json();
setData(result);
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data ? JSON.stringify(data) : "טוען..."}</div>;
}
- בלי useCallback, fetchData תיווצר מחדש בכל רנדר, וה-useEffect ירוץ מחדש בכל פעם
- עם useCallback, fetchData תיווצר מחדש רק כש-userId משתנה
השוואה בין שלושת ההוקים¶
| הוק | מטרה | מחזיר | רנדר מחדש? |
|---|---|---|---|
| useRef | שמירת ערך בין רנדרים / גישה ל-DOM | אובייקט עם current | לא |
| useMemo | שמירת תוצאת חישוב | הערך המחושב | לא (המטרה למנוע חישוב) |
| useCallback | שמירת הפניה לפונקציה | הפונקציה עצמה | לא (המטרה למנוע רנדר של ילדים) |
מתי להשתמש ומתי לא¶
כללי אצבע¶
- השתמשו ב-useRef כש: צריכים גישה ל-DOM, או שומרים ערכים שלא צריכים לגרום לרנדר (כמו timer IDs)
- השתמשו ב-useMemo כש: יש חישוב יקר שרוצים להימנע מלהריץ שוב, או כשמעבירים אובייקט/מערך כ-dependency ל-useEffect
- השתמשו ב-useCallback כש: מעבירים פונקציה כ-prop לקומפוננטה עטופה ב-memo, או כשהפונקציה היא dependency של useEffect
מתי לא להשתמש¶
// לא צריך - חישוב פשוט
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);
// עדיף פשוט:
const fullName = `${firstName} ${lastName}`;
// לא צריך - הקומפוננטה לא עטופה ב-memo
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
// אם Child לא עטוף ב-memo, אין טעם ב-useCallback
// לא צריך - התלויות משתנות בכל רנדר
const data = useMemo(() => processData(items), [items]);
// אם items הוא מערך חדש בכל רנדר, ה-useMemo לא יעזור
דוגמה מסכמת¶
import { useState, useRef, useMemo, useCallback, memo } from "react";
interface Todo {
id: number;
text: string;
completed: boolean;
}
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
const TodoItem = memo(({ todo, onToggle, onDelete }: TodoItemProps) => {
console.log(`Rendering todo: ${todo.text}`);
return (
<li>
<span
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
onClick={() => onToggle(todo.id)}
>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>מחק</button>
</li>
);
});
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<"all" | "active" | "completed">("all");
const inputRef = useRef<HTMLInputElement>(null);
const nextIdRef = useRef(1);
const addTodo = () => {
const text = inputRef.current?.value.trim();
if (!text) return;
setTodos((prev) => [
...prev,
{ id: nextIdRef.current++, text, completed: false },
]);
if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.focus();
}
};
const toggleTodo = useCallback((id: number) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const deleteTodo = useCallback((id: number) => {
setTodos((prev) => prev.filter((todo) => todo.id !== id));
}, []);
const filteredTodos = useMemo(() => {
switch (filter) {
case "active":
return todos.filter((t) => !t.completed);
case "completed":
return todos.filter((t) => t.completed);
default:
return todos;
}
}, [todos, filter]);
const stats = useMemo(() => {
const total = todos.length;
const completed = todos.filter((t) => t.completed).length;
const active = total - completed;
return { total, completed, active };
}, [todos]);
return (
<div>
<h1>רשימת משימות</h1>
<div>
<input ref={inputRef} placeholder="משימה חדשה..." />
<button onClick={addTodo}>הוסף</button>
</div>
<div>
<button onClick={() => setFilter("all")}>הכל ({stats.total})</button>
<button onClick={() => setFilter("active")}>
פעילות ({stats.active})
</button>
<button onClick={() => setFilter("completed")}>
הושלמו ({stats.completed})
</button>
</div>
<ul>
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
</div>
);
}
בדוגמה הזו השתמשנו בכל שלושת ההוקים:
- useRef - לגישה לשדה הקלט ולשמירת מונה ID
- useMemo - לסינון המשימות ולחישוב הסטטיסטיקות
- useCallback - לפונקציות toggle ו-delete שמועברות לקומפוננטות ילד עטופות ב-memo
סיכום¶
- הוק useRef מאפשר גישה ישירה לאלמנטי DOM ושמירת ערכים בין רנדרים ללא גרימת רנדר מחדש
- הוק useMemo שומר תוצאות של חישובים יקרים ומחשב מחדש רק כשהתלויות משתנות
- הוק useCallback שומר הפניה יציבה לפונקציה, מה שמונע רנדרים מיותרים של קומפוננטות ילד
- forwardRef מאפשר להעביר ref לקומפוננטות ילד מותאמות אישית
- חשוב להשתמש בהוקים אלה רק כשיש צורך אמיתי - אופטימיזציה מוקדמת מוסיפה מורכבות מיותרת
- הכלל: כתבו קוד פשוט קודם, ורק אם יש בעיית ביצועים - הוסיפו אופטימיזציה