• בלוג
  • ניסוי Valtio (או: לא מבין את ההתלהבות מזוסטנד)

ניסוי Valtio (או: לא מבין את ההתלהבות מזוסטנד)

18/05/2024

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

import { proxy, useSnapshot } from 'valtio'

const state = proxy({ count: 0, text: 'hello' })

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

function Counter() {
  const snap = useSnapshot(state)
  return (
    <div>
      {snap.count}
      <button onClick={() => ++state.count}>+1</button>
    </div>
  )
}

עכשיו אני יודע מה אתם חושבים - איך הקומפוננטה יודעת שמשהו השתנה בסטייט וצריך להתרנדר מחדש? ויותר ספציפית, איך הקומפוננטה יודעת שדווקא כש count משתנה צריך להתרנדר מחדש אבל כש text משתנה לא צריך כי הקומפוננטה לא כוללת אותו.

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

נראה את זה עובד? בשמחה. בניתי מערך דו מימדי של קופסאות ובכל קופסה יש מספר 1 או 0. כולם מתחילים עם 0, והמערך נשמר במשתנה state:

const state = proxy(new Array(10).fill(0).map(_ => new Array(10).fill(0)))

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

const Box = React.memo((props) => {
  const { i, j } = props;
  console.count(`Box: ${i} / ${j}`);
  const canvas = useSnapshot(state);
  const cell = canvas[i][j];
  
  const handleClick = React.useCallback((e) => {
    state[i][j] = state[i][j] === 0 ? 1 : 0;
  });
  
  return <div style={styleCell(cell)} onClick={handleClick}></div>
})

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

אפשר למצוא את הדוגמה המלאה בקודפן כאן: https://codepen.io/ynonp/pen/rNgOXwK?editors=1010

עכשיו רק בשביל השוואה כתבתי את אותו משחק בזוסטנד ושם קיבלתי store שנראה ככה:

const useStore = create((set) => ({
  data: new Array(10).fill(0).map(_ => new Array(10).fill(0)),
  toggle: (i, j) => set((state) => produce(state, draft => {
      draft.data[i][j] = draft.data[i][j] === 0 ? 1 : 0
  }))
}));

וגם בתוך הקומפוננטה הייתי צריך להתאמץ יותר בשביל להגיע לפונקציה:

const Box = React.memo((props) => {
  const { i, j } = props;
  const cell = useStore((state) => state.data[i][j]);
  const toggle = useStore((state) => state.toggle);

  console.count(`Box: ${i} / ${j}`);

  const handleClick = React.useCallback((e) => {
    toggle(i, j);
  });

  return <div style={styleCell(cell)} onClick={handleClick}></div>
})

זה הקודפן המלא עם זוסטנד: https://codepen.io/ynonp/pen/RwmrNXG?editors=1010

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