• בלוג
  • טיפ ריאקט - שמירת סטייט גלובאלי בסיגנלים

טיפ ריאקט - שמירת סטייט גלובאלי בסיגנלים

30/04/2024

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

1. מה זה סיגנל

סיגנל הוא מידע שיודע לדווח לקוד שמסתכל עליו כשהוא השתנה. זה אוביקט שיש לו מאפיין value שמחזיק את הערך. בשביל לשפר ביצועים אפשר לשים סיגנל בתור ערך ב JSX ואז לא צריך לייצר את כל ה Virtual DOM של הקומפוננטה ובאופן אוטומטי preact מעדכן רק את הערך של הסיגנל, או לקחת את הערך שלו ואז הקומפוננטה תרונדר מחדש כשהערך משתנה.

זאת הדוגמה מתוך התיעוד שלהם:

import { signal } from "@preact/signals";

// Create a signal that can be subscribed to:
const count = signal(0);

function Counter() {
  // Accessing .value in a component automatically re-renders when it changes:
  const value = count.value;

  const increment = () => {
    // A signal is updated by assigning to the `.value` property:
    count.value++;
  }

  return (
    <div>
      <p>Count: {value}</p>
      <button onClick={increment}>click me</button>
    </div>
  );
}

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

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

2. הסיגנל והפונקציות

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

// file: state.tsx

import { signal } from "@preact/signals";

const palette = [
  '#ffffff', // White
  '#000000', // Black
  '#9bbc0f', // Lime Green
  '#8bac0f', // Olive Green
  '#306230', // Dark Green
  '#0f380f', // Darker Green
  '#34a853', // Green
  '#31a354', // Slightly Darker Green
];

const initialState = () => ({
  size: 8,
  data: new Array(8).fill(0).map((_, i) =>
    new Array(8).fill(0).map((_, j) => (
      signal(0)
    )))
})

export const picture = signal(initialState())

export function color(row: number, column: number) {
  const n = picture.value.data[row][column].value
  return palette[n % palette.length]
}

export function setColor(row: number, column: number, color: number) {
  picture.value.data[row][column].value = color;
}

export function clear() {
  picture.value = initialState()
}

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

3. קומפוננטת צייר

קומפוננטת הצייר פשוט מציגה את התמונה ומחברת כל פיקסל לפונקציית setColor שהגדרתי בסטייט:

import { color, setColor, picture } from './state.tsx'

export default function Painter() {  
  const size = picture.value.size;

  return (
    <div className="grid" style={{gridTemplateColumns: `repeat(${size}, minmax(0, 1rem))`}}>
    {
    new Array(size).fill(0).map((_, i) => (
      new Array(size).fill(0).map((_, j) => (
        <div
          className="h-4 w-auto border"
          style={{background: color(i, j)}}
          onClick={() => setColor(i, j, 1)}
        ></div>
      ))
    ))
    }
    </div>
  )
}

4. קומפוננטת כפתור מחיקה

קומפוננטת כפתור המחיקה מציגה כפתור בודד עבור מחיקה שמחובר לפונקציה clear שהגדרתי בסטייט:

import { clear } from './state.tsx';

export default function ControlPanel() {
  return (
    <div>
      <button onClick={clear}>Clear</button>
    </div>
  )
}

5. ריאקט

בשביל להפעיל את הסיגנלים האלה בריאקט צריך רק את החבילה המתאימה ולקרוא לעוד פונקציה. ברמת החבילות יש להתקין את signals-react ולהשתמש בה בייבוא. לדוגמה בקובץ state אני אשתמש בשורת הייבוא הבאה:

import { signal } from "@preact/signals-react";

כל שאר הקובץ נשאר ללא שינוי.

השינוי השני הוא בקומפוננטת הצייר, ובעצם בכל קומפוננטה שצריכה להקשיב לסיגנלים - עליי להוסיף קריאה לפונקציה useSignals של signals-react. הקומפוננטה Painter בגירסת ריאקט נראית לכן כך:

import { color, setColor, picture } from './state.js'
import { useSignals } from "@preact/signals-react/runtime";

export default function Painter() {  
  useSignals();
  const size = picture.value.size;
  console.count('render');
  return (
    <div style={{display: 'grid',
    gridTemplateColumns: `repeat(${size}, minmax(0, 1rem))`}}>
    {
    new Array(size).fill(0).map((_, i) => (
      new Array(size).fill(0).map((_, j) => (
        <div
          key={i * 8 + j}
          style={{background: color(i, j), height: '1rem', width: 'auto', border: '1px solid grey'}}
          onClick={() => setColor(i, j, 1)}
        ></div>
      ))
    ))
    }
    </div>
  )
}

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

6. קוד וזה

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

https://painter.deno.dev

ובגירסת ריאקט לפלייקוד כאן:

https://playcode.io/1853176

והקוד בגיטהאב:

https://github.com/ynonp/deno-painter-demo