הפרדת אירועים מ-אפקטים

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

You will learn

  • כיצד לבחור בין מטפל באירועים לבין אפקט
  • מדוע אפקטים מגיבים, ומטפלי אירועים אינם
  • מה לעשות כאשר אתה רוצה שחלק מהקוד של האפקט שלך לא יהיה תגובתי
  • מהם אירועי אפקט וכיצד לחלץ אותם מהאפקטים שלך
  • הכי לקרוא את הprops וstate העדכניים מאפקטים באמצעות אפקט אירועים

בחירה בין מטפלי אירועים לאפקטים

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

תאר לעצמך שאתה מיישם רכיב של חדר צ’אט. הדרישות שלך נראות כך:

  1. הרכיב שלך אמור להתחבר אוטומטית לחדר הצ’אט הנבחר.
  2. כאשר אתה לוחץ על כפתור “שלח”, זה אמור לשלוח הודעה לצ’אט.

נניח את הקודם אבל אתה לא בטוח איפה לשים אותו. האם להשתמש ברופאי אירועים או אפקטים? בכל פעם שאתה צריך לענות על שאלה זו, שקוללמה הקוד צריך לפעול.](/learn/synchronizing-with-effects#what-are-effects-and-how-the-the-different-from-events)

מטפלי אירועים פועלים בתגובה לאינטראקציות

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

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>;
</>
);
}

עם מטפל באירועים, אתה יכול להיות בטוח ש’סendMessage(message)’ יפעל רק אם המשתמש ילחץ על הכפתור.

אפקטים פועלים בכל פעם שיש צורך בסנכרון

זכור כי אתה גם צריך לשמור את הרכיב מחובר לחדר הצ’אט. לאן הולך הקוד הזה?

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

function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

עם קוד זה, אתה יכול להיות בטוח שתמיד יש חיבור פעיל לשרת הצ’אט שנבחר כעת, ללא קשר לאינטראקציות הספציפיות שמבצע משתמש. בין אם משתמש רק פתח את האפליקציה שלך, בחר חדר אחר או ניווט למסך אחר ובחזרה, האפקט שלך מבטיח שהרכיב יישאר מסונכרן עם החדר שנבחר עכשיו, ו[יתחבר מחדש בכל פעם שנדרש.](/learn/lifecycle-of-reactive-effects#why-synchronization-cany-reactive-to-han

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  function handleSendClick() {
    sendMessage(message);
  }

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

ערכים ריאקטיביים והיגיון תגובתי

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

יש דרך יותר מדויקת לחשוב על זה.

props, מצב ומשתנים המוצהרים בתוך גוף הרכיב שלך נקראים ערכים תגובתיים. בדוגמה זו, serverUrl אינו ערך תגובתי, אבל roomId ו-message כן. הם משתתפים בעיבוד:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

// ...
}

ערכים תגובתיים כמו אלה יכולים להשתנות עקב עיבוד מחדש. לדוגמה, יכול להשתמש לערוך את ההודעה’ או לבחור ‘roomId’ אחר בתפריט נפתח. מטפלי אירועים ואפקטים מגיבים לשינויים בצורה שונה:

  • ההיגיון בתוך מטפלי אירועים הוא לא תגובתי. זה לא יפעל שוב אלא אם המשתמש יבצע שוב את אותה אינטראקציה (למשל, קליק). מטפלי אירועים יכולים לקרוא ערכים תגובתיים מבלי “להגיב” לשינויים שלהם.
  • ההיגיון בתוך אפקטים הוא reactive. אם האפקט שלך קורא ערך תגובתי, עליך לציין אותו כתלות. לאחר מכן, אם עיבוד גורם לערך זה החדש החידוש, React יפעיל את החידוש החדש שלך.

בואו נחזור על הדוגמה הקודמת כדי להמחיש את ההבדל הזה.

ההיגיון בתוך רופא אירועים אינו תגובתי

תסתכל על שורת הקוד הזו. האם ההיגיון הזה צריך להיות תגובתי או לא?

// ...
sendMessage(message);
// ...

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

function handleSendClick() {
sendMessage(message);
}

מרפאה אירועים אירועים ריאקטיביים, כך ששלחו הודעה(הודעה)’ יפעל רק כאשר המשתמש ילחץ על כפתור השליחה.

ההיגיון בתוך אפקטים הוא תגובתי

כעת נחזור לשורות אלו:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...

מנקודת המבט של המשתמש, שינוי ב-roomId משמעו שהם רוצים להתחבר לחדר אחר. במילים אחרות, ההיגיון לחיבור לחדר צריך להיות תגובתי. אתה רוצה ששורות הקוד האלה “יעמדה בקצב” עם הערך הריאקטיבי, ויפעלו שוב אם הערך הזה שונה. בשביל זה שייך לאפקט:

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);

האפקטים הם תגובתיים, אז createConnection(serverUrl, roomId) ו-connection.connect() יפעלו עבור כל ערך מובחן של roomId. האפקט שלך שומר על חיבור הצ’אט מסונכרן לחדר שנבחר עכשיו.

חילוץ לוגיקה לא-ריאקטיבית מתוך אפקטים

דברים נעשים מסובכים יותר כאשר אתה רוצה לערבב לוגיקה תגובתית עם לוגיקה לא תגובתית.

לדוגמה, דמיינו רוצים להציג את המשתמש כאשר אתה מתחבר לצ’אט. אתה קורא את הנושא הנוכחי (כהה או בהיר) מprops כדי להציג את ההודעה בהתאמה נכונה:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...

עם זאת, theme הוא ערך תגובתי (הוא יכול להשתנות כמו עיבוד מחדש), ו[כל ערך תגובתי הנקרא על ידי אפקט חייב להיות מוכרז כתלות שלו.](/learn/lifecycle-of-reactive-effects#react-verifies-that-you-specificated-every-reactive-value-the-reactive-me-the-dependence:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ All dependencies declared
// ...

שחק עם הדוגמה הזו וראה אם ​​אתה יכול לזהות את הבעיה בחוויית המשתמש הזו:

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

כאשר ה-‘roomId’ משתנה, הצ’אט מתחבר מחדש כפי שהיית מצפה. אבל תלמת ש’נושא’ הוא גם תות, הצ’אט גם מתחבר מחדש בכל פעם שאתה מחליף בין הנושא הכהה והבהיר. זה לא נהדר!

במילים אחרות, אתה לא רוצה שהשורה הזו תהיה תגובתית, למרות שהיא בתוך אפקט (שהוא תגובתי):

// ...
showNotification('Connected!', theme);
// ...

אתה צריך דרך להפריד את ההיגיון הלא תגובתי הזה מהאפקט התגובתי סביבו.

הכרזה על אירוע אפקט

Under Construction

סעיף זה מתאר API ניסיוני שעדיין לא שוחרר בגרסה יציבה של React.

השתמש בהוק מיוחד בשם useEffectEvent כדי לחלץ את ההיגיון הלא תגובתי הזה מהאפקט שלך:

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...

כאן, onConnected נקרא Effect Event. זה חלק מהלוגיקת האפקט שלך, אבל הוא מתנהג הרבה יותר כמו מטפל באירועים. ההיגיון בתוכו אינו תגובתי, והוא תמיד “רואה” את הערכים העדכניים ביותר של הprops וstate שלך.

עכשיו אתה יכול לקרוא לאירוע ‘onConnected’ אפקט מתוך האפקט שלך:

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

זה פותר את הבעיה. שים לב שהיית צריך להסיר את ‘onConnected’ מרשימת התלות של האפקט שלך. אירועי אפקט הם ריאקטיביים ויש להשמיט אותם מהתלות.

ודא שההתנהגות החדשה פועלת כפי שהיית מצפה:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

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

קריאת הprops אחר וstate עם אפקט אירועים

Under Construction

סעיף זה מתאר API ניסיוני שעדיין לא שוחרר בגרסה יציבה של React.

אירועי אפקט מאפשרים לך לתקן דפוסים רבים שבהם אתה עלול להתפתות לדכא את קו התלות.

לדוגמה, נניח שיש לך אפקט לרישום הביקורים בדף:

function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}

מאוחר יותר, אתה מוסיף מספר מסלולים לאתר. רכיב ה’דף’ שלך מקבל props ‘כתובת אתר’ עם הנתיב הנוכחי. אתה רוצה להעביר את ה-URL כחלק משיחת ה-‘logVisit’ שלך, אבל קו התלות מתלונן:

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url'
// ...
}

תחשוב מה אתה רוצה שהקוד יעשה. אתה רוצה לרשום ביקור עבור כתובות אתרים שונות זו שכל כתובת אתר מייצגת דף אחר. במילים אחרות, קריאת logVisit זו צריכה להיות תגובתית לכתוב לכתובת ה-URL. זו הסיבה, שבמקרה זה, הגיוני לעקוב אחר קו התלות ולהוסיף ‘כתובת אתר’ בתור תלות:

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}

כעת נניח שברצונך לכלול את מספר הפריטים בעגלת הקניות יחד עם כל ביקור בדף:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}

השתמשת ב-‘numberOfItems’ בתוך האפקט, אז ה-Linter מבקש ממך להוסיף אותו כתלות. עם זאת, אתה לא רוצה שהקריאה ‘logVisit’ תהיה תגובתית ביחס ל’numberOfItems’. אם משתמש מכניס משהו לעגלת הקניות, ו-‘numberOfItems’ ranking, זה לא אומר שהמשתמש ביקר שוב בעמוד. במילים אחרות, ביקור בדף הוא, במידה מסוימת, “אירוע”. זה קורה בדיוק בזמן.

פצל את הקוד לשני חלקים:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}

כאן, ‘onVisit’ הוא אירוע אפקט. הקוד שבתוכו אינו מגיב. אתה יכול להשתמש ב-‘numberOfItems’ (או בכל ערך תגובתי אחר!) אלא לדאוג שזה יגרום לקוד שמסביב להפעיל מחדש בשינויים.

מצד שני, האפקט נשאר עצמו תגובתי. הקוד בתוך האפקט משתמש בprops url, כך שהאפקט יפעל מחדש לאחר כל רינדור מחדש עם url אחר. זה, בתורו, יקרא את אירוע אפקט ‘onVisit’.

כתוצאה מכך, תתקשר ל-‘logVisit’ עבור כל שינוי ב-‘url’, ותמיד תקרא את ה-‘numberOfItems’ העדכני ביותר. עם זאת, אם ‘numberOfItems’ משתנה מעצמו, זה לא יגרום לאף אחד מהקודים להפעיל מחדש.

Note

אולי אתה תוהה אם אתה יכול לקרוא ל- onVisit() ללא ארגומנטים, ולקרוא את ה-URL שבתוכו:

const onVisit = useEffectEvent(() => {
logVisit(url, numberOfItems);
});

useEffect(() => {
onVisit();
}, [url]);

זה יעבוד, אבל עדיף להעביר את ה-URL הזה לאירוע אפקט במפורש. על ידי העברת שלך ‘כתובת אתר’ כטיעון לאירוע האפקט, אתה אומר שביקור בדף עם ‘כתובת אתר’ שונה שונה “אירוע” נפרד מנקודת המבט של המשתמש. ה-‘visitedUrl’ חלק מה”אירוע” שקרה:

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]);

זה שאירוע הקט שלך “מבקש” במפורש את ה-‘visitedUrl’, כעת אינך יכול לפתוח בטעות ‘url’ מהתלות של האפקט. אם תסיר את התלות של ‘כתובת האתר’ (מה ששלח לביקורים נפרדים בדפים להיספר כאחד), ה-linter יזהיר אותך על כך. אתה רוצה ש-‘onVisit’ יהיה תגובתי ביחס ל-‘url’, אז במקום לקרוא את ה-‘url’ בפנים (היכן שהוא לא יהיה תגובתי), אתה מעביר אותו מה אפקט שלך.

זה הופך להיות חשוב במיוחד אם יש היגיון אסינכרוני כלשהו בתוך האפקט:

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // Delay logging visits
}, [url]);

כאן, url בתוך onVisit תואם לכתובת האתר האחרונה (שיכולה כבר הייתה להשתנות), אבל visitedUrl תואמת את ה-url שבמקור גרמה לאפקט הזה (ולקריאת onVisit זו) לפעול.

Deep Dive

האם זה בסדר לדכא את קו התלות במקום זאת?

בבסיסי הקוד הקיימים, לפעמים אתה עשוי לראות את כלל המוך מודחק כך:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 Avoid suppressing the linter like this:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}

לאחר ש-‘useEffectEvent’ גורם לחלק יציב ב-React, אנו ממליצים לעולם לא לדכא את ה-linter.

החיסרון הראשון של דיכוי הכלל הוא ש-React כבר לא מזהיר אותך כשהאפקט שלך צריך “להגיב” לתלות תגובתית חדשה שהכנסת לקוד שלך. בדוגמה הקודמת, הוספת ‘url’ לתלויות כי React הזכיר לך לעשות זאת. לא תקבל עוד תזכורות כאלה עבור כל עריכה עתידית של אפקט זה אם תשבית את ה-linter. זה מוביל לבאגים.

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

אתה יכול לראות למה?

import { useState, useEffect } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  function handleMove(e) {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  }

  useEffect(() => {
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        The dot is allowed to move
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

הבעיה עם הקוד הזה היא בדיכוי. אם תסיר את הדיכוי, תראה שהאפקט הזה צריך להיות תלוי בפעולה ‘handleMove’. זה הגיוני: handleMove מוצהר בתוך גוף הרכיב, מה שהופך אותו לערך תגובתי. כל ערך תגובתי חייב להיות מוגדר כתלות, אחרת הוא להתייאש עם הזמן!

מחבר הקוד המקורי “שיקר” ל-React באומרו שהאפקט אינו תלוי ([]) בערכים תגובתים כלשהם. זה מה ש-React לא סינכרן מחדש את האפקט לאחר ש-‘canMove’ השתנה (ו-‘handleMove’ איתו). איך ש-React לא סינכרן מחדש את האפקט, ה-‘handleMove’ המצורף כמאזין הוא הפונקציה ‘handleMove’ עיבוד עיבוד הראשוני. על העיבוד הראשוני, canMove היה נכון, וזוהי שhandleMove מהרינדור הראשוני יראה לנצח את הערך הזה.

אם לעולם לא תדחיק את ה-linter, לעולם לא תראה בעיות עם ערכים מיושנים.

עם useEffectEvent, אין צורך “לשקר” ל-linter, והקוד עובד כפי שהיית מצפה:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  const onMove = useEffectEvent(e => {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  });

  useEffect(() => {
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        The dot is allowed to move
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

זה לא ש-‘useEffectEvent’ הוא תמיד אומר הפתרון הנכון. עליך להחיל אותו רק על שורות הקוד שאינך רוצה שיהיו תגובתיים. בארגז החול שלמעלה, לא רצית שהקוד של האפקט יהיה תגובתי לגבי canMove. זה היה הגיוני לחלץ אירוע אפקט.

קרא את הסרת תלות אפקט לקבלת חלופות נכונות אחרות לדיכוי ה-linter.

מגבלות של אירוע אפקט

Under Construction

סעיף זה מתאר API ניסיוני שעדיין לא שוחרר בגרסה יציבה של React.

אירועי אפקט מוגבלים מאוד איך אתה יכול להשתמש בהם:

  • התקשר אליהם רק מתוך אפקטים.
  • לעולם אל תעביר אותם לרכיבים אחרים או לווים.

לדוגמה, אל תצהיר ותעביר אירוע אפקט בצורה הבאה:

function Timer() {
const [count, setCount] = useState(0);

const onTick = useEffectEvent(() => {
setCount(count + 1);
});

useTimer(onTick, 1000); // 🔴 Avoid: Passing Effect Events

return <h1>{count}</h1>
}

function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Need to specify "callback" in dependencies
}

במקום זאת, תמיד הכריז על אפקט אירועים ישירות ליד האפקטים המשתמשים בהם:

function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}

function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});

useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Good: Only called locally inside an Effect
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // No need to specify "onTick" (an Effect Event) as a dependency
}

אירועי אפקט הם “חלקים” לא תגובתיים של קוד האפקט שלך. הם צריכים להיות ליד האפקט המשתמש בהם.

Recap

  • מטפלי אירועים פועלים בתגובה לאינטראקציות ספציפיות.
  • אפקטים פועלים בכל פעם שיש צורך בסנכרון.
  • ההיגיון בתוך מטפלי אירועים אינו תגובתי.
  • ההיגיון בתוך אפקטים הוא תגובתי.
  • אתה יכול להעביר לוגיקה לא תגובתית מאפקטים לאירועי אפקט.
  • התקשר רק ל-Effect Events מתוך אפקטים.
  • אל תעביר אירועי אפקט לרכיבים אחרים או הHooks.

Challenge 1 of 4:
תקן משתנה שלא מתעדכן

רכיב ‘טימר’ זה שומר על ranking מצב ‘ספירה’ אשר גדל כל שנייה. הערך שבאמצעותו הוא גדל מאוחסן בהstate ‘increment’. אתה יכול לשלוט בשינוי ‘increment’ עם לחצני הפלוס והמינוס.

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

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + increment);
    }, 1000);
    return () => {
      clearInterval(id);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}