איך לצמצם Boilerplate בקוד Redux באמצעות ES6 Proxy

02/08/2020

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

1. על מה אנחנו מדברים כשאנחנו אומרים Boilerplate

הקוד הבא לקוח מתוך דוגמת Redux מתוך תיעוד הספריה:

let nextTodoId = 0
export const addTodo = text => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})

export const setVisibilityFilter = filter => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})

export const toggleTodo = id => ({
  type: 'TOGGLE_TODO',
  id
})

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

ב Redux כל פעולה גלובאלית שאנחנו רוצים לעשות ביישום מיוצגת על ידי אוביקט Action. הקוד הוא מתוך הקובץ actions/index.js וכל מה שהוא עושה זה להגדיר פונקציות שיוצרות אוביקטים אלה. במילים אחרות בעולם מונחה עצמים רגיל בשביל להוסיף אוביקט Todo היינו יכולים לדמיין קוד כזה שפשוט מפעיל פונקציה:

todos.addTodo(input.value);

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

export const addTodo = text => ({
  type: 'ADD_TODO',
  text
})

dispatch(addTodo(input.value));

השורה האחרונה היא הגיונית וממש דומה לקוד הפעלת פונקציה פשוטה, אבל כל מה שלפניה נקרא Boilerplate. זה קוד שאני כותב אותו רק בשביל ש Redux יהיה שמח.

עכשיו אני יודע מה אתם חושבים - מה הבעיה לוותר על ה Action Creator ופשוט לכתוב קוד שנראה כך:

dispatch({ type: 'ADD_TODO', text: input.value });

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

2. איך ES6 Proxy יכולים לעזור

דרך אחת ליצור את ה Action Creators בצורה אוטומטית היא להשתמש ב ES6 Proxy. הרעיון הוא לתפוס כל קריאה לאוביקט ה Actions וליצור ממנה אוביקט בצורה דינמית. כך נראה הקוד:

function convertActionNameToType(actionName) {
  return actionName.replaceAll(/([A-Z])/g, "_$1").toUpperCase();
}

export const actions = new Proxy(
  {},
  {
    get: function(target, prop) {
      if (target[prop] === undefined)
        return function(args) {
          return {
            type: convertActionNameToType(prop),
            ...args
          };
        };
      else return target[prop];
    }
  }
);

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

const AddTodo = ({ dispatch }) => {
  let input;

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          if (!input.value.trim()) {
            return;
          }
          dispatch(actions.addTodo({ text: input.value, id: lastId++ }));
          input.value = "";
        }}
      >
        <input ref={node => (input = node)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

כמובן מה שנחמד שעכשיו כל פעם שאני צריך להוסיף Action חדש אני כבר לא צריך להוסיף עבורה Action Creator. כל מה שאני צריך הוא לכתוב את המימוש בתוך ה Reducer ולהתחיל להשתמש ב Action החדש שיצרתי. מצד שני אנחנו נשארים עם התבנית של קריאה לפונקציות בתוך ה Dispatch ואני לא צריך להגדיר לבד אוביקט עם type כל פעם שרוצה לקרוא ל Action.

בפוסטים הבאים בסידרה אני מתכנן לנסות לצמצם Boilerplates נוספים של Redux - למשל את קוד ה Reducer ואת ה mapStateToProps המפורסם.