Redux – ארכיטקטורת Flux, אבל כמו שצריך

אם עבדתם אפילו קצת עם הספריה הנהדרת React מבית פייסבוק, כנראה שהבנתם (או לפחות ניסו להסביר לכם) את ההבדל בין מאפייני רכיב (props) למצב שלו (state). ואכן בפוסט המצויין Thinking in React אנו מוצאים הסבר מפורט על איך להחליט איזה מידע שייך לכל אחד מהם (בגדול, כל מה שיכול להגיע מבחוץ יהיה בפרופס, וכל מה שישתנה ע"י הרכיב וגם באחריותו יהיה ב-state).

ארכיטקטורת Flux, שהתפרסמה גם היא ע"י פייסבוק ביחד עם React, לוקחת את הקונספט של MVC צעד אחד קדימה, ע"י הגדרת מקומות המרכזים state גלובאלי, אלו הם ה-Stores. כדי לשמור על זרימת נתונים ברורה וצפויה, שינויים למידע ב-stores לא מתבצעים ישירות, אלא ע"י יצירת אירועים הנקראים פעולות (Actions), עליהם מאזינים ה-stores ומבצעים שינויים במידע בהתאם. ה-Actions נוצרים לרוב בעקבות פעולת משתמש, לדוגמה לחיצה על כפתור. על כל אלו מנצח ה-Dispatcher שדואג שהפעולות ישוגרו בסדר הגיוני ולא בו זמנית. רכיבי ה-React עצמם מאזינים לשינויים ב-stores, ומעדכנים את ה-state של עצמם בהתאם.

אם אתם לא סגורים על מה שקראתם עכשיו, אני ממליץ בחום על ההרצאה הקצרה הזאת של Jing Chen לפני שאתם ממשיכים לקרוא את הפוסט.

חשוב לציין ש-Flux עצמה היא רק design pattern, ולא תשתית קוד. למעשה הקוד היחיד המסופק ע"י פייסבוק הוא ה-Dispatcher, כל השאר הן רק מוסכמות. ולמרות ש-Flux חדשנית ומשתלבת יפה עם React, היא עדיין מרגישה מעט בוסרית ולא חלקה כמו שהיינו מצפים. לדוגמה, בכל רכיב (Component) שצריך גישה למידע ב-stores, אנחנו צריכים להירשם לקבלת עדכונים, ולעדכן את ה-state בכל שינוי:

function getTodoState() {
    return { allTodos: TodoStore.getAll() };
}

var TodoApp = React.createClass({
    getInitialState: function() {
        return getTodoState();
    },
    componentDidMount: function() {
        TodoStore.addChangeListener(this._onChange);
    },
    componentWillUnmount: function() {
        TodoStore.removeChangeListener(this._onChange);
    },
    _onChange: function() {
        this.setState(getTodoState());
    }
};

כמה boilerplate! וזה עוד לפני שהתחלנו לממש את render… הדוגמה הזאת (שהיא אגב, דוגמה רשמית של Flux) גם מעלה שאלות שהתשובות עליהן לא פשוטות בכלל:

  1. מה עם רכיבים ילדים? האם לחלחל אליהם את ה-state ידנית או שגם הם יאזינו ל-store?
  2. מה קורה אם השינוי ב-store לא היה רלוונטי לרכיב? האם להתעלם ולרנדר? אולי לבדוק את השינוי? אולי PureRenderMixin?
  3. מה קורה אם יש שני stores? צריך להירשם לשניהם? או אולי לאחד אותם? ומה אם הם תלויים אחד בשני?
  4. איך בוחרים איזה מידע ישב ב-store ואיזה ב-state של רכיבי ה-React עצמם?

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

Redux להצלה

אז למרות ש-Flux הוא לא הדבר הכי נוח בעולם, העיקרון של state יחיד גלובאלי המשיך להתפתח בקהילת הקוד הפתוח, ובמהרה החלו לצוץ חלופות רבות כגון: ReFlux, Fluxible, Flummox, Fluxette (נשבע לכם שאני לא ממציא) ו-Alt. אך נראה שהמנצחת הגדולה בהתקוטטות הזאת היא ספריית Redux מאת Dan Abramov (נשמע ישראלי, אבל הוא לא).

הספרייה הזאת (וגם כמה אחרות) לוקחת כמה צעדים קדימה את העיקרון של Flux:

  1. ה-store הוא יחיד, והוא מפוצל לשני חלקים: אובייקט state יחיד לכל האפליקציה, ופונקצייה טהורה אחת בשם Reduce המקבלת את אובייקט ה-state הנוכחי ו-action כלשהו, ומחזירה state חדש.
  2. ה-state היחיד הוא היררכי, וניתן להרכיב (לשלב) Reducers שונים כך שכל אחד יטפל בחלק אחר של ה-state. כך ניתן לפתח בנפרד חלקים שונים של האפליקציה.
  3. רכיבי React לא צריכים להכיל state בכלל. כל המידע שלהם צריך להיות מסופק בתור props מרכיב האבא ו/או מה-state הראשי.

שימו לב שהסעיף הראשון פותר בנקל את בעיית הרינדור בשרת, מכיוון שכעת ה-state הוא יחיד, גלובאלי, ואפשר לטעון אותו בתחילת כל בקשת HTTP ולשמור אותו חזרה בסופה, למשל בתור JSON.

בנוסף לשינוי בארכיטקטורה, הספריה מספקת מספר פונקציות בסיסיות, שביחד עם כמה מוסכמות, הופכות כל רכיב React לישות המורכבת (בגדול) מ-3 חלקים:

  1. פונקציית render, כמו שאנחנו כבר מכירים, שהיא לרוב פונקצייה טהורה, כלומר דטרמיניסטית ותלוייה אך ורק בקלט שלה ולא בשום מידע חיצוני.
  2. פונקציית mapStateToProps, שכשמה כן היא, מקבלת את ה-state הגלובאלי (ולעתים גם props מרכיב האבא) ומייצרת את ה-props הסופיים של הרכיב.
  3. אובייקט או פונקציה בשם mapDispatchToProps הממפה בין props של הרכיב ל-Action Creators, שהן פונקציות המחזירות אובייקט action אותו יש להעביר ל-reducer ובכך לשנות את ה-state.

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

ולמה זה טוב יותר?

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

יתרון נוסף של Redux הוא נוחות כתיבת בדיקות. מכיוון שה-Reducers הן פונקציות טהורות, הן צפויות לחלוטין ולכן קל מאוד לכתוב להן בדיקות יחידה המקבלות state ו-action, ומוודאות שה-state החדש הוא מה שציפינו. באופן זה אנו מפרידים לחלוטין את כל בדיקות ה-business logic מבדיקות ה-UI.

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

אוקיי, אני רוצה Redux, איך מתחילים?

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

אני שוב ממליץ בחום לצפות בסרטוני ההדרכה, ובסופם אתם כבר אמורים לדעת איך להתחיל.

רוצים ללמוד עוד? בבקשה:

לקריאה נוספת