איתחול רידאקס ממידע שמגיע מהשרת

30/11/2022

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

החיסרון בגישה כזאת הוא שאחרי "התקנת" JSON חדש מהשרת, כל הקומפוננטות ירונדרו מחדש, גם אם החלק שלהן במידע לא השתנה. ב JavaScript כשאני מחליף את כל הסטייט באוביקט חדש, גם אם זהה, עדיין החלפתי את ה References ולכן ריאקט לא יכול לזהות שלא היה שינוי.

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

1. הקוד עם הבעיה

התוכנית שלנו היום מכילה קוד רידאקס שיודע לעדכן את הסטייט שלו דרך action:

export const slice = createSlice({
  name: 'items',
  initialState,
  reducers: {
    reset: (state, action) => {
      return action.payload;
    },
  },
})

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

const initialItems = [
  { id: 1, text: 'first item' },
  { id: 2, text: 'second item' },
  { id: 3, text: 'third item' },
  { id: 4, text: 'fourth item' },
];


function randomizeItems() {
  const item = _.sample(initialItems);
  return initialItems.map(i => (
    i.id === item.id ? { ...i, text: `${i.text} ${_.random(10)}`} : {...i }
  ));
}

יש קומפוננטה שיודעת להציג פריט מהרשימה, ועוד קומפוננטה שמציגה את כל ה-4:

const Item = React.memo(({ id }) => {
  console.count(`Item ${id}`);
  const me = useSelector(state => state.items.find(i => i.id === id));
  if (!me) {
    return <p>Missing item {JSON.stringify(id)}</p>
  }
  return (<p>{me.id} - {me.text}</p>);
});

function Main() {
  const items = useSelector(state => state.items);
  const dispatch = useDispatch();
  return (
    <>
      <button onClick={() => dispatch(slice.actions.reset({ items: randomizeItems() }))}>Randomize</button>
      {items.map(i => <Item id={i.id} key={i.id} />)}
    </>
  )
}

עכשיו בואו נראה מה קורה כאן:

  1. כשלוחצים על כפתור Randomize אנחנו מגרילים אוביקט סטייט חדש שמורכב מ-4 פריטים חדשים לגמרי. אותו דבר היה קורה אם הייתי מפעיל JSON.parse על אוביקט שמגיע מהשרת.

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

אתם יכולים לשחק עם הקוד והכפתור בקודסנדבוקס בקישור: https://codesandbox.io/s/damp-rain-q71w55?file=/src/App.js

2. איך לצמצם רנדרים

ברגע שאני מבין שה render-ים המיותרים הגיעו בגלל שרידאקס החליף את כל האוביקטים בחדשים, אני יכול לתקן את הבעיה ברמה של Redux. הספריה rfc6902 יודעת לזהות הבדלים בין שני אוביקטי JSON ולשנות אוביקט אחד כדי "להגיע" לשני, בצורה שהיא Mutable. כשאני שם את הקוד הזה בתוך immer אז יוחלפו רק החלקים שבאמת השתנו באוביקט הסטייט. אני מוסיף לסלייס את הפונקציה:

resetWithDiff: (state, action) =>{
  applyPatch(state, createPatch(state, action.payload));
},

ומייבא את שתי הפונקציות applyPatch ו createPatch מ rfc6902:

import {applyPatch, createPatch} from 'rfc6902'

ומוסיף כפתור לאפליקציה שישלח dispatch עם הפעולה החדשה:

function Main() {  
  const items = useSelector(state => state.items);
  const dispatch = useDispatch();
  return (
    <>
      <button onClick={() => dispatch(slice.actions.reset({ items: randomizeItems() }))}>Randomize</button>
      <button onClick={() => dispatch(slice.actions.resetWithDiff({ items: randomizeItems() }))}>Randomize With Diff</button>
      {items.map(i => <Item id={i.id} key={i.id} />)}
    </>
  )
}

שוב אתם יכולים לשחק עם הקוד באותו קודסנדבוקס בקישור: https://codesandbox.io/s/damp-rain-q71w55?file=/src/App.js

כשנלחץ על הכפתור החדש אנחנו רואים שקיבלנו הודעות render רק מהפריטים שבאמת השתנו (שני פריטים, אחד איבד את התוספת לטקסט והשני קיבל תוספת).

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