מבוא זריז ל Redux

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

מצאתי במקרה פוסט ישן שכתבתי על רידאקס. ארבע שנים עברו ולמרות שרידאקס לא השתנתה הרבה, כן קרו כמה דברים מרגשים ב JavaScript בזמן הזה כמו הספריה immer או המילה const. בקיצור היה נראה לי שווה לנסות לכתוב מבוא זריז וטוב יותר לרידאקס.

(וכן - ניסיתי את MobX, לא נפלתי מהכסא, ואני נשאר עם רידאקס). מוכנים? נמשיך לקוד.

1. קצת תיאוריה

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

const initialState = {
  rooms: [
    { id: 0, name: 'Loby' },
    { id: 1, name: 'JavaScript Chats' },
  ],
  activeRoomId: 0,
  messages: [
    { id: 0, from: 'ynon', text: 'Hello Everyone' },
  ],
  username: "guest",
};

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

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

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

בשלב השני נחלק את הפעולות לחלקים בעץ המידע עליו הן יכולות להשפיע. פעולת ״עדכון רשימת חדרים״ למשל תשפיע על השדה rooms באובייקט המידע, וכך גם ״יצירת חדר שיחה חדש״. לעומתן הפעולות ״שליחת הודעה״, ״הצטרפות לחדר״ ו ״עדכון רשימת הודעות״ כולן ישפיעו על שדה messages של אובייקט המידע.

2. עכשיו אנחנו מוכנים לראות קצת קוד

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

נמשיך לקוד שמגדיר את הפעולות:

export function receivedRooms(newListOfRooms) {
  return { type: 'RECEIVED_ROOMS', payload: newListOfRooms };
}

export function setActiveRoom(roomId) {
  return { type: 'SET_ACTIVE_ROOM', payload: roomId };
}

export function createRoom(roomName) {
  return { type: 'CREATE_ROOM', payload: roomName };
}

export function receivedMessage(message) {
  return { type: 'RECEIVED_MESSAGE', payload: message };
}

export function setUsername(newUsername) {
  return { type: 'SET_USERNAME', payload: newUsername };
}

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

  1. הפעולה receivedRooms מתארת את האירוע בו קיבלנו רשימת חדרים חדשה מהשרת.

  2. הפעולה receivedMessage מתארת את האירוע בו קיבלנו הודעה חדשה בצ'ט.

  3. הפעולה setUsername מתארת את האירוע בו משתמש החליט לשנות את שם המשתמש.

  4. הפעולה createRoom מתארת מצב בו חדר שיחה חדש צריך להיווצר ביישום.

  5. הפעולה setActiveRoom מתארת את האירוע של החלפת חדר שיחה.

כל פעולה כזו מתארת אירוע שקורה ביישום ושמשפיע על מצב היישום. לדוגמא אחרי שנקבל פעולת setActiveRoom אנחנו חושבים שהשדה activeRoomId יקבל ערך חדש - הערך שמתואר באירוע. אחרי שנקבל אירוע receivedMessage אנחנו מצפים שיתווסף אוביקט חדש לרשימת ההודעות בשדה messages.

את הדרך בה כל פעולה משפיעה על אוביקט המצב ב Redux אפשר לתאר באמצעות פונקציה - פונקציה שתקבל כפרמטר את אוביקט המצב ואת הפעולה ותחזיר אוביקט מצב חדש. ברידאקס פונקציה זו נקראת Reducer.

כך יכול להיראות ה Reducer לאפליקציית הצ'ט שאנחנו בונים:

const reducer = produce((state, action) => {
  switch(action.type) {
    case 'SET_USERNAME':
      state.username = action.payload;
      break;

    case 'RECEIVED_MESSAGE':
      state.messages.push(action.payload);
      break;

    case 'CREATE_ROOM':
      state.rooms.push({ id: nextId(state.rooms), name: action.payload });
      break;

    case 'SET_ACTIVE_ROOM':
      state.activeRoomId = action.payload;
      break;

    case 'RECEIVED_ROOMS':
      state.rooms = action.payload;
      break;
  }
}, initialState);

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

3. נוסיף קצת Redux לקערה

ספריית Redux יודעת לקחת Reducer כמו שכתבנו ולהפוך אותו לאוביקט Store לפי הארכיטקטורה של פלאקס:

  1. האוביקט יחזיק מצב פנימי (זה ה State) שמתאר את כל מה שקורה ביישום.

  2. לאוביקט תהיה פונקציית Dispatch שתקבל Action ותשנה את המצב הפנימי.

  3. לאוביקט תהיה פונקציית getState שתחזיר את המצב הפנימי.

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

import { createStore } from 'redux';

והשני הוא הפעלת הפונקציה מהספריה:

window.store = createStore(reducer);

4. למה זה מדליק

נעבור לדפדפן ונפתח את הקונסול (בינתיים אין לנו עדיין ממשק משתמש לכן כל המשחקים יהיו דרך שם). המשתנה store מכיל את האוביקט שקיבלנו מ Redux ויש לו פונקציה בשם getState שמחזירה את הסטייט הראשוני. נסו מחלון הקונסול לכתוב:

store.getState();

כדי לראות את אוביקט ה initialState שיצרתם.

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

store.dispatch({ type: 'SET_USERNAME', payload: 'master' })

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

5. אז מה בעצם הרווחנו בכל הסיפור הזה?

די הרבה האמת. הנה כמה נקודות:

זוכרים את אובייקט המידע שהתחלנו אתו? בכל רגע נתון אפשר לראות את כל מצב היישום (כולל כל ההודעות, חדר השיחה הנוכחי, שם המשתמש) במקום אחד. פשוט כתבו את השורה הבאה בחלון ה Console בדפדפן:

store.getState()

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

מאחר וכל מבנה היישום משתמש במידע Immutable, קל מאוד לשלב את הספריה עם ריאקט ולשפר ביצועים באמצעות React.memo. למעשה בחיבור של ממשק ריאקט ל Redux אנחנו מקבלים את Memo באופן אוטומטי.