לדלג לתוכן

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 - שמירת תוצאות קודמות

תחביר בסיסי

const memoizedValue = useMemo(() => {
  // חישוב יקר
  return computeExpensiveValue(a, b);
}, [a, b]);
  • הפרמטר הראשון הוא פונקציה שמחזירה את הערך המחושב
  • הפרמטר השני הוא מערך תלויות - החישוב יתבצע מחדש רק כשאחד מהערכים משתנה

דוגמה - סינון רשימה

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 לקומפוננטות ילד מותאמות אישית
  • חשוב להשתמש בהוקים אלה רק כשיש צורך אמיתי - אופטימיזציה מוקדמת מוסיפה מורכבות מיותרת
  • הכלל: כתבו קוד פשוט קודם, ורק אם יש בעיית ביצועים - הוסיפו אופטימיזציה