הבעיה של ריאקט ו State
ריאקט הגיעה עם תפיסה חדשנית לפיתוח ווב כתוצאה מבעיה במערכת ווב גדולה (המערכת של פייסבוק). האתגר שלהם היה שאירועים שונים יכולים להשפיע על "נראות" של אלמנטים על העמוד, ולכן היה קשה להבין את כל האופנים השונים בהם עמוד יכול להופיע. יותר מזה, לפעמים כמה מקומות בעמוד היו צריכים להציג שינוי בצורה מתואמת, למשל כשהגיעה הודעה חדשה היה צריך להציג אותה גם בפאנל ההודעות וגם לעדכן את המספר בפאנל העדכונים. כשמשתמש קורא הודעה במסך ההודעות יש למחוק את העדכון על הודעה חדשה מפאנל העדכונים. אבל בגלל שלא היה מקום אחד ששומר את המידע, כל אזור במסך שמר את המידע של עצמו, מידע נשמר בצורה כפולה וכך נוצרו באגים של סינכרון.
האתגר של פייסבוק אמיתי ועדיין משפיע על פיתוח מערכות רבות מבוססות ריאקט. כשמידע זהה נשמר בצורה כפולה בכמה מקומות שונים במערכת עלינו לסנכרן בין המקומות. קוד אירוע שפועל על המידע צריך "לזכור" לעדכן את המידע בכל המקומות הרלוונטים ופה קל לפספס ולהגיע לבאגים.
הפיתרון הריאקטי הוא פשוט וגאוני: נשמור את המידע רק במקום אחד. כל דבר שיכול להשתנות בעמוד יכול להשתנות בגלל "מידע" מסוים שמשפיע עליו, ולכל מידע יש רק בית אחד. מידע הוא סטייט וסטייט שייך לקומפוננטה. קומפוננטות אחרות מקבלות את המידע הזה דרך Props אבל לעולם לא ישמרו עותק אצלן. בצורה כזאת כשמידע מתעדכן העדכון משפיע על כל המקומות במסך שתלויים בו.
כל זה עובד טוב בספר הלימוד אבל בחיים האמיתיים יש בעיה שחוזרת כמעט בכל יישום ריאקט שנכתוב: סטייט נוטה לעלות למעלה לאזורים המשותפים יותר מהר ממה שהיינו רוצים.
ניקח דוגמה פשוטה של מונה לחיצות:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div className="counter" >
<p>Count = {count}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
)
}
ועכשיו נרצה לשים מספר מונים כאלה על המסך ומעליהם לכתוב מה הערך המקסימלי של מונה כלשהו מקבוצת המונים. נו, זה לא נשמע מסובך, אני מתחיל עם App:
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import Counter from './Counter'
import CounterGroup from './CounterGroup'
function App() {
return (
<CounterStatistics />
<Counter />
<Counter />
<Counter />
)
}
export default App
ואז מנסה לכתוב את CounterStatistics כדי להציג את המקסימום והמינימום - ופה פוגש את ריאקט ותיאוריית הסטייט שלו. כי בשביל ש CounterStatistics יוכל להשתמש במידע, כלומר בערכים שמופיעים במוני הלחיצות, אני צריך שהמידע הזה יעלה למעלה ויישמר בקומפוננטה משותפת, לא בתור משתנה בודד בתוך Counter אלא בתור מערך של מספרים. זאת הדרך הריאקטית קדימה:
function App() {
const [counterValues, inc] = useCounters(3);
return (
<>
<CountersStats values={counterValues} />
<Counter value={counterValues[0]} inc={inc(0)} />
<Counter value={counterValues[1]} inc={inc(1)} />
<Counter value={counterValues[2]} inc={inc(2)} />
</>
)
}
ועכשיו הייתי צריך לשנות את הקומפוננטה של Counter שלא תכיל סטייט פנימי רק בשביל לשים אותה בתוך קומפוננטה שתציג סטטיסטיקות.
וכן אני מבין מה קורה פה ולמה היינו צריכים ללכת את הדרך הזאת, אבל באמת יש פה שאלה אם זה שווה את המאמץ, ואולי דווקא במקרים כאלה כן שווה לשכפל את הסטייט ולשמור סטייט נפרד נוסף לסטטיסטיקות, כלומר:
function App() {
const [min, setMin] = useState(0);
const [max, setMax] = useState(0);
function update(value) {
setMin(Math.min(min, value));
setMax(Math.max(max, value));
}
return (
<>
<CountersStats />
<Counter onChange={update} />
<Counter onChange={update} />
<Counter onChange={update} />
</>
)
}
וכן גם פה צריך לשנות קצת את Counter ולהוסיף את onChange בתור מאפיין אופציונאלי אבל זה שינוי פחות משמעותי מלהעביר את הסטייט החוצה.
מה דעתכם?