• בלוג
  • טיפ ריסלקט - שימו לב כשאתם מפעילים filter או map בתוך Selector

טיפ ריסלקט - שימו לב כשאתם מפעילים filter או map בתוך Selector

06/12/2022

ספריית Reselect עוזרת לנו לבנות פונקציות שמושכות מידע מ Redux Store. ריסלקט מגיעה כחלק מספריית Redux Toolkit ובשביל להשתמש בה בפרויקט אנחנו צריכים רק לייבא את הפונקציה המרכזית שלה createSelector:

import { createSelector } from "@reduxjs/toolkit";

בשביל להבין למה צריך את createSelector נצטרך להיזכר איך עובדת הפונקציה useSelector בה אנחנו משתמשים בתוך קומפוננטות:

  1. בתוך קוד קומפוננטה אני קורא ל useSelector כדי "לחבר" בין קוד הקומפוננטה לנתיב מסוים באוביקט ה State ב Redux.

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

const todos = useSelector((state: AppState) => state.todos);

כדי לגשת לכל מערך ה todos שיש בסטייט.

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

  2. לפעמים אני רוצה להפעיל חישובים מורכבים בתוך useSelector. במצב כזה אולי לא משתלם להפעיל את כל החישוב מחדש, במיוחד אם אני יודע במה החישוב תלוי. לדוגמה נתבונן ב Selector הבא:

const todosCount = useSelector((state: AppState) => state.todos.length);

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

  1. הספריה Reselect מאפשרת לנו לתאר את הקשר בין Selector-ים שונים, וכך לחסוך חישובים מיותרים. סלקטור יחושב מחדש רק אם חלק מהתלויות שלו השתנו.

בואו נראה עוד דוגמה קצת יותר מורכבת אבל עדיין בעולם של Todos:

import { createSelector } from "@reduxjs/toolkit";
import { AppState } from "./store";

const todos = (state: AppState) => state.todos;

export const todosThatStartWithA = createSelector(todos, (todos) =>
  todos.filter((t) => t.message.startsWith("a"))
);

export const finishedTodosThatStartWithA = createSelector(
  todosThatStartWithA,
  (todos) => todos.filter((t) => t.completed)
);

export const numberOfFinishedTodosThatStartWithA = createSelector(
  finishedTodosThatStartWithA,
  (finishedTodosThatStartWithA) => {
    console.log(`Recalculating the length`);
    return finishedTodosThatStartWithA.length;
  }
);

יש לי כל מיני שאלות שקשורות ל todos, למשל:

  1. מי ה todos במערך שההודעה שלהם מתחילה ב a ?

  2. מי ה todos במערך שההודעה שלהם מתחילה ב a, והם מסומנים בתור completed ?

  3. כמה todos יש במערך שגם ההודעה שלהם מתחילה ב a וגם מסומנים בתור completed ?

השימוש ב createSelector מספק דרך נוחה לשלב כמה Selectors, וגם לחשב מחדש כל Selector רק כשהתלויות שלו באמת משתנות. אבל בואו לא נתבלבל, אפילו Reselect אינו קוסם.

למרות שהכתיב נותן הרגשה כאילו ה Selectors תלויים אחד בשני, בפועל בגלל ששני ה Selectors שבאמצע משתמשים ב filter (ולכן מייצרים מערך חדש), מצב התלויות הוא שכל ה Selectors תלויים ב state.todos. ברגע שמערך ה todos משתנה מכל סיבה שהיא, כל ה Selectors יחושבו מחדש, כי todosThatStartWithA ו finishedTodosThatStartWithA תמיד מחזירים מערך חדש. אם נריץ את הקוד נגלה שכל שינוי ב todos, גם אם ה todos שהשתנו לא מתחילים ב a, עדיין גרם לחישוב מחדש של ה Selector האחרון.

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