ToCode
Відкрити в Telegram
1 419
Підписники
+124 години
Немає даних7 днів
Немає даних30 день
Архів дописів
1 419
# מדריך קוד: בואו נכתוב משחק זיכרון ב Redux ו React
לפני שנתיים כתבתי כאן משחק זיכרון עם ריאקט בעזרת הפונקציה useReducer. היום אני רוצה לנסות משחק דומה עם Redux כדי לראות מה התחדש ברידאקס בשנים האחרונות ובמיוחד כדי לשחק עם Redux Toolkit, ספריה שהופכת את הכתיבה ברידאקס למשחק הרבה יותר מהנה.
## הסוד של רידאקס טולקיט הוא בכלל Immer
האמת חייבת להיאמר, כתיבת קוד JavaScript שמתעסק עם Immutable Data זה לא תענוג. אם אני צריך לקחת אוביקט סטייט שמורכב מאוביקטים פנימיים ולשנות משהו באחד האוביקטים הפנימיים שלו אני מהר מאוד מוצא את עצמי כותב דברים כאלה:
function handwrittenReducer(state, action) {
return {
...state,
first: {
...state.first,
second: {
...state.first.second,
[action.someId]: {
...state.first.second[action.someId],
fourth: action.someValue,
},
},
},
}
}
ומפה הדרך לאבדון קצרה.
הטריק הכי מלהיב עם Redux Toolkit זה החיבור המובנה שלה עם Immer. אימר, למי שלא מכיר, זו ספריית JavaScript שמאפשרת לי לכתוב קוד שמשנה מידע והופכת אותו אוטומטית לקוד שמשכפל את המידע במקום לשנות אותו.
הפונקציה הכי חשובה של Redux Toolkit נקראת createSlice והיא בעצם יוצרת גם Reducer וגם Action Creators, משולבת עם Immer כך שהקוד שאני כותב בה "כאילו" משנה את האוביקטים.
בואו נצלול למשחק הזיכרון כדי לראות את הסיפור הזה בפעולה. כל הקוד שאני כותב עליו זמין בריפו הזה:
https://github.com/ynonp/redux-memory-game
אז במשחק זיכרון יש לי חבילה של קלפים שאני יכול ליצור עם:
const initialState = {
cards: _.shuffle(_.range(10).map(i => ({
value: Math.floor(i / 2),
hidden: true,
found: false,
}))),
};
המאפיין value של קלף מייצג את המספר שעליו, hidden אומר אם הוא הפוך ו found אומר אם מצאנו אותו יחד עם הזוג שלו.
הפונקציה הראשונה שאני צריך במשחק זיכרון היא פונקציה שנקראת כשמשתמש לוחץ על קלף. הנה המימוש המפתיע שלי:
function onPlay(state, action) {
const index = action.payload;
if (state.cards[index].hidden) {
state.cards[index].hidden = false;
}
const visibleCards = state.cards.filter(c => !c.found && !c.hidden);
if (visibleCards.length === 2) {
if (visibleCards[0].value === visibleCards[1].value) {
visibleCards[0].found = true;
visibleCards[1].found = true;
}
}
}
ולמה זה מפתיע? כי אנחנו יודעים שב Redux כל Reducer צריך לקבל state ו action ולהחזיר state חדש. פה אני פשוט משנה את ה state במה שנראה כמו שינוי במקום:
state.cards[index].hidden = false;
באופן אוטומטי ספריית immer הופכת את הקוד הזה לקוד שלא באמת משנה את הסטייט אלא יוצר סטייט חדש ומחזיר אותו.
אגב פונקציה שניה שתעזור לי במשחק זיכרון תופעל כמה שניות אחרי התור השני בשביל להפוך את כל הקלפים שצריך להסתיר:
function onCheck(state) {
for (let c of state.cards) {
if (!c.found) {
c.hidden = true;
}
}
}
את כל הקוד הזה כתבתי בקובץ שנקרא memorySlice.js ועד עכשיו לא היה שום דבר שקשור לרידאקס או ל redux-toolkit בקובץ. החלק האחרון הוא כבר נקודת הכניסה ל Redux:
export const memorySlice = createSlice({
name: 'memory',
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: { play: onPlay, check: onCheck },
});
הפונקציה createSlice מקבלת את שתי הפונקציות שכתבתי לתוך מפתח reducer של אובייקט הפרמטרים שלה ויוצרת גם Reducer מסודר וגם Action Creators. עכשיו אני יכול להשתמש בערך ההחזר שלה כדי לייצא את התוצאות:
export const { play, check } = memorySlice.actions;
export default memorySlice.reducer;
לסיום הקובץ אני יוצר ומייצא עוד שתי פונקציות עזר איתן קוד מקבצים אחרים יוכל לקרוא מידע מה State:
export const selectVisibleCards = (state) => state.memory.cards.filter(c => !c.hidden && !c.found);
export const selectCards = (state) => state.memory.cards;
## איך משתמשים ב Reducers1 419
# שאלה של גישה
לא מזמן דיברתי עם מתכנת ריאקט מאוד מוכשר, ששיכנע אותי בנאומים חוצבי להבות שהדרך הנכונה לכתוב קוד ריאקט היא להשתמש ב useEffect וב Custom Hooks כדי להעביר כמה שיותר לוגיקה, כולל לוגיקה של תקשורת, לתוך הקומפוננטות. הרבה יותר קל, הוא טען, לנהל את הלוגיקה של השליפות כשהשליפה קרובה לקוד שמציג את התוצאה שלה. בנוסף, כשאתה מוחק את הקומפוננטה אוטומטית אתה מוחק את קוד השליפה שקשור אליה, ולא נשאר עם מנגנוני שליפה שאף אחד לא משתמש בהם.
ואז מצאתי את המשפט הבא בתיעוד של Redux Toolkit:
> With RTK Query, you usually define your entire API definition in one place. This is most likely different from what you see with other libraries such as swr or react-query, and there are several reasons for that. Our perspective is that it's much easier to keep track of how requests, cache invalidation, and general app configuration behave when they're all in one central location in comparison to having X number of custom hooks in different files throughout your application.
בתרגום וקיצור הם אומרים שהם בנו את RTK Query כי הרבה יותר קל לנהל תקשורת כשכל הקוד שמתעסק בה נמצא במקום אחד.
"מי צודק" זאת בכלל לא השאלה כאן. הרבה אתגרים בתכנות בנויים ככה שאפשר לפתור אותם בכל מיני דרכים. כל גישה מגיעה עם הכלים שלה ובכל גישה אפשר לבנות פיתרונות טובים (או פחות טובים).
הדרך להיות מתכנתים טובים יותר היא לא למצוא את "הגישה האחת" שתמיד נכונה, אלא להיפתח ליותר גישות, ולהבין בתוך גישה מסוימת מה הסטנדרטים שמקובלים בה, מה זה אומר לכתוב קוד טוב (או גרוע) כשהלוגיקה בתוך הקומפוננטות, ומה זה אומר לכתוב קוד טוב (או גרוע) כשהלוגיקה נמצאת במקום אחר, ואיזה מלכודות או סכנות צפויות לנו בכל אחת מהגישות.
1 419
# הזמנה לוובינר: היכרות עם Solid.JS
ריאן קרניאטו, היוצר של סוליד, העיד על עצמו שלקח לו הרבה זמן להבין מה אנשים מוצאים בריאקט ולהתאהב בפריימוורק בעצמו. עבורו הרגע המכריע היה החשיפה של React Hooks, שנתנו עדיפות לקומפוננטות פונקציונאליות ואיפשרו לארגן קוד לשימוש חוזר במקום אחד - בתוך Custom Hook - ואז להשתמש בו שוב ושוב במספר גדול של קומפוננטות.
אבל בגלל איך שריאקט בנוי ל Hooks יש בעיה: כמעט כל Hook בריאקט חייב לקבל רשימת תלויות, בגלל שריאקט לא יודע "לראות" לבד את הקשר בין קוד לבין הדברים שהוא תלוי בהם. אותו חיבור שהוא הבסיס של מה שנקרא ריאקטיביות.
במילים אחרות, כשקוד ריאקט רוצה ליצור אפקט על משתנה סטייט מסוים, למשל שכל פעם שמשתנה סטייט מסוים מתעדכן אז לעדכן אוטומטית את ה document.title, הוא יצטרך להיראות כך:
function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}
אבל הדיון על "רשימת התלויות" הולך הרבה יותר רחוק מאיך לבנות Custom Hooks בריאקט. אם נחשוב על זה, כל המהות של קומפוננטה זה להיות משהו שמתעדכן כש State או Props שלו מתעדכנים - ושוב יש לנו את משחק התלויות. מידע, ששמור בתור סטייט או פרופ, משפיע על הדבר שאנחנו רואים על המסך.
הדרך שבה ריאקט מזהה ומעדכן את "התוצאות" כשהתלויות משתנות היא Virtual DOM. ריאקט שומר מבנה נתונים גדול של מה צריך להיות ומה קיים כרגע, ויודע לזהות הבדלים בין שני המצבים. כל פעם שתלות מסוימת משתנה ריאקט יחשב מחדש את ההבדלים ויכתוב את התוצאה המעודכנת למסך.
סוליד, הפריימוורק שיצר ריאן קרניאטו, לוקח טייק אחר לגמרי.
במקום לעקוב אחר תלויות במבני נתונים שנקראים Virtual DOM, הוא שומר על קשר ישיר בין משתנה לבין הדברים שתלויים בו. הריאקטיביות של סוליד היא ריאקטיביות ברזולוציה גבוהה - אף פעם לא מעדכנים מחדש קומפוננטה מלאה רק בגלל שמשהו בסטייט השתנה, אלא כל דבר שמשתנה גורם לעדכון מדויק של הדברים שמושפעים ממנו.
התוצאה היא פריימוורק שנראה דומה לריאקט אבל עובד אחרת לגמרי:
1. הקומפוננטות הן פונקציות וקוד לשימוש חוזר הוא Custom Hook, אבל התלויות מזוהות בצורה אוטומטית ואין שום אילוץ להפעיל Hooks רק מתוך קוד של קומפוננטה.
2. פונקציית הקומפוננטה עצמה מורצת רק פעם אחת כדי "לחבר" בין משתני הסטייט לבין הדברים שרואים על המסך. משם, כל פעם שמשתנה סטייט מתעדכן הוא מייצר עדכון אוטומטי של ה DOM.
3. אין Virtual DOM ולכן יש הרבה פחות חשיבות (מבחינת ביצועים) לחלוקה נכונה לקומפוננטות.
4. בהרבה מאוד מצבים נקבל ביצועים טובים יותר עבור קוד כמעט זהה.
ביום חמישי הקרוב אעביר בזום וובינר של שעה על סוליד בו אראה לכם איך לעבוד עם הפריימוורק וגם נראה את ההבדל מבחינת ביצועים בינה לבין ריאקט. בסיום הוובינר, בנוסף להיכרות עם פריימוורק די מדליק, גם תבינו מהי ריאקטיביות ואיך פריימוורק ריאקטיבי צריך להיראות. גם אם בסוף לא תעבדו עם סוליד, היכרות איתו ועם שיטת המחשבה שלו תעזור לכם להבין את ריאקט טוב יותר.
אפשר להצטרף לוובינר בחינם בלחיצת כפתור בדף האירוע כאן:
https://www.tocode.co.il/workshops/116
נתראה
ינון1 419
# מהו useDeferredValue ולמה שיהיה לכם אכפת
ריאקט 18 יצא לפני כמה ימים והפיצ'ר המרכזי שלו נקרא Concurrent Mode. פיצ'ר זה מאפשר לריאקט להתחיל לרנדר משהו, להבין שזה לוקח יותר מדי זמן או שיש משהו יותר חשוב לעשות ואז לעצור, לרנדר את הדבר היותר חשוב ולחזור לעבודה הארוכה יותר.
אחד השימושים של הפיצ'ר הזה הוא הפונקציה useDeferredValue שמגדירה לריאקט שחישוב מסוים הוא פחות חשוב ואפשר לחכות קצת עם ה render-ים שתלויים בו כדי לא להאט את כל העמוד.
הפונקציה יעילה במיוחד עבור רשימה עם תיבת סינון: כשמשתמש מקליד טקסט בתיבה אנחנו רוצים לראות תגובה מיידית, אבל סינון הפריטים עצמו יכול לקחת קצת יותר זמן ולעבוד ברקע.
הקוד הבא ממחיש את ההתנהגות הרצויה תוך שימוש ב useDeferredValue:
export default function App() {
const [search, setSearch] = useState("");
const dsearch = useDeferredValue(search, { timeoutMs: 10000 });
return (
<div className="App">
<input
type="search"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<FilteredList search={dsearch} isPending={dsearch !== search} />
</div>
);
}
ואפשר לראות אותו בפעולה בקודסנדבוקס הבא:
<iframe src="https://codesandbox.io/embed/condescending-andras-gtdmwt?fontsize=14&hidenavigation=1&theme=dark"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="condescending-andras-gtdmwt"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>
קצת הסברים על הקוד:
1. הסטייט search מייצג את הטקסט בתיבה; המשתנה dsearch הוא עותק של search שהאלמנטים שתלויים בו פחות חשובים.
2. בפרק הזמן בין עדכון search לעדכון dsearch ריאקט מנסה ברקע לרנדר את הקומפוננטה FilteredList שמושפעת מ dsearch. בהתחלה הערכים של dsearch ו search יהיו שונים ואחרי שהרינדור האיטי יסתיים הערכים יהיו שווים. אני בודק את ההבדל בין שני המשתנים כדי לדעת אם ריאקט עכשיו עובד ברקע, ומשקף את זה למשתמשים באמצעות המשתנה isPending.
3. אם תכנסו לקודסנדבוקס ותשחקו שם עם המספרים תגלו שאומנם עבור 10,000 שורות הקוד עובד ממש בסדר, ככל שמעלים את מספר השורות אנחנו מצליחים לשבור את useDeferredValue ולמשל ב 90,000 שורות האיטיות כבר מורגשת בעדכון תיבת הטקסט. אולי זה ייפתר בגירסאות עתידיות של ריאקט ובכל מקרה זו תזכורת טובה לכך שתיקון גנרי בפריימוורק בדרך כלל לא יכול לפתור לכולם את הבעיות, וגם בריאקט 18 נצטרך לשים לב למה אנחנו מרנדרים כדי לשמור על ביצועים טובים.1 419
# עוד שתי סיבות טובות לחוב טכני
המונח "חוב טכני" (Technical Debt) מתאר מצב שבו אנחנו עושים פשרה במערכת כדי להתקדם מהר יותר. הבחירה במונח "חוב" באה להזכיר לנו שלפשרה הזאת יש מחיר, ושעוד מעט נצטרך "להחזיר" את החוב כלומר לארגן מחדש את הקוד בצורה נכונה. למי שצובר הרבה חובות טכניים יהיה קשה להתקדם ובסופו של דבר יצטרך להכריז על "פשיטת רגל", כלומר לזרוק את כל הקוד ולהתחיל מחדש.
הסיבות הקלאסיות לחוב טכני כוללות-
1. בחירת פריימוורק שייתן פיתרון מהיר למרות שאנחנו יודעים שהוא לא מספיק גמיש או כולל בעיות ביצועים (היוש פונגאפ)
2. כתיבת קוד ללא בדיקות או ללא תיעוד
3. כתיבת קוד שלא מטפל במקרי שגיאה נפוצים
4. אי שידרוג תלויות כשגירסאות חדשות יוצאות
בכל המקרים האלה אנחנו בוחרים "לוותר" על איכות של קוד כדי להגיע מהר יותר ללקוחות עם מוצר שעובד (גם אם לא עובד מושלם), כדי שנוכל לקבל פידבק על המוצר ואחרי זה להמשיך לסבב תיקונים.
חוץ מהסיבות הקלאסיות לחוב טכני יש עוד שני מקרים שמצדיקים חזרה אחורה ותיקון, למרות שהם הרבה פחות מכוונים:
## כשאין לנו מספיק ידע כדי לפתור בעיה
בפרויקט מסוים הייתי צריך לבנות ללקוח תשתית בדיקות אבל הפרויקט השתמש בכמה ספריות שלא עבדו טוב עם jest. באותו זמן, איך שלא סובבתי את הגדרות הבניה לא הצלחתי לגרום ל jest לבדוק את הפרויקט ובסופו של דבר כתבנו את הבדיקות עם mocha ו sinon.
בפרויקט הזה לא היתה בחירה מודעת להשתמש בפיתרון מסוים שיהיה מהיר יותר מהאלטרנטיבות רק בשביל לפתור את הבעיה, אלא הפיתרון שנבחר היה היחיד שהיה נראה אפשרי באותו זמן.
## כשאנחנו לא מספיק מכירים את הדריכות
בפרויקט אחר כתבתי ללקוח קומפוננטת ריאקט לטבלה שהיתה מאוד מושקעת מבחינת ביצועים - היא יכלה להכיל מאות אלפי שורות וכל פעם עשתה render רק לשורות שרואים על המסך, כדי שאפשר יהיה להפעיל סינונים ומיונים מהירים.
הקוד עבד מצוין רק אחרי שהם הגיעו לפרודקשן שמתי לב שהם משתמשים בטבלה כדי להציג מספר מאוד קטן של נתונים, בדרך כלל פחות מעשר שורות. כששאלתי על זה הסתבר שבאמת בהתחלה היתה איזו אי הבנה והם רצו משהו בתור הכנה לעתיד אבל בסוף העתיד הזה לעולם לא יגיע.
## מה עושים
בשתי הדוגמאות יש לנו פיתרון טוב שעובד. מוקה וסינון, למרות שהם ישנים, עדיין מתוחזקים ואין שום בעיה להמשיך להשתמש בהם גם היום וגם בעתיד הנראה לעין. טבלה שיודעת לטפל גם במאות אלפי שורות היא קומפוננטה טובה, גם אם בפועל היא מציגה הרבה הרבה פחות.
ובכל זאת שתי הדוגמאות נכנסות אצלי לקטגוריה של חוב טכני ודורשות Refactoring. אלה פיתרונות יותר מורכבים ממה שהיה צריך וכל הרחבה שלהם תחייב השקעה גדולה יותר. בסופו של דבר אנחנו "משלמים שכירות" על כל שורת קוד במערכת, וגמישות יותר חשובה ממנגנונים מתוחכמים שלא משתמשים בהם.
1 419
# איך עובדת רשימת הקפיצות של vim
אחד הפיצ'רים שאני הכי אוהב ב vim הוא רשימת הקפיצות. אם אתם לא מכירים (ואולי גם אם כן) הנה הסבר קצר מה זה ולמה זה טוב.
## קפיצה ב vim
שתי הדרכים "הקלאסיות" לטייל בקוד בעורכי טקסט הן ללחוץ על שם קובץ עם סמן העכבר ולהזיז את הסמן עם החצים עד שמגיעים למקום שרוצים לערוך. ב vim אנחנו כמעט לא משתמשים בשני מנגנונים אלה ובמקומם יש לנו קפיצות:
1. קפיצה לקובץ חדש עם
:e.
2. קפיצה לטקסט מסוים בקובץ עם /.
3. קפיצה לשורה מסוימת, או ל marker מסוים.
4. קפיצה למקום האחרון בו שיניתי טקסט עם '..
5. קפיצה לקובץ ששמו נמצא מתחת לסמן עם gf.
6. קפיצה להגדרה של פונקציה עם gd.
ועוד המון קפיצות בסגנון זה.
הטריק הוא שכל פעם שאנחנו קופצים ממקום למקום ב vim, אז וים שומר את המקום הקודם שהיינו בו ברשימה שנקראת רשימת קפיצות. בכל רגע נתון אפשר לכתוב :jumps ולראות את הרשימה הזאת, או לכתוב :clearjumps כדי למחוק את ההיסטוריה.
## טיול ברשימת הקפיצות
נניח ש jumps מראה לי את הפלט הבא:
:
jump line col file/text
8 1 0 NERD_tree_1
7 12 0 NERD_tree_1
6 4 0 package.json
5 1 0 src/App.vue
4 3 24 src/main.js
2 1 0 NERD_tree_2
1 5 0 NERD_tree_2
> 0 1 61 import { createRouter, createWebHashHistory } from 'vue-router'
1 1 0 ~/tmp/blog/vue/memory-game/node_modules/vue-router/dist/vue-router.d.ts
אז זה אומר שאני עכשיו מסתכל על השורה שמתחילה ב import ומסומנת בחץ. אני יכול ללחוץ על Ctrl+O כדי לזוז קפיצה אחת אחורה כדי להגיע לנווט הקבצים שנמצא בדיוק שורה אחת מעליי, או Ctrl+I כדי לקפוץ שורה אחת קדימה לקובץ vue-router.d.ts.
אפשר גם לדלג לקפיצה מסוימת, למשל לחיצה על 6 ואז על Ctrl+O תשלח אותי לקפיצה מספר 6 כלומר לקובץ package.json.
שימוש בקפיצות וצפיה ברשימת הקפיצות עוזר לנו לשמור על אוריינטציה במיוחד בניווט בפרויקט גדול ולהבין בדיוק מה שיניתי ואיפה.1 419
# היום למדתי: הפונקציה rerender של react-testing-library
הפונקציה rerender עושה בדיוק מה שהשם שלה רומז, ואני לא מאמין שהעברתי חיים שלמים בלעדיה. היא הכי שימושית כשיש לנו קומפוננה שה props שלה צריכים להשתנות במהלך הבדיקה. ככה זה עובד-
## הקומפוננטה שאני בודק
בשביל המשחק ניקח קומפוננטה שמציגה מספר כלשהו של תיבות טקסט מסונכרנות, כשהמספר מתקבל דרך prop. אני רוצה לוודא שכשאני משנה את מספר התיבות, הטקסט בכל התיבות עדיין נשאר אותו דבר. זה קוד הקומפוננטה:
export function ManyInputs(props) {
const { n=5 } = props;
const [ value, setValue ] = useState('');
return (
<div data-testid="many-inputs">
{new Array(n).fill(0).map((_, index) => (
<input
key={index}
type="text"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
))}
</div>
);
}
## החיים בלי rerender
דרך מסורבלת לבדוק את ההתנהגות של הקומפוננטה כשמספר התיבות משתנה היא לשים את הקומפוננטה בקומפוננטת עזר שנכתוב בקוד הבדיקה. לחיצה על כפתור או שינוי טקסט בקומפוננטת העזר תשנה את מספר התיבות, וכך אפשר יהיה לראות את האפקט.
הנה בדיקה לדוגמה שמשתמשת ברעיון הזה:
test('change n', () => {
const Helper = () => {
const [n, setN] = useState(5);
return (
<div>
<label>
Number Of Boxes:
<input type="text" value={n} onChange={(e) => setN(Number(e.target.value))} />
</label>
<ManyInputs n={n} />
</div>
);
};
render(<Helper />);
let allInputs = within(screen.getByTestId('many-inputs')).getAllByRole('textbox');
userEvent.type(allInputs[0], 'hello');
for (const tb of allInputs) {
expect(tb).toHaveDisplayValue('hello');
}
userEvent.type(screen.getByLabelText('Number Of Boxes:'), '8');
allInputs = within(screen.getByTestId('many-inputs')).getAllByRole('textbox');
for (const tb of allInputs) {
expect(tb).toHaveDisplayValue('hello');
}
});
## החיים היפים עם rerender
במקום לבזבז מקום על כתיבת קומפוננטת עזר, הפונקציה rerender יכולה לעזור לנו לשנות prop על הקומפוננטה שאותה אנחנו בודקים. הפונקציה נשלחת אלינו כשדה באוביקט שחוזר מ render, ומספיק להפעיל אותה עם ה props החדשים. הנה קוד הבדיקה המעודכן:
test('change n', () => {
const { rerender } = render(<ManyInputs n={5} />);
let allInputs = within(screen.getByTestId('many-inputs')).getAllByRole('textbox');
expect(allInputs).toHaveLength(5);
userEvent.type(allInputs[0], 'hello');
for (const tb of allInputs) {
expect(tb).toHaveDisplayValue('hello');
}
rerender(<ManyInputs n={8} />);
allInputs = within(screen.getByTestId('many-inputs')).getAllByRole('textbox');
expect(allInputs).toHaveLength(8);
for (const tb of allInputs) {
expect(tb).toHaveDisplayValue('hello');
}
});
יותר קצר, יותר מדויק והרבה יותר קל לתחזוקה.1 419
# פיבוט
כשסטארט-אפ לא מצליח להתרומם המייסדים עושים פיבוט - לוקחים את המוצר שפיתחו, משנים אותו קצת, בונים סביבו סיפור אחר ויש להם סטארט אפ חדש. פיבוטים טובים הם המפתח להצלחה:
נטפליקס הפכו מחברה ששולחת DVD בדואר לחברת הפקת תוכן, ובדרך הובילו את מהפכת הסטרימינג באינטרנט.
לטוויטר קראו בהתחלה Odeo והמטרה שלהם היתה להפוך פודקסטים לנגישים יותר.
והבצק פליידו שהילדים לשים כל היום? הוא בכלל התחיל בתור חומר ניקוי לקירות.
פיבוט טוב לא זורק לפח את כל מה שהיה קודם, אלא בונה עליו את הצעד הבא בשביל להגיע לפופולריות.
וכמו חברות גם אנחנו המתכנתים צריכים כל כמה שנים איזה פיבוט טוב, כדי להוסיף עניין ולמצוא הזדמנויות חדשות. כשמדענית נתונים לומדת Full Stack, היא לא מפסיקה להיות מדענית נתונים, היא פשוט לוקחת צעד בכיוון חדש תוך שימוש בכל הידע הקיים. צעד אחרי צעד, פיבוט אחרי פיבוט, אנחנו בונים מיומנות ובונים קריירה.
1 419
# איך סוליד פתר את הבעיה הכי מעצבנת עם useEffect של ריאקט
הפונקציה useEffect של ריאקט מגדירה קשר חד כיווני בין מידע מסוים שאנחנו עוקבים אחריו לבין "משהו" חיצוני שצריך לקרות. בין הדוגמאות הפופולריות לאפקטים נוכל למצוא:
## דוגמאות לאפקטים
1. קומפוננטה שומרת מזהה של משאב חיצוני, וכל פעם שהוא מתעדכן הקומפוננטה מושכת מידע חדש מהרשת שמתבסס על המזהה הזה.
2. קומפוננטה שומרת מחרוזת טקסט שמשמשת בתור title של הדף. כל פעם שהמחרוזת מתעדכנת גם משנים את ה document.title בהתאמה.
3. קומפוננטה מגדירה שעון שמתקתק כל X שניות, כאשר X הוא משתנה סטייט. כשהמשתנה מתעדכנן גם השעון משנה את התדירות.
4. קומפוננטה כוללת שדה קלט בטופס. כל פעם שמשתמש מעדכן את המידע מריצים וולידציה על המידע ואולי מגדירים הודעת שגיאה במשתנה סטייט אחר.
5. קומפוננטה מעדכנת משתנה State כל פעם ש prop מסוים שלה מתעדכן.
הבעיה הכי מעצבנת עם useEffect היא שהפונקציה מטפלת בשני סוגים של מקרים, בלי לתת לנו דרך טובה להבדיל ביניהם:
1. יש אפקטים שצריכים לקרות גם אחרי ה render הראשון - לדוגמה להפעיל timer או למשוך מידע מ API מרוחק.
2. יש אפקטים שצריכים לקרות רק כשאחד הערכים שמשפיע על האפקט משתנה - לדוגמה עדכון ה document.title כשמשתנה state משנה את ערכו, או עדכון משתנה state אחרי שקיבלנו ערך חדש ל property מסוים.
הקומפוננטה Ticker הבאה היא דוגמה למקרה הראשון. היא מציגה מספר שעולה ב-1 כל שניה:
function Ticker() {
const [value, setValue] = useState(0);
useEffect(() => {
setInterval(() => {
setValue(v => v + 1);
}, 1000);
}, []);
return (
<p>{value}</p>
)
}
והקומפוננטה הבאה מציגה תיבת טקסט שכוללת את התוכן של document.title וכל פעם שהתיבה מתעדכנת העדכון ייכתב החוצה ל document.title:
export default function TitleEditor() {
const [value, setValue] = useState(document.title);
useEffect(() => {
console.count(`writing ${value} to document.title`);
document.title = value;
}, [value]);
return (
<input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
)
}
בעוד ש useEffect מציע פיתרון ממש טוב לסוג הראשון של אפקטים, קומפוננטה כמו TileEditor עושה עבודה מיותרת - היא מעדכנת את document.title גם בפעם הראשונה כשהערך זהה.
עכשיו תגידו ובצדק שבדוגמה הפשוטה של TitleEditor מאוד קל לפתור את זה - פשוט בודקים אם document.title זהה ל value שיש לי, ואם כן לא מעדכנים אותו. אבל במקרים יותר מורכבים ייתכן ואנחנו רוצים לסנכרן את המשתנה עם מידע בשרת וגם הקריאה עצמה לוקחת זמן. הדרך המקובלת בריאקט לדלג על האפקט אחרי render ראשוני היא לייצר ref כדי לזכור האם האפקט רץ פעם אחת. אפשר לקרוא על הטכניקה כאן:
https://medium.com/swlh/prevent-useeffects-callback-firing-during-initial-render-the-armchair-critic-f71bc0e03536
## טריק קטן של סוליד שעושה את ההבדל
אז מה הטריק של סוליד שהופך את כל הסיפור הזה להרבה יותר קל? פשוט - הפונקציה useEffect שלהם, שנקראת createEffect יכולה לקבל פרמטר שני מיוחד שיעבור לפונקציית ה Callback רק בהפעלה הראשונה של האפקט (ובהפעלות הבאות בפרמטר הזה יישלח הערך "הקודם" של האפקט).
הקומפוננטה הבאה בסוליד משיגה את אותו אפקט, תוך דילוג על הכתיבה הראשונה (המיותרת) ל document.title:
function App() {
const [value, setValue] = createSignal(document.title);
createEffect((prev) => {
if (prev) {
document.title = value();
}
return value();
}, null);
return (
<input
type="text"
value={value()}
onInput={(e) => setValue(e.currentTarget.value)}
/>
);
}
שימו לב שאצל סוליד בשביל להשתמש בטריק הזה האפקט חייב להחזיר איזשהו ערך (כדי שיהיה מה להעביר ל prev פעם הבאה), ובנוסף בתוך האפקט בקוד שבאמת מורץ חייבת להיות קריאה של value(), כדי שהאפקט יידע שהוא מושפע משינויים במשתנה זה. זאת הסיבה שאני מחזיר את התוצאה של value() בסוף הפונקציה.1 419
# אפילו שזה רק שורה
משמעות הקיצור DRY היא Don't Repeat Yourself, ובהקשר של קוד אנחנו מתכוונים לא לחזור על אותו קטע קוד שוב ושוב. אבל מה אם קטע הקוד הזה הוא שורה אחת?
## איך בודקים קומפוננטה בריאקט
ספריית הבדיקות הפופולרית לריאקט היום נקראת react-testing-library ויש בה מודול שנקרא userEvent, שמדמה פעולות של משתמש. הקוד הזה לדוגמה לוקח קומפוננטה בשם MyComponent ולוחץ על הכפתור שבתוכה עם הטקסט click me:
test('trigger some awesome feature when clicking the button', () => {
render(<MyComponent/>)
userEvent.click(screen.getByRole('button', name: /click me!/))
})
עכשיו בואו נגיד שיש לנו כמה עשרות בדיקות על הקומפוננטה, ועל הקומפוננטה מוגדרים 4 כפתורים מרכזיים - למשל אם קומפוננטה למשחק זיכרון צבעים אפשר לכתוב בדיקה כזאת שמנסה את המשחק:
test('memory game red-blue-blue-red', () => {
render(<MemoryGame colors=['red', 'blue', 'blue', 'red'] />);
userEvent.click(screen.getByRole('button', name: /red/));
userEvent.click(screen.getByRole('button', name: /blue/));
userEvent.click(screen.getByRole('button', name: /blue/));
userEvent.click(screen.getByRole('button', name: /red/));
expect(screen.getByText('Bravo!')).toBeInTheDocument();
});
## ואיך זה נשבר?
אז יש לנו את המשחק זיכרון וכתבנו לו כמה עשרות בדיקות שכולן לוחצות על כפתורי הצבעים לפי סדר מסוים ובודקות מה קורה בעקבות הלחיצה, ועכשיו מישהו שינה משהו בקוד המשחק ובמקום כפתור עם טקסט הצבע מיוצג בצורה אחרת, אולי אלמנט div עם צבע רקע שמתאים לצבע, למשל אלמנט div עם צבע רקע אדום.
ראיתם מה קרה עכשיו לבדיקות?
בשביל שהבדיקה תחזור לעבוד צריך להחליף כל מקום שמופיעה השורה:
userEvent.click(screen.getByRole('button', name: /red/));
בשורה מקבילה עבור ה div. במקרים פשוטים אפשר להשתמש בפונקציית Replace All של עורך הטקסט שלכם, במקרים יותר מורכבים יכול להיות שמצפה לכם קצת עבודה ידנית.
## מה עושים במקום?
הלקח כאן הוא ש DRY לא קשור בכלל למספר השורות שחוזרות על עצמן אלא למשמעות של הפעולה - קוד הבדיקה הראשון שכתבתי חיבר בין "לחץ על הכפתור האדום" ל"לחץ על הכפתור האדום בדרך של userEvent ועם מבנה ה DOM של הקומפוננטה MemoryGame". החלק שחזר על עצמו שוב ושוב בקוד היה "כדי ללחוץ על הכפתור האדום צריך למצוא אלמנט כפתור עם הטקסט red". המשפט הזה הופיע בצורה מובלעת כל פעם שכתבתי את שורת הלחיצה.
גישה טובה יותר שהיתה מונעת את הבעיה מראש היתה להגדיר איזה פעולות אנחנו צריכים לעשות עם הקומפוננטה וליצור פונקציה קטנה עבור כל פעולה, נניח פונקציה שלוחצת על הכפתור האדום:
function clickOnRed(screen) {
userEvent.click(screen.getByRole('button', name: /red/));
}
את קוד הבדיקה אני אחליף עכשיו ל:
test('memory game red-blue-blue-red', () => {
render(<MemoryGame colors=['red', 'blue', 'blue', 'red'] />);
clickOnRed();
clickOnBlue();
clickOnBlue();
clickOnRed();
expect(successTextElement()).toBeInTheDocument();
});
וקובץ הבדיקות שלי עכשיו הרבה יותר יציב - כשמשהו ישתנה באופן שבו אני ניגש לכפתור האדום, אני צריך לשנות קוד רק בפונקציה אחת.