Menu

טעינת נתונים ב-React עם useEffect: טעינה, שגיאות, race

טוענים נתונים ב-React על ידי התחלת הבקשה ב-useEffect, החזקת loading, error ו-data ב-state, והתעלמות מתשובות שמגיעות אחרי שהקלטים השתנו. הדף מכסה async/await באפקטים, AbortController, race conditions ומתי להשתמש בספרייה במקום.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

כדי לטעון נתונים ב-React, התחילו את הבקשה בתוך useEffect, שמרו את התשובה ב-state, ורנדרו הודעת טעינה עד שהיא מגיעה. שימו את הערכים שהבקשה תלויה בהם, כמו id, במערך התלויות כדי שהאפקט יטען שוב כשהם משתנים.

הדוגמאות בדף הזה לא יכולות להגיע לרשת, ולכן fetchUser הוא API מזויף: promise שמסתיים אחרי השהיה, כמו ש-fetch עושה. שנו את 800 ל-3000 כדי לראות את טקסט הטעינה נשאר זמן רב יותר.

עם API אמיתי, גוף האפקט נראה כך:

useEffect(() => {
    fetch('https://api.example.com/users/1')
        .then((res) => res.json())
        .then((data) => setUser(data));
}, []);

טעינה, שגיאה ונתונים

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

לחצו על User 3: ה-API המזויף נכשל, הודעת השגיאה מחליפה את השם, והקונסול מציג את השגיאה. בלוק ה-finally מנקה את loading בשני המסלולים, כך שבקשה שנכשלה אף פעם לא משאירה את הספינר דולק.

כש-fetch מדבר עם שרת אמיתי, תשובת 404 או 500 לא גורמת ל-promise להיכשל. בדקו את res.ok וזרקו שגיאה בעצמכם:

const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();

async/await בתוך אפקט

הפונקציה שמעבירים ל-useEffect לא יכולה להיות async. React מצפה שהיא תחזיר כלום או פונקציית cleanup, ופונקציה async תמיד מחזירה promise.

// Wrong: the effect returns a promise
useEffect(async () => {
    const data = await fetchUser(id);
    setUser(data);
}, [id]);

// Right: define an async function inside and call it
useEffect(() => {
    async function load() {
        const data = await fetchUser(id);
        setUser(data);
    }
    load();
}, [id]);

race conditions

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

התיקון הוא דגל ignore. לכל הרצה של האפקט יש דגל משלה, וה-cleanup שלה קובע אותו ל-true. מכיוון ש-React מריצה את ה-cleanup לפני ההרצה הבאה, תשובה מהרצה מיושנת רואה ignore === true ונזרקת. הבלוק הזה מרנדר את אותו פרופיל פעמיים, בלי הדגל ועם הדגל:

לחצו על הכפתור וחכו. התשובה המהירה של user 2 מגיעה ראשונה, ולכן שני הפרופילים רושמים ושומרים את Grace. בערך שנייה וחצי אחרי הלחיצה, התשובה האיטית של user 1 מגיעה: הפרופיל בלי הדגל עובר ל-Ada למרות ש-user 2 נבחר, ואילו זה עם הדגל רושם dropped stale Ada ושומר את Grace.

אותו באג מופיע בתיבות חיפוש, בטאבים ובכל רשימה שמסוננת בשרת. דוגמת הטעינה שלמעלה משמיטה את הדגל כדי להישאר קצרה, אבל לכל טעינה באפקט צריך להיות הדגל, או ה-abort שמתחת.

ביטול עם AbortController

דגל ה-ignore זורק תשובה מיושנת, אבל הבקשה עדיין רצה עד הסוף. עם fetch אמיתי אפשר לבטל אותה. צרו AbortController באפקט, העבירו את ה-signal שלו ל-fetch, וקראו ל-abort() ב-cleanup:

useEffect(() => {
    const controller = new AbortController();

    async function load() {
        try {
            const res = await fetch(`/api/users/${id}`, { signal: controller.signal });
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            setUser(await res.json());
        } catch (err) {
            if (err.name === 'AbortError') return; // cancelled on purpose
            setError(err);
        }
    }

    load();
    return () => controller.abort();
}, [id]);

fetch שבוטל נכשל עם AbortError, ולכן בלוק ה-catch מדלג עליו במקום להציג אותו ככישלון. abort מכסה גם unmount: כשהקומפוננטה מוסרת, ה-cleanup שלה מבטל את הבקשה.

העברה להוק מותאם אישית

טעינה, שגיאה וההגנה מפני race זהות בכל קומפוננטה שטוענת נתונים. שימו אותן ב-הוק מותאם אישית וכל קומפוננטה מבקשת נתונים בשורה אחת.

fetchPosts מוגדרת מחוץ לקומפוננטה, כך שהזהות שלה אף פעם לא משתנה והיא בטוחה במערך התלויות. אילו הוגדרה בתוך App, היא הייתה פונקציה חדשה בכל רינדור, האפקט היה רץ אחרי כל רינדור, ומכיוון שהאפקט קובע state, הוא לעולם לא היה מפסיק לטעון.

שנו את 500 ל-2000 ולחצו על הכפתור פעמיים ברצף: הקונסול מציג שתי בקשות, ורק הפוסטים של הנושא שבו סיימתם מופיעים.

טעינה ב-event handler

אפקט מיועד לנתונים שהקומפוננטה צריכה כי היא על המסך: דף פרופיל טוען את הפרופיל שלו. כשבקשה קורית כי המשתמש עשה משהו, כמו לחיצה על Search או Save, בצעו אותה ב-event handler. שם יודעים בדיוק מה הפעיל אותה, ושום דבר לא רץ מחדש כש-state לא קשור משתנה.

async function handleSubmit(e) {
    e.preventDefault();
    setStatus('saving');
    const res = await fetch('/api/notes', { method: 'POST', body: JSON.stringify({ text }) });
    setStatus(res.ok ? 'saved' : 'error');
}

טעויות נפוצות

תלויות חסרות. אפקט שקורא את id אבל יש לו [] טוען את המשתמש הראשון לנצח. רשמו כל ערך שהבקשה משתמשת בו.

טעינה בגוף הקומפוננטה. fetch מחוץ לאפקט רץ בכל רינדור, ואם הוא קובע state, הוא מתחיל לולאה.

לסמוך על כך ש-fetch ייכשל בשגיאות. הוא נכשל רק כשהרשת נופלת. בדקו את res.ok.

לשכוח לאפס את הטעינה. כשה-id משתנה, קבעו את loading בחזרה ל-true, אחרת הנתונים הישנים נשארים על המסך בלי שום סימן שנתונים חדשים בדרך.

מתי להשתמש בספרייה או ב-framework

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

import { useQuery } from '@tanstack/react-query';

function Profile({ id }) {
    const { data, error, isPending } = useQuery({
        queryKey: ['user', id],
        queryFn: () => fetch(`/api/users/${id}`).then((res) => res.json()),
    });

    if (isPending) return <p>Loading...</p>;
    if (error) return <p>{error.message}</p>;
    return <p>{data.name}</p>;
}

TanStack Query ו-SWR הן הבחירות הנפוצות לנתונים בצד הלקוח. frameworks הולכים רחוק יותר וטוענים נתונים בשרת לפני שהדף מגיע לדפדפן: Next.js עם server components, React Router עם loaders. זה חוסך את הבהוב הטעינה ואת שרשרת הבקשות שמתחילה כשהנתונים של הורה חייבים להגיע לפני שילד יכול בכלל להתחיל לטעון.

React 19: use() עם Suspense

React 19 מוסיפה את use, שקורא promise בזמן הרינדור. הקומפוננטה עושה suspend עד שה-promise מסתיים, וגבול ה-<Suspense> הקרוב מציג בינתיים fallback, כך שלקומפוננטה עצמה אין מצב טעינה:

import { use, Suspense } from 'react';

function Profile({ userPromise }) {
    const user = use(userPromise);
    return <p>{user.name}</p>;
}

<Suspense fallback={<p>Loading...</p>}>
    <Profile userPromise={userPromise} />
</Suspense>

ה-promise חייב להיווצר מחוץ לקומפוננטה (על ידי framework, cache או הורה), לא בתוך הרינדור שקורא אותו, אחרת כל רינדור מתחיל בקשה חדשה. הדף על ההוק use מכסה את זה עם דוגמאות שרצות.

שאלות נפוצות

איך טוענים נתונים כשקומפוננטת React נטענת?

מתחילים את הבקשה ב-useEffect עם הערכים שהיא תלויה בהם במערך התלויות ([] אם אין), ושומרים את התוצאה ב-state עם useState. מרנדרים הודעת טעינה עד שהנתונים מגיעים.

למה ה-callback של useEffect לא יכול להיות async?

פונקציה async תמיד מחזירה promise, ו-React מצפה שהאפקט יחזיר כלום או פונקציית cleanup. כתבו פונקציה async בתוך האפקט וקראו לה מיד.

מה זה race condition בטעינת נתונים ב-React?

כשהקלט משתנה מהר, שתי בקשות רצות במקביל והישנה יכולה לענות אחרונה ולדרוס את הנתונים החדשים. קבעו דגל ignore ב-cleanup של האפקט ודלגו על setState כשהוא דולק, או בטלו את הבקשה עם AbortController.

להשתמש ב-useEffect או בספרייה לטעינת נתונים?

useEffect עובד באפליקציות קטנות ושווה להבין אותו. ל-caching, מניעת כפילויות, ניסיונות חוזרים וטעינה מחדש, ספרייה כמו TanStack Query או SWR, או טעינת הנתונים המובנית ב-framework כמו Next.js, חוסכות לכם לכתוב את הלוגיקה הזו בעצמכם.

איך מציגים ספינר טעינה בזמן שהנתונים נטענים?

מחזיקים ב-state בוליאני loading (או מחרוזת status), קובעים אותו לפני שהבקשה מתחילה ומנקים אותו כשהיא מסתיימת, גם במסלול ההצלחה וגם במסלול השגיאה. מרנדרים את הספינר כל עוד הוא true.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל