React 19: היכולות החדשות

React 19 יצאה בדצמבר 2024, והגרסאות 19.1 ו-19.2 הגיעו אחריה ב-2025. הדגש שלה הוא עבודה עם נתונים וטפסים: פחות useState ידני לסטטוס טעינה ושגיאה, ויותר כלים מובנים. השיעור הזה עובר על מה שחדש, ועל מה שהוסר ותפגשו בקוד ישן.

לפני שמתחילים: כל מה שכאן עובד בפרויקט Vite רגיל עם react@19. רק Server Components ו-Server Actions (בסוף השיעור) דורשים פריימוורק כמו Next.js.

Actions: פונקציה אסינכרונית שמנהלת את עצמה

עד React 19, שליחת טופס דרשה לנהל ידנית שלושה מצבים: הנתונים, "בטעינה" ושגיאה. Action היא פונקציה (גם אסינכרונית) שמעבירים ל-React, ו-React מנהלת בשבילכם את מצב ההמתנה.

הדרך הכי פשוטה: להעביר פונקציה ל-action של טופס. React קוראת לה עם FormData, ומאפסת את הטופס בסיום.

function NewsletterForm() {
    async function subscribe(formData) {
        const email = formData.get('email');
        await fetch('/api/subscribe', {
            method: 'POST',
            body: JSON.stringify({ email }),
        });
    }

    return (
        <form action={subscribe}>
            <input name="email" type="email" required />
            <button type="submit">הרשמה</button>
        </form>
    );
}

שימו לב: אין onSubmit, אין event.preventDefault() ואין useState לשדה. הטופס עובד עם ה-name של כל שדה.

useActionState: תוצאה, שגיאה ו"בטעינה" בהוק אחד

כשצריך להציג תוצאה או שגיאה, useActionState עוטף את ה-Action ומחזיר שלושה דברים:

import { useActionState } from 'react';

async function updateName(previousState, formData) {
    const name = formData.get('name');
    if (name.length < 2) {
        return { error: 'השם קצר מדי' };
    }
    try {
        await saveName(name);
        return { error: null, saved: name };
    } catch (error) {
        return { error: 'השמירה נכשלה, נסו שוב' };
    }
}

function NameForm() {
    const [state, formAction, isPending] = useActionState(updateName, { error: null });

    return (
        <form action={formAction}>
            <input name="name" />
            <button disabled={isPending}>
                {isPending ? 'שומר...' : 'שמירה'}
            </button>
            {state.error && <p role="alert">{state.error}</p>}
            {state.saved && <p>נשמר: {state.saved}</p>}
        </form>
    );
}
  • state - מה שה-Action החזירה בפעם האחרונה (בהתחלה: הערך ההתחלתי).
  • formAction - הפונקציה שמעבירים ל-action של הטופס.
  • isPending - true כל עוד ה-Action רצה.

ה-Action מקבלת את ה-state הקודם כפרמטר ראשון ואת ה-FormData כשני. בגרסאות הבטא של React 19 ההוק נקרא useFormState, ותפגשו את השם הזה במדריכים ישנים.

useFormStatus: מצב הטופס מתוך קומפוננטת ילד

כפתור שליחה שחוזר בהרבה טפסים לא צריך לקבל isPending כ-prop. useFormStatus קורא את מצב הטופס שהוא נמצא בתוכו:

import { useFormStatus } from 'react-dom';

function SubmitButton({ children }) {
    const { pending } = useFormStatus();
    return (
        <button type="submit" disabled={pending}>
            {pending ? 'שולח...' : children}
        </button>
    );
}

function ContactForm() {
    return (
        <form action={sendMessage}>
            <textarea name="message" />
            <SubmitButton>שליחה</SubmitButton>
        </form>
    );
}

חשוב: useFormStatus מגיע מ-react-dom (לא מ-react), ועובד רק בקומפוננטה שמרונדרת בתוך ה-<form>, לא בקומפוננטה שמגדירה את הטופס.

useOptimistic: להציג את התוצאה לפני שהשרת ענה

בלייק, בהודעה חדשה או בסימון משימה, המשתמש מצפה לראות את השינוי מיד. useOptimistic מציג ערך "אופטימי" בזמן שה-Action רצה, וחוזר לערך האמיתי כשהיא מסתיימת:

import { useOptimistic } from 'react';

function Messages({ messages, sendMessage }) {
    const [optimisticMessages, addOptimistic] = useOptimistic(
        messages,
        (current, newText) => [...current, { text: newText, sending: true }]
    );

    async function formAction(formData) {
        const text = formData.get('text');
        addOptimistic(text);      // מופיע מיד ברשימה
        await sendMessage(text);  // כשהשרת עונה, messages מתעדכן מההורה
    }

    return (
        <>
            <ul>
                {optimisticMessages.map((message, index) => (
                    <li key={index}>
                        {message.text} {message.sending && <small>(נשלח...)</small>}
                    </li>
                ))}
            </ul>
            <form action={formAction}>
                <input name="text" />
                <button>שליחה</button>
            </form>
        </>
    );
}

אם השליחה נכשלת, הערך האופטימי פשוט נעלם והרשימה חוזרת למה שבאמת נשמר.

use: לקרוא Promise או Context

use הוא API חדש שקורא ערך ממשאב: Promise או Context. בניגוד להוקים, מותר לקרוא לו בתוך תנאי או לולאה.

import { use, Suspense } from 'react';

function Comments({ commentsPromise }) {
    // React משהה את הקומפוננטה עד שה-Promise מסתיים
    const comments = use(commentsPromise);
    return comments.map((comment) => <p key={comment.id}>{comment.text}</p>);
}

function Post({ commentsPromise }) {
    return (
        <Suspense fallback={<p>טוען תגובות...</p>}>
            <Comments commentsPromise={commentsPromise} />
        </Suspense>
    );
}

ה-Promise צריך להיווצר מחוץ לקומפוננטה (בהורה, בספריית נתונים או בפריימוורק). Promise שנוצר מחדש בכל רינדור יגרום ללולאה אינסופית. עם Context, use(ThemeContext) עושה את מה ש-useContext עושה, אבל אפשר לקרוא לו גם אחרי if.

ref כ-prop רגיל: להיפרד מ-forwardRef

עד React 19, קומפוננטה שרצתה לקבל ref הייתה צריכה לעטוף את עצמה ב-forwardRef. עכשיו ref הוא prop רגיל:

// React 19
function TextInput({ ref, ...props }) {
    return <input ref={ref} {...props} />;
}

// שימוש
const inputRef = useRef(null);
<TextInput ref={inputRef} placeholder="שם" />

גם Context התקצר: אפשר לכתוב <ThemeContext value="dark"> במקום <ThemeContext.Provider value="dark">.

title ו-meta מתוך קומפוננטה

תגיות <title>, <meta> ו-<link> שמרונדרות בכל מקום בעץ עוברות אוטומטית ל-<head>. כבר לא צריך ספרייה כמו react-helmet בשביל כותרת לכל דף:

function ProductPage({ product }) {
    return (
        <article>
            <title>{product.name} | החנות שלי</title>
            <meta name="description" content={product.summary} />
            <h1>{product.name}</h1>
        </article>
    );
}

React Compiler: פחות useMemo ו-useCallback

React Compiler, שגרסה 1.0 שלו יצאה באוקטובר 2025, הוא כלי build שמוסיף memoization אוטומטית. הוא מנתח את הקוד בזמן הבנייה ומדלג בעצמו על רינדורים מיותרים, כך שברוב המקרים כבר לא צריך לעטוף ידנית ב-useMemo, useCallback ו-React.memo.

npm install --save-dev babel-plugin-react-compiler
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [
        react({
            babel: { plugins: ['babel-plugin-react-compiler'] },
        }),
    ],
});

הקומפיילר מניח שהקוד עומד בחוקי ה-Hooks ולא משנה state ישירות. מה שלמדתם בשיעור אופטימיזציית ביצועים עדיין חשוב: כדי להבין מה הקומפיילר עושה, ולפרויקטים שעוד לא עברו אליו.

React 19.2: Activity ו-useEffectEvent

<Activity> מסתיר חלק מהממשק בלי לאבד את ה-state שלו. טאב שחוזרים אליו נשאר בדיוק כמו שעזבתם אותו:

<Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
    <Settings />
</Activity>

useEffectEvent יוצר פונקציה שקוראים לה מתוך Effect. היא תמיד רואה את הערכים העדכניים, אבל לא נכנסת לרשימת התלויות. כך Effect לא "מתחבר מחדש" רק כי ערך משני השתנה:

function ChatRoom({ roomId, theme }) {
    const onConnected = useEffectEvent(() => {
        showNotification('מחובר!', theme);
    });

    useEffect(() => {
        const connection = createConnection(roomId);
        connection.on('connected', () => onConnected());
        connection.connect();
        return () => connection.disconnect();
    }, [roomId]); // theme לא ברשימה, ובכל זאת onConnected רואה את העדכני
}

Server Components ו-Server Actions

React 19 הפכה ליציבים שני רעיונות שרצים בשרת, ולכן דורשים פריימוורק (Next.js, או React Router במצב פריימוורק):

  • Server Components - קומפוננטות שרצות רק בשרת, ניגשות ישירות למסד הנתונים, ולא שולחות את הקוד שלהן לדפדפן בכלל.
  • Server Actions - פונקציה שמסומנת ב-'use server' ורצה בשרת, אבל אפשר להעביר אותה ל-action של טופס בדפדפן כאילו הייתה פונקציה רגילה.
// actions.js
'use server';

export async function addTodo(formData) {
    await db.todos.create({ title: formData.get('title') });
}

זה נושא לקורס Next.js בפני עצמו. חשוב לדעת שהוא קיים, ושה-Actions שלמדתם למעלה הם אותו מנגנון בדיוק, רק בצד הלקוח.

מה הוסר, ותפגשו בקוד ישן

ישןבפרויקט חדש
ReactDOM.render(...)createRoot(el).render(...) - הישן הוסר ב-React 19
forwardRefref כ-prop רגיל (הישן עדיין עובד)
propTypes ו-defaultProps בקומפוננטות פונקציהTypeScript, וערכי ברירת מחדל בפירוק ה-props
Context.Provider<Context value={...}>
create-react-appVite או פריימוורק (ראו התקנה והגדרות)
String refs (ref="input")useRef

שדרוג פרויקט קיים

npm install react@19 react-dom@19

# codemods רשמיים שמתקנים את רוב השינויים אוטומטית
npx codemod@latest react/19/migration-recipe

מומלץ לשדרג קודם ל-React 18.3, שמציגה אזהרות על כל מה שהוסר ב-19, לתקן אותן, ורק אז לעבור ל-19.

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה מחזיר useActionState?

    1. Promise
    2. את הטופס
    3. [state, formAction, isPending]
    4. רק את התוצאה
    הצגת התשובה

    תשובה ג. התוצאה האחרונה, הפונקציה לטופס, ומצב ההמתנה.

  2. איפה עובד useFormStatus?

    1. רק בקומפוננטה שמרונדרת בתוך ה-form, והוא מגיע מ-react-dom
    2. רק בשרת
    3. בכל קומפוננטה
    4. רק עם Redux
    הצגת התשובה

    תשובה א. הוא קורא את מצב הטופס שהוא נמצא בתוכו, לא את זה שמוגדר באותה קומפוננטה.

  3. מה עושה React Compiler?

    1. מריץ בדיקות
    2. מוסיף memoization אוטומטית בזמן הבנייה, ומייתר ברוב המקרים useMemo ו-useCallback
    3. ממיר ל-TypeScript
    4. יוצר Server Components
    הצגת התשובה

    תשובה ב. גרסה 1.0 יצאה באוקטובר 2025, והוא מניח שהקוד עומד בחוקי ה-Hooks.