זהו נושא דיון מלווה לערך המקורי שב־https://www.tocode.co.il/bundles/react/lessons/performance-lab
היי, האם הגעתי למינימום רנדרים?
לא הייתי בטוח אם יש דרך פשוטה לגרום לכך שלא יהיה רנדר לMyButton כשמשנים את הdelta.
זה יכול להיות אפשרי רק אם שומרים את delta בref במקום בstate. לא הצלחתי כל כך לממש את זה בגלל שצריך שיהיה רנדר כל פעם שמכניסים ערך בinput כדי שיהיה אפשר לראות את השינוי כל פעם בinput.
באופן כללי זה נראה לי גם די הגיוני שאם הdelta השתנתה אז אני רוצה שיהיה רנדר גם לכפתור בגלל שהפונקציה inc מתעדכנת בהתאם לערך החדש שהיא צריכה להוסיף בעת לחיצה על הכפתור.
נראה מעולה
MyButton חייב לעבור render כשמשנים את ה delta - כי זה אומר שמשהו בהתנהגות הכפתור השתנה. כאן בדיוק אתה רואה דוגמא ל render חיוני
שלום,
מצרפת את הפתרון, אשמח להערות, תודה!
import React, { useRef } from "react";
import ReactDOM, { render } from "react-dom";
import { useState, useMemo, useCallback } from "react";
import "../styles/style.scss";
const Header = React.memo(function Header(props) {
console.count("Header.render");
return <h1>My Counter Demo</h1>;
});
const DisplayValue = React.memo(function DisplayValue(props) {
console.count("DisplayValue.render");
const { val } = props;
return <p>Value: {val}</p>;
});
const DisplayMod5 = React.memo(function DisplayMod5(props) {
console.count("DisplayMod5.render");
const { val } = props;
const text =
val % 5 === 0 ? "Value is divisible by 5" : "Value does not divide by 5";
return <p>{text}</p>;
},(prevProps , nextProps)=>{
return (prevProps.val % 5 === 0) === (nextProps.val % 5 === 0);
});
const MyButton = React.memo(function MyButton(props) {
console.count("MyButton.render");
return <button onClick={props.onClick}>Click Me</button>;
});
function Counter() {
console.count("Counter.render");
const [nonce , setNonce] = useState(0);
const countRef = useRef(0); const count = countRef.current;
const deltaRef = useRef(1); const delta = deltaRef.current;
const inc = useCallback(function inc() {
countRef.current += delta;
render();
},[countRef]);
function render() {
setNonce(v=>v+1);
}
return (
<>
<Header />
<label>
Increase by:
<input type="number"
value={delta}
onChange={e => { deltaRef.current = Number(e.target.value); render();}} />
</label>
<DisplayValue val={count} />
<DisplayMod5 val={count} />
<MyButton onClick={inc} />
</>
);
}
ReactDOM.render(<Counter />, document.querySelector('main'));
למה בחרת להשתמש ב ref עבור הערכים של count ו delta ?
שתי שאלות
למה מביאים את useMemo אם הכתיבה היא React.memo
מבחינת משאבים - שימוש ב memo ו callback לא עולה במשאבים מהצד השני?
כלומר חוסכים ברנדר אבל מעמיסים על הזכרון?
הי,
אלה שני דברים שונים: ב React.Memo אנחנו משתמשים בעת הגדרת הקומפוננטה, אבל ב useMemo נשתמש כשיש לנו קומפוננטה שמייצרת קומפוננטה אחרת ואנחנו רוצים במקרה ספציפי לגרום לקומפוננטה הפנימית להיות Memoized
אני מדבר על ההבדל ביניהם יותר לעומק בוידאו כאן (יש גם טקסט עם דוגמאות):
https://www.tocode.co.il/past_workshops/86
לגבי נושא המשאבים - כן התוכנית תיקח יותר זיכרון אבל זה לא משהו שגורם לבעיות ביצועים. רנדרים מיותרים זה כן משהו שלריאקט מאוד קשה איתו ויכול להשפיע על חווית המשתמש (ושוב רק במקרים קיצוניים, רוב הזמן אף אחד לא ישים לב אם יש קצת רנדרים מיותרים)
כדי שהכפתור ירונדר רק פעם אחת.
(אחר כך שמתי לב לטעות קטנה שהיתה לי בתוך הפונקציה inc ותיקנתי אצלי:
countRef.current += deltaRef.current;
)
מבחינת הגיון של ריאקט - זה לא הגיוני להשתמש שם ב ref. השדות האלה הם באופן מובהק State של הקומפוננטה (זאת המשמעות של סטייט)
אנחנו משתמשים ב ref או בשביל לשמור מידע סטטי (למשל חיבור לאיזשהו API חיצוני או מבנה נתונים חיצוני), או בשביל לשמור קישור לאלמנט ב DOM.
נסי לארגן מחדש את הקוד אבל להשאיר את count ו delta בתור State
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { useState, useMemo, useCallback, useRef } from "react";
import "./styles.css";
function Header(props) {
console.count("Header.render");
return <h1>My Counter Demo</h1>;
}
const DisplayValue = React.memo(function DisplayValue(props) {
console.count("DisplayValue.render");
const { val } = props;
return <p>Value: {val}</p>;
})
const DisplayMod5 = React.memo(function DisplayMod5(props) {
console.count("DisplayMod5.render");
const { val } = props;
const text =
val % 5 === 0 ? "Value is divisible by 5" : "Value does not divide by 5";
return <p>{text}</p>;
}, function isEqual(prevProps, nextProps) {return (prevProps.val % 5 === 0) === (nextProps.val % 5 === 0);})
const MyButton = React.memo(function MyButton(props) {
console.count("MyButton.render");
return <button onClick={props.onClick}>Click Me</button>;
})
function Counter() {
console.count("Counter.render");
const [count, setCount] = useState(0);
const [delta, setDelta] = useState(1);
const deltaRef = useRef(delta);
const inc = useCallback(() => {
setCount(val => val + deltaRef.current);
}, [deltaRef])
useEffect(() => {
deltaRef.current = delta;
}, [delta])
return (
<>
<Header />
<label>
Increase by:
<input
type="number"
value={delta}
onChange={e => setDelta(Number(e.target.value))}
/>
</label>
<DisplayValue val={count} />
<DisplayMod5 val={count} />
<MyButton onClick={inc} />
</>
);
הכנתי קוד שלא מבצע רנדור נוסף לכפתור ולא גורם לבעיות הנראות לעין. אשמח אם תוכל לעבור על זה ולראות האם זה תקין.
הי הכל נראה טוב רק לא הבנתי למה צריך את ה ref והאפקט:
useEffect(() => {
deltaRef.current = delta;
}, [delta])
אי אפשר היה לעבוד ישירות עם delta בתוך ה useCallback של inc ?
באופן כללי useEffect זה הפונקציה שצריך הכי להיזהר ממנה בריאקט. היא נועדה אך ורק בשביל לסנכרן בין משתנה בעולם של ריאקט ל״משהו״ מחוץ לריאקט (לדוגמה - לשמור את גודל החלון במשתנה סטייט בשביל לעשות משהו כשמשתמש משנה את גודל החלון).
אם הייתי משתמש ישירות עם delta בתוך ה useCallback של inc הייתי צריך להוסיף את delta לרשימת הdependencies ואז בכל פעם שdelta הייתה משתנה הפונקציה inc הייתה נוצרת מחדש.
הייתי צריך את ה ref והאפקט כדי שהפונקציה inc תישאר מעודכנת תמיד עם הערך העדכני של delta,
בכל פעם ש delta משתנה כך גם deltaRef.current אך ה reference של deltaRef נשאר ללא שינוי, לכן הפונקציה inc לא תיווצר מחדש.
“באופן כללי useEffect … נועדה אך ורק בשביל לסנכרן בין משתנה בעולם של ריאקט ל״משהו״ מחוץ לריאקט…”
אשמח לדעת איילו בעיות יכולות להופיע אם משתמשים בה כדי לגרום לעדכון ערכים פנימיים של הקומפוננטה כתגובה לשינוי כלשהו ב state.
תודה רבה
אשמח לעזרה…
כדי למנוע רינדורים מיותרים של : DisplayMod5, DisplayValue, Header
עטפתי את הקומפוננטות ב React.memo וזה באמת עזר
אני מנסה לחשוב איך אפשר למנוע רינדורים מיותרים של MyButton, וראיתי בתחילת הדיון שכתבת: “MyButton חייב לעבור render כשמשנים את ה delta - כי זה אומר שמשהו בהתנהגות הכפתור השתנה. כאן בדיוק אתה רואה דוגמא ל render חיוני”,
אבל לכאורה אין צורך שהוא יתרנדר ממש בכל פעם שמשנים את הערך של הinput?
הי זו באמת נקודה מעניינת. לכאורה ה delta מעניינת אותנו רק כשלוחצים על הכפתור. האם היה אפשר לוותר על ה render של הכפתור כשה delta משתנה, ופשוט בלחיצה ללכת ל input, לבדוק מה כתוב שם ולעשות setState לפי הערך הזה?
אני חושב שזאת גישה מעניינת וגם אפשרית, אבל היא הולכת קצת רחוק מדי. זה קוד לדוגמה שלא מרנדר את הכפתור כשמשנים את ה Delta:
const AddButton = memo(function AddButton({setCount}) {
console.count(`Count::render`);
const deltaRef = useRef();
const inc = useCallback(() => {
setCount(c => c + Number(deltaRef.current.value));
}, []);
return (
<div>
<label>
Delta:
<input type="number" ref={deltaRef} defaultValue={1} />
</label>
<button onClick={inc}>Increase</button>
</div>
)
});
function App() {
const [count, setCount] = useState(0)
return (
<>
<p>Count = {count}</p>
<AddButton setCount={setCount} />
</>
)
}
export default App
אבל אני לא מאמין שהייתי כותב קוד כזה בעולם האמיתי. יש יתרון להחזיק משתנים ב State כי זה פשוט יותר נוח לעבוד עם זה. לוותר על הסטייט רק בשביל לחסוך render נראה פשוט חבל. אולי אם יש סיטואציה של בעיית ביצועים ספציפית בקומפוננטה מסוימת זה יכול להיות טריק שעוזר.
תודה על ההסבר המפורט!
אז בעצם לפי מה שאני מבינה השיפור היחיד שאפשר לעשות בקוד הוא לעטוף את Header, DispalyValue, DisplayMod5 בReact.Memo??
כן זה ברמה המיידית וחוץ מזה ב DisplayMod5 צריך לוודא שמבוצע render רק אם באמת מה שיופיע על המסך משתנה, כלומר רק אם היה שינוי בשארית החלוקה בחמש.
ובהערה כללית לנושא זה - אנחנו רוצים שיבוצע render רק כאשר דברים צריכים להשתנות על המסך. כשקורה render בלי שמשהו השתנה על המסך זה בזבוז של זמן מעבד ובהיקפים גדולים זה יכול להאט את המערכת