מה עושה useEffectEvent בריאקט ומתי עלינו להשתמש בו
ריאקט 19.2 הוסיפו פונקציה חדשה כדי לטפל במקרה קלאסי ודי מעצבן של קוד אפקט לא מעודכן. בפוסט זה נראה בקצרה את הבאג, נראה איך useEffectEvent עוזר להתמודד אתו ונשאל אם בכלל זאת בעיה אמיתית.
1. קוד סטארטר - כמה זמן אני מחובר
הקוד הבא מציג שורת טקסט על המסך שמראה כמה זמן משתמש מסוים מחובר. כל שניה מעלים את time ב-1 ומאפסים כששם המשתמש מתעדכן. עד פה הכל ריאקטי ויפה:
import { useState, useEffect, useEffectEvent } from 'react'
import './App.css'
function EffectEventDemo({name}: {name: string}) {
const [time, setTime] = useState(0);
useEffect(() => {
const ivl = setInterval(() => {
setTime(c => c + 1)
}, 1000);
return () => clearInterval(ivl);
}, [name]);
return <p>User {name} is online for {time} seconds</p>
}
function App() {
return (
<EffectEventDemo name='demo' />
)
}
export default App
2. עכשיו נדמיין עדכון קוד צד שרת
הסיפור מתחיל להתלכלך אם נדמיין שבמקום רק להעלות את הטיימר אנחנו רוצים גם לעדכן קוד צד שרת, למשל בשביל לשמור סטטיסטיקה של כמה זמן כל משתמש נמצא באתר שלנו. בשביל זה אולי נרצה לכתוב את הקוד הבא:
function EffectEventDemo({name}: {name: string}) {
const [time, setTime] = useState(0);
useEffect(() => {
const ivl = setInterval(() => {
fetch(`/ping`, {
method: 'POST',
contentType: 'application/json',
body: JSON.stringify({
name,
time,
}),
});
setTime(c => c + 1)
}, 1000);
return () => clearInterval(ivl);
}, [name]);
return <p>User {name} is online for {time} seconds</p>
}
ואם כתבתם ריאקט יותר מכמה ימים ובטח אם עשיתם קורס ריאקט שפה באתר בטוח אתם רואים שזה לא עובד. מאחר והאפקט לא תלוי ב time פונקציית setInterval נקראה רק פעם אחת כשהקומפוננטה נכנסה למסך. הפונקציה הפנימית אותה העברנו ל setInterval ואחראית על עדכון השעון ושליחת ההודעה לשרת נוצרה פעם אחת בעת יצירת הקומפוננטה והמשתנה time שבתוך הפונקציה לנצח ישמור על ערכו. התוצאה היא שליחת הודעות לשרת בהן name הוא אכן שם המשתמש אבל time הוא תמיד 0.
3. פתרון עם useEffectEvent
תשמחו לשמוע שהחל מגרסה 19.2 של ריאקט תוכלו לפתור את זה בצורה מאוד פשוטה עם פונקציה חדשה בשם useEffectEvent. פונקציה זו מקבלת "פונקצייה פנימית" שאנחנו אמורים לכתוב בתוך effect ומעדכן אותה כך שהיא לא תיתקע על ערכים של משתנים ותמיד תקרא את הערך העדכני של משתני הסטייט או הפרופס בהם היא תלויה. בקיצור הקוד הזה עובד ממש אחלה ושולח לשרת כל שניה הודעה עם שם המשתמש ומספר השניות האמיתי שהמשתמש מחובר:
function EffectEventDemo({name}: {name: string}) {
const [time, setTime] = useState(0);
const notifyServer = useEffectEvent(() => {
fetch(`/ping`, {
method: 'POST',
contentType: 'application/json',
body: JSON.stringify({
name,
time,
}),
});
});
useEffect(() => {
const ivl = setInterval(() => {
notifyServer();
setTime(c => c + 1)
}, 1000);
return () => clearInterval(ivl);
}, [name]);
return <p>User {name} is online for {time} seconds</p>
}
דווקא לאור הפתרון הפשוט הזה מעניין להיזכר מה עשינו לפני useEffectEvent ומה מלמד אותנו הפתרון על האופי של ריאקט. גם לפני useEffectEvent פונקציית עדכון השרת היתה צריכה דרך לגשת ל time, אבל עדיין לא יכלה להיות מוגדרת "יחד" עם השינויים בו.
פתרון אחד היה לבנות אפקט נוסף שיהיה תלוי ב time במקום לעדכן את שניהם באותו Callback. כל פעם ש time משתנה מופעל אפקט עדכון השרת. זה היה נראה כך:
function EffectEventDemo({name}: {name: string}) {
const [time, setTime] = useState(0);
useEffect(() => {
fetch(`/ping`, {
method: 'POST',
contentType: 'application/json',
body: JSON.stringify({
name,
time,
}),
});
}, [time, name]);
useEffect(() => {
const ivl = setInterval(() => {
notifyServer();
setTime(c => c + 1)
}, 1000);
return () => clearInterval(ivl);
}, [name]);
return <p>User {name} is online for {time} seconds</p>
}
זה עובד אבל החיסרון כאן הוא שאין לנו שליטה על תדירות עדכון השרת. אנחנו מושפעים מהשינויים ב name וב time.
אפשרות שניה היא לשמור את זמן החיבור ואולי את name מחוץ לקומפוננטות ולכתוב את כל קוד עדכון השרת במקום אחר. זאת הגישה שלוקחת react-query. הקוד בגדול היה נראה כך (נכתב על ידי ChatGPT):
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { sendOnlineTime } from "./api";
type Props = {
name: string;
};
export function UserOnlineTimer({ name }: Props) {
const [time, setTime] = useState(0);
// Local timer (increments every second)
useEffect(() => {
const interval = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
// React Query handles the polling request
useQuery({
queryKey: ["user-online", name, time],
queryFn: () => sendOnlineTime(name, time),
enabled: time > 0,
refetchInterval: 1000,
refetchIntervalInBackground: true,
});
return <p>User {name} is online for {time} seconds</p>;
}
הפעם זה מבנה טוב ואפילו את הקריאה ל useQuery אפשר להוציא ל Custom Hook מה שעוד יותר יקל עלינו בפיתוח קומפוננטות "מדווחות" בהמשך.
אז האם useEffectEvent שווה את הכסף? קשה להגיד. מצד אחד הוא פותר בעיה אמיתית שהיתה עם Hooks ונותן פתח מילוט אמיתי למי שנתקע עם אפקט שצריך לקרוא מידע מסטייט. מצד שני בגלל איך שאנחנו בונים אפליקציות ריאקט היום זאת לא בעיה נפוצה כמו שהיינו מדמיינים שתהיה.
מה דעתכם? מתי נתקלתם ב Stale Data באפקט? האם useEffectEvent תעזור לכם?