fa
Feedback
ToCode

ToCode

رفتن به کانال در Telegram

טיפים קצרים למתכנתים מאת ינון פרק

نمایش بیشتر
1 419
مشترکین
اطلاعاتی وجود ندارد24 ساعت
اطلاعاتی وجود ندارد7 روز
اطلاعاتی وجود ندارد30 روز
آرشیو پست ها
ToCode
1 419
Untitled</a> by Ynon Perek (<a href="https://codepen.io/ynonp">@ynonp</a>) on <a href="https://codepen.io">CodePen</a>. </iframe>

ToCode
1 419
# טיפ JavaScript: שמירת מידע לקבצים וטעינה חזרה יישומי Front End מאפשרים למשתמשים להכניס מידע. הרבה פעמים אנחנו שומרים את המידע בשרת אבל לפעמים אין לנו שרת או שהמידע לא מתאים לשמירה בבסיס הנתונים ואז היינו רוצים לתת את המידע למשתמש בתור קובץ. בדוגמה זו אראה איך לממש טופס עם שני כפתורים - כפתור אחד "מוריד" את המידע שבטופס לקובץ JSON על מחשב המשתמש וכפתור שני "מעלה" קובץ JSON מהמחשב כדי למלא ממנו את הטופס. ## קוד ה HTML קוד ה HTML של הדוגמה כולל טופס עם 3 שדות. לכל שדה מוגדר name שיהיה המפתח ב JSON, והערך יהיה הערך שהמשתמש הכניס לשדה. זה הקוד:
<div>
  <button id="exportButton">Export to JSON</button>
  <button id="importButton">Import From JSON</button>
</div>
<hr />
<form>  
  <div>
  <label>Name: </label>
  <input type="text" name="name" />
  </div>
  
  <div>
    <label>Phone Number: </label>
    <input type="tel" name="tel" />
  </div>
  <div>
    <label>Email:</label>
    <input type="email" name="email" />
  </div>
</form>
## שמירת המידע לקובץ בשביל לקחת את תוכן השדות לקובץ אצלי על המחשב אני צריך שני דברים: קודם כל אני צריך להעביר את כל המידע מהטופס לאוביקט JSON, ואחר כך צריך "להוריד" את אוביקט ה JSON בתור קובץ. בשביל החלק הראשון קל להשתמש ב FormData:
const form = document.querySelector('form');
const fd = new FormData(form);
const data = JSON.stringify(Object.fromEntries(fd));
אחרי זה אני יוצר אלמנט "קישור" שיוריד את הקובץ, לוחץ עליו ומוחק אותו מהמסך:
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(data);
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href",     dataStr);
downloadAnchorNode.setAttribute("download", "form.json");
  document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
קוד הפונקציה המלא הוא לכן:
function download() {
  const form = document.querySelector('form');
  const fd = new FormData(form);
  const data = JSON.stringify(Object.fromEntries(fd));

  const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(data);
  const downloadAnchorNode = document.createElement('a');
  downloadAnchorNode.setAttribute("href",     dataStr);
  downloadAnchorNode.setAttribute("download", "form.json");
  document.body.appendChild(downloadAnchorNode); // required for firefox
  downloadAnchorNode.click();
  downloadAnchorNode.remove();  
}
## העלאת הקובץ ועדכון הטופס החלק השני הוא לתת למשתמש אפשרות להעלות קובץ ולעדכן את הטופס בהתאם לפרטים שבקובץ. פה אנחנו נעשה תהליך הפוך - קודם ניצור אלמנט input שמיועד להעלאת קבצים, נלחץ עליו כדי לפתוח את הדיאלוג של הדפדפן לבחירת קובץ, נקרא את תוכן הקובץ, נהפוך את התוכן ל JSON ואז נשתמש בקוד JavaScript כדי לעדכן את השדות שבטופס מתוך הערכים שקראנו מה JSON. סך הכל הפונקציה היא:
function upload() {
  const form = document.querySelector('form');
  const input = document.createElement('input');
  input.type = 'file';

  input.onchange = e => { 
    const file = e.target.files[0]; 

    const reader = new FileReader();
    reader.readAsText(file,'UTF-8');

    reader.onload = readerEvent => {
      const content = JSON.parse(readerEvent.target.result); // this is the content!
      console.log(typeof content);
      for (const key of Object.keys(content)) {
        console.log(key);
        form.querySelector(`input[name="${key}"]`).value = content[key];
      }
    }
  }

  input.click();
}
בקישור הבא תוכלו למצוא את כל הקוד בקודפן עובד: https://codepen.io/ynonp/pen/XWZbpoK או אם אתם קוראים את זה מהדפדפן אז הנה הוא בהטמעה: <iframe height="300" style="width: 100%;" scrolling="no" title="Untitled" src="https://codepen.io/ynonp/embed/XWZbpoK?default-tab=html%2Cresult" frameborder="no" loading="lazy" allowtransparency="true" allowfullscreen="true"> See the Pen <a href="https://codepen.io/ynonp/pen/XWZbpoK">

ToCode
1 419
# חצי שעה ביום בחצי שעה ביום אפשר ללמוד שפה חדשה (או שפת תכנות חדשה). בחצי שעה ביום אפשר לכתוב פרויקט צד, כולל כזה שמכניס כסף או עושה רושם טוב על חברים. בחצי שעה ביום אפשר ללמוד לעצב, גם אם כרגע זה נראה שאין לך כישרון. בחצי שעה ביום אפשר להיכנס לכושר ולהרגיש מלאי אנרגיה כל השבוע. ובטח לכם יש עוד אינסוף רעיונות של דברים שהייתם רוצים לעשות. הבעיה עם החצי שעה ביום הזאת זה שחייבים להתמיד. זה לא עוזר לעשות ספורט שבועיים ואז לעזוב את זה, או ללמוד פייתון חודש ואז לא לגעת בזה יותר. הערך של "חצי שעה ביום" מגיע בטווח הרחוק, אחרי חודשים או שנים. אבל לחצי שעה ביום של עבודה לטווח רחוק כן יש ערך מיידי - היא מספקת לנו תחושת משמעות, גאווה, ביטחון עצמי וגם אושר. חצי שעה ביום לטווח הרחוק זאת החצי שעה שיש לכם בשבילכם. אל תמהרו לוותר עליה.

ToCode
1 419
# להבין שיש בעיה אחד הזכרונות הראשונים שלי מעבודה אמיתית בתכנות הוא שאחרי שסיימתי איזו משימה הייתי צריך לעדכן את האינדנטציה של הקוד למבנה אחר מזה שהעורך שלי היה מכוון אליו. שיניתי את הגדרות העורך ועברתי לפתוח קובץ קובץ ולשמור אותו מחדש עם האינדנטציה הנכונה. חברה שראתה אותי מעבר לכתף עושה את העבודה הסיזיפית הזו הציעה שאכתוב סקריפט במקום והראתה לי איך להשתמש ב perl. לקח לנו שעות לכתוב את הסקריפט שיעבוד כמו שצריך, הרבה יותר זמן ממה שהיה לוקח לי לסיים לעבור על כל הקבצים ידנית. ולקח לי שבועות להבין שעשינו את הדבר הנכון - כי בפעם הבאה שהיה צריך לתקן אינדנטציה כבר היו לנו את הכלים לעבוד מהר. הרבה פעמים לוקח זמן לזהות שהיתה בעיה בשיטת העבודה שלנו ושיש דרך טובה יותר. לאורך זמן, כשאנחנו נשארים עם הכלים ושיטות העבודה הפחות טובים, רק כי זה מה שאנחנו מכירים, אנחנו עושים נזק לעצמנו ולארגון: 1. אנחנו נכתוב בדיקות בצורה מסוימת, בטוחים שזאת הדרך היחידה או הטובה ביותר לכתוב בדיקות, למרות שלא מקבלים מהן את ה ROI הטוב ביותר האפשרי. כשמישהו יגיד משהו נמהר להסביר למה הבדיקות שלנו הן קריטיות להצלחת המערכת. 2. אנחנו נשתמש בכלים בצורה מסוימת, בטוחים שככה צריך לעבוד איתם, למרות שאנשים אחרים משתמשים בצורה יותר יעילה באותם כלים. כשהם יראו לנו את הדרך שלהם נמהר להסביר למה הם טועים ולמה השיטה שלהם לא מתאימה לנו. 3. אנחנו נבחר בטכנולוגיה מסוימת ונעקם אותה כדי שתתאים לצרכים שלנו, למרות שטכנולוגיה אחרת היתה יכולה לעבוד הרבה יותר טוב. אם משהו לא עובד, אם אתם רואים שאתם עובדים קשה מדי בשביל לקבל תוצאה שאחרים מקבלים יותר בקלות, אם אתם בטוחים שאצלכם בצוות מתמודדים עם בעיות שאף אחד לא התמודד איתן בעבר - יש סיכוי לא רע שאתם תקועים בבור שחפרתם לעצמכם. הדרך לצאת ממנו היא קודם כל לראות אותו. להבין שחלק מהאילוצים שלכם הם פיקטיביים או שאפשר לשנות אותם, וששווה לתת הזדמנות לגישה אחרת ולתת לה את הזמן כדי לראות תוצאות. שימוש יעיל בכלים אחרים לוקח זמן, אבל זה כמו לרכב על אופניים עם גלגלי עזר. הורדת גלגלי העזר, גם אם כרגע נראית הרסנית, מהר מאוד תפתח לכם אפשרויות חדשות להתקדם.

ToCode
1 419
    runner.start(cmd);
    form.current.querySelector('input').value = '';
  }

  return (
    <div className="App">
      <p>{JSON.stringify(runner.lines)}</p>
      <form onSubmit={start} ref={form}>
        <label>
          Command:
          <input type="text" />
        </label>
        <button>Start</button>
      </form>
      <Output />
    </div>
  )
});

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

ToCode
1 419
# בואו נכתוב מסוף עם Python, React ו MobX התמיכה המצוינת של פייתון ב Socket.IO מאפשרת לנו במעט מאוד קוד לכתוב ממשקים גרפיים לכלי שורת פקודה. בדוגמה של היום אני רוצה לכתוב ממשק גרפי להרצה של פקודות על מכונה מרוחקת. יהיה לנו שרת פייתון שמקבל הודעות ומריץ פקודות, את כל הפלט של הפקודה הוא שולח ב Web Socket ליישום צד לקוח להצגה על המסך. ## קוד צד שרת - פייתון ו Socket.IO נתקין את הספריה python-socketio וגם את aiohttp, שתיהן עם pip:
$ pip install aiohttp python-socketio
ואנחנו מוכנים לכתוב קוד צד שרת. בשביל שרת Socket.IO בפייתון מספיק לכתוב פונקציה לכל הודעה שאנחנו צריכים לקבל ולסמן אותה עם ה Decorator המתאים. אני רוצה לקבל הודעה שנקראת start ושיהיה לה פרמטר שהוא הטקסט של שורת הפקודה להרצה. פייתון יריץ את הפקודה ויקרא שורות מ stdout, וכל שורה שהוא קורא הוא ישלח חזרה לצד לקוח. בשביל זה מספיק לי קובץ פייתון אחד אני קורא לו server.py עם התוכן הבא:
from aiohttp import web
import asyncio
import socketio

sio = socketio.AsyncServer(cors_allowed_origins='*', aync_mode='aiohttp')
app = web.Application()

sio.attach(app)

@sio.on('start')
async def start_command(sid, cmd):
    print("Socket ID: " , sid)
    proc = await asyncio.create_subprocess_shell(
        cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE)

    while not proc.stdout.at_eof():
        next_line = await proc.stdout.readline()
        await sio.emit('output', next_line, to=sid)

    await proc.wait()

if __name__ == '__main__':
    web.run_app(app)
שתי פונקציות התקשורת כאן הן sio.on שמחברת קוד טיפול באירוע לאירוע עם שם מסוים (בדוגמה שלנו start) ו sio.emit ששולחת אירוע ללקוח. אני משתמש בפרמטר to כדי לשלוח את הפלט רק ללקוח שהריץ את הפקודה. ## קוד צד לקוח - מובאקס בצד הלקוח אני רוצה לכתוב מחלקה של לוגיקה שיודעת להריץ פקודות ולשמור את הפלט שלהן. יהיה לי נוח לכתוב את הלוגיקה במובאקס עם מערך ששומר את כל שורות הפלט. כל פעם שמגיעה שורת פלט חדשה נכתוב אותה למערך, וכשמריצים פקודה חדשה נרוקן את המערך. את הקוד כתבתי בקובץ בשם runner.js בתוך תיקיה בשם mobx. זה תוכן הקובץ:
import { observable, makeObservable, action } from 'mobx';
import { io } from "socket.io-client";

const socket = io("http://localhost:8080");

const enc = new TextDecoder();


class Runner {
  constructor() {
    this.output = [];
    makeObservable(this, {
      clear: action.bound,
      append: action.bound,
      output: observable,
    });
  }

  append(msg) {
    const msgText = enc.decode(msg);
    this.output.push(msgText);
  }

  clear() {
    this.output = [];
  }

  start(cmd) {
    this.clear();
    socket.emit('start', cmd);
  }
}

const runner = new Runner();
socket.on('output', (msg) => {
  runner.append(msg);
});
export default runner;
הפונקציה היחידה ששווה להתעכב עליה כאן היא append וספציפית על שורת ההמרה בה:
const enc = new TextDecoder();

// ...
const msgText = enc.decode(msg);
אנחנו מקבלים הודעות מפייתון בתור רצפים של בתים ולא בתור מחרוזות, בגלל שככה עובד ה API של הרצת פקודות מערכת בפייתון. ב JavaScript, צריך לתרגם את אוסף הבתים הזה למחרוזת וזה התפקיד של TextDecoder. אפשר היה גם לעשות את ההמרה בפייתון לפני השליחה אם אתם מרגישים יותר נוח שם. ## הצגת הפלט בממשק ריאקט והחלק האחרון הוא קוד ריאקט שמציג תיבה לכתיבת פקודה וקופסה שבה מוצג כל הפלט. משתמש כותב פקודה בתיבה, שרת פייתון מריץ אותה ושולח את הפלט חזרה והשתמש מקבל את כל הפלט לתיבת ה output. זה הקוד בקובץ App.jsx:
import { useRef } from 'react'
import { observer } from 'mobx-react-lite';
import runner from './mobx/runner';
import './index.css';

const Output = observer(function Output() {
  return (
    <div className="output">
      <ul>
        {runner.output.map((line, index) => (
          <li key={index}><pre>{line}</pre></li>
        ))}
      </ul>
    </div>
  );
});

const App = observer(function App() {
  const form = useRef(null);
  function start(ev) {
    ev.preventDefault();
    const cmd = form.current.querySelector('input').value;

ToCode
1 419
בגירסא 18 של ריאקט, אם מוסיפים קריאה ל startTransition כמו שהוספתי בדוגמה, הדפדפן עוצר את הרינדור באמצע כדי לטפל בלחיצה על הכפתור. זה נותן לדפדפן הזדמנות לטפל באירוע mousemove באמצע ה render ולעדכן את המשתנה הגלובאלי. התוצאה היא ש-6 המופעים של MouseTracker לא מציגים את אותו ערך. צריך להגיד - הבעיה היא לא ש-6 הקומפוננטות מציגות טקסט שונה בזמנים שונים. לפעמים זה הגיוני שממשק יתעדכן בזמן אמת וחלקים מסוימים בו יתעדכנו יותר מהר מאחרים. הבעיה שלנו כאן היא שמבחינת המשתמש כל 6 המספרים מוצגים בדיוק באותו זמן, כלומר משתמש לוחץ על הכפתור לשינוי הערך של count, ריאקט מתחיל עבודה חישובית וכשהוא מסיים אותה הוא כותב את כל 6 המספרים למסך במכה אחת. במצב כזה זה לא הגיוני להציג ערך שונה בכל קומפוננטה. פיתרון? לא להשתמש במשתנים גלובאליים מתוך קומפוננטות אלא להוציא אותם החוצה ל Store מסודר, ולוודא שהספריה שאתם משתמשים בה לניהול הסטייט הגלובאלי משתמשת בפונקציה חדשה של ריאקט 18 בשם useSyncExternalStore. פונקציה זו בעצם לוקחת מה Store בתחילת ה render עותק של אותו משתנה גלובאלי ודואגת להעביר אותו ב render לכל הקומפוננטות שמסתמכות עליו. אם אתם סקרנים איך זה עובד מאחורי הקלעים שווה לצפות בהרצאה של דאישי.

ToCode
1 419
# מה זה Tearing ב React ולמה שיהיה לכם אכפת גירסה 18 של ריאקט הכניסה לשימוש מנגנון חדש שנקרא Concurrent Mode. אם אתם בונים אפליקציית ריאקט חדשה ב Vite או create-react-app, המנגנון מופעל כברירת מחדל. אם אתם משדרגים אפליקציה ישנה לריאקט 18 אתם תקבלו הודעה שמבקשת מכם להחליף את פקודת ה ReactDOM.render בפקודה בשם ReactDOM.createRoot כדי להפעיל את המנגנון. מנגנון ה Concurrent Mode אמור לעזור לפתור בעיות ביצועים שנובעות מ render-ים ארוכים מדי. מנגנון Concurrent Mode מוסיף פונקציה בשם startTransition שמאפשרת לנו לסמן שעדכון מסוים הוא חשוב וצריך להפסיק render-ים פחות חשובים ומהר מהר לעשות render חדש רק בשביל השינוי הזה. לכאורה פיצ'ר נחמד ולא מזיק שאפילו יכול לעזור לביצועים, אבל האמת קצת יותר מורכבת והיא עלולה לקפוץ עליכם בהפתעה. את המושג tearing בהקשר של בעיית UI לא מצאתי בתיעוד של ריאקט או בהכרזה על ריאקט 18, אלא רק מפוסטים אחרים שדיברו על הבעיה והרצאה מצוינת ביוטיוב של Daishi Kato. אם יש לכם 20 דקות פנויות שווה להקשיב לו: https://www.youtube.com/watch?v=oPfSC5bQPR8. בחזרה ל Tearing - בהקשר של ריאקט המושג מתאר מצב בו Concurrent Mode גורם ל UI להיות לא קונסיסטנטי בגלל שינוי משתנים גלובאליים. הבעיה היא כזאת: 1. יש לנו כמה קומפוננטות שמושפעות ממשתנה גלובאלי - זה יכול להיות ref, משתנה ששמור ב Store חיצוני, או אפילו התאריך או השעה הנוכחיים שנלקחים מהדפדפן. 2. באמצע שריאקט עושה render לקומפוננטות האלה, פתאום הוא מגלה שיש שינוי יותר דחוף כי מישהו קרא ל startTransition. אז ריאקט עוצר הכל והולך לעשות את ה render היותר דחוף שלו. בואו נגיד שהיו לי 6 קומפוננטות שמושפעות ממשתנה גלובאלי, וריאקט רינדר 3 מהן לפני שהיה צריך לעצור. 3. תוך כדי ה render היותר דחוף, הערך של המשתנה הגלובאלי משתנה. 4. כשריאקט חוזר לרנדר את 3 הקומפוננטות שנשארו, הוא מרנדר אותן לפי הערך החדש של אותו משתנה גלובאלי. 5. ריאקט מציג על המסך את כל 6 הקומפוננטות במכה אחת, למרות שהן רונדרו בזמנים שונים ועם ערכים שונים של המשתנה הגלובאלי. על המסך אנחנו רואים ממשק לא קונסיסטנטי. חצי מהממשק מתבסס על ערך ישן של המשתנה הגלובאלי, והחצי השני על הערך החדש. דאישי הראה בהרצאה דוגמה מעניינת, אחרי שצמצמתי אותה קצת הגעתי לקוד הזה שממחיש בדיוק את הבעיה:
import { useState, useTransition } from 'react'

let lastMouseX = 0;

window.addEventListener('mousemove', (e) => {
  lastMouseX = e.offsetX;
});

function MouseTracker() {
  const start = performance.now();
  while (performance.now() - start < 20);
  
  return (
    <p>{lastMouseX}</p>
  );
}

function App() {
  const [count, setCount] = useState(0)
  const [isPending, startTransition] = useTransition();

  return (
    <div className="App">
      <p>isPending = {JSON.stringify(isPending)}</p>
      <button onClick={() => {
        startTransition(() => {
          setCount(c => c + 1)
        });
      }}>{count}</button>
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
      <MouseTracker />
    </div>
  )
}

export default App
יש לנו משתנה גלובאלי בשם lastMouseX שהערך שלו משתנה כל פעם שמזיזים את העכבר. יש לנו גם 6 קומפוננטות מסוג MouseTracker שפשוט מציגות את הערך. בשביל שה render שלהן ייקח זמן ולריאקט תהיה הזדמנות לעשות כמה דברים במקביל הוספתי קוד של המתנה בתוך קוד הקומפוננטה. בעולם האמיתי זה יכול להיות חישוב ארוך או מסובך. בגירסאות ישנות של ריאקט לא משנה מה היינו עושים תמיד היינו מקבלים את אותו ערך של lastMouseX מופיע 6 פעמים. בזמן שריאקט עסוק ב render, הדפדפן לא מפעיל את ה callback שמטפל בטיפול באירוע. רק אחרי שה render מסתיים לדפדפן יש הזדמנות לעדכן את המשתנה הגלובאלי ואנחנו נקבל את הערך החדש בפעם הבאה שנלחץ על הכפתור שמרנדר מחדש את הקומפוננטה. בגירסא 18 של ריאקט לפני Concurrent Mode, או אם לא קוראים ל startTransition, ריאקט מבצע את ה render "במכה אחת" כמו בגירסאות הישנות, ושוב לדפדפן אין הזדמנות לשנות את הערך של המשתנה הגלובאלי בין רינדור של קומפוננטות.

ToCode
1 419
# קוד שאני לא רוצה לכתוב כתבתי השבוע קוד תקשורת צד לקוח מאפס, כלומר בלי ספריה כמו react-query או swr. הקוד משך מידע מהשרת וגם שמר אותו ב cache מקומי, הגביל את מספר התשובות ששמורות ב cache ואת הזמן בו שומרים אותן וטיפל בכל מיני מקרי קצה. ככל שהתקדמתי שורה ועוד שורה חשבתי - ״למה אני צריך לכתוב את זה? בטח מישהו כבר כתב את זה קודם״ ״חייבת להיות ספריה שעושה את זה... אני לא מאמין שאני מבזבז ככה את הזמן״ ״יו אני בטח מפספס פה מלא מקרי קצה, בחיים לא אצליח לתפוס ולתקן את כולם״ ״אין מצב שזה יעבוד כמו שצריך על כל הדפדפנים ואין סיכוי שאני מוצא זמן לבדוק את זה על כל מכשיר״ ״עוד פעם if ??? זה כבר החמישי שלי בפונקציה. למה כל כך הרבה דברים יכולים להשתבש!?״ וככל שאני חופר במחשבות האלה במקום לכתוב קוד הלכתי להסתכל בקוד של ספריות שעושות דברים דומים (כי נו, אין דבר שמעודד בזבוז זמן יותר מאשר קוד שאתה לא רוצה לכתוב). יכולים לנחש מה מצאתי שם? נכון, בדיוק את אותו קוד שאני לא רציתי לכתוב. את הטיפול באותם מקרי קצה, את הבדיקות ואת ה if-ים. הרצון לכתוב רק קוד "נקי" ו"יפה" הוא נחמד, אבל החיים הם לא ספר לימוד. הרבה מקטעי הקוד שמפעילים את החיים שלנו מלאים בטיפול באינסוף מקרי קצה, באינסוף "פלסטרים" שמתכנתים הדביקו כל פעם שנתקלו בבעיה חדשה או תיקנו באג חדש. וויתור על כתיבת קוד כזה, רק כי "חייבת להיות דרך טובה יותר", ושילוב ספריה חיצונית במקום לא הופך את המערכת לנקיה יותר - הוא פשוט מעביר את הלכלוך למקום שקשה לראות אותו. לפעמים, כשיש ספריה חיצונית טובה ומתוחזקת זה כדאי. כשאין אחת כזאת צריך גם קצת להתלכלך לפעמים. (נ.ב. הדרך היחידה לכתוב קוד שמטפל בהמון מקרי קצה בלי להשתגע נקראת בדיקות. בלעדיהן, סגירה של כל חור חדש פותחת 3 חורים קודמים שחשבתם שסגרתם).

ToCode
1 419
              Object.values(this.items),
              (i) => i.index
            );
            delete this.items[oldestItem.id];
          }
        });
      });
  }
ועכשיו גם ההכנסה וגם המחיקה מתבצעות בטרנזאקציה אחת ויובילו ל render יחיד. מוזמנים לראות את הקוד ולשחק איתו בקודסנדבוקס בקישור: https://codesandbox.io/s/distracted-gagarin-qve7lc?file=/src/mobx/swapi.js או בהטמעה כאן: <iframe src="https://codesandbox.io/embed/distracted-gagarin-qve7lc?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="distracted-gagarin-qve7lc" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe> ## סיכום - טיפים לשימוש ב MobX מובאקס הוא ספריה ריאקטיבית לניהול סטייט גלובאלי. כשמשתמשים בו נכון הוא יכול לעזור לנו לכתוב יישומי צד-לקוח נקיים וקלים לתחזוקה. לסיכום הפוסט אלה הנקודות המרכזיות שצריך לשים לב אליהן בעבודה עם מובאקס: 1. שימו לב לריאקטיביות. מהו משתנה ריאקטיבי, מתי הוא מתעדכן ומי עוקב אחריו. משתנים ריאקטיביים דורשים מעקב צמוד בהרבה ממשתנים רגילים כי הם כמו מנגנון אירועים מובנה בתוך המחלקה. 2. שימו לב מתי ניגשים למשתנים ריאקטיביים. כל גישה למשתנה ריאקטיבי יוצרת יחס של האזנה לשינויים במשתנה זה. 3. התחילו עם מובאקס ובניית כל הלוגיקה והפעולות במנותק מה UI, ורק אחר כך ממשו את ה UI בתור שכבה דקה של גישה למידע. בהשוואה הקלאסית בין רידאקס למובאקס אני לא חושב שמובאקס יותר פשוט מרידאקס או להיפך. הם פשוט שונים. לא משנה איזה מהשניים תבחרו, היכרות טובה עם שיטת העבודה עוזרת לכתוב קוד נקי ומדויק יותר.

ToCode
1 419
          delete this.items[oldestItem.id];
        }
      });
  }
}

export const charactersRepo = new Repo("https://swapi.dev/api/people");
הקובץ מייצא אוביקט Repo שמחובר ל REST Endpoint עבור דמויות ממלחמת הכוכבים. בצד של ריאקט אני רוצה לבנות קומפוננטה שמשתמשת בריפו שיצרתי כדי להציג מידע על דמויות ממלחמת הכוכבים. הקומפוננטה תחזיק משתנה סטייט שמייצג את הדמות שעכשיו מוצגת, כל פעם שהוא יתעדכן היא תבקש מריפו לבצע fetch למידע של אותה דמות, וכשהמידע יהיה מוכן הקומפוננטה תציג אותו. כך נראה הקוד:
const SWCharacter = observer(function SWCharacter() {
  const [id, setId] = useState("");
  const data = charactersRepo.items[id]?.data;

  useEffect(() => {
    if (id !== "") {
      charactersRepo.fetch(id);
    }
  }, [id]);

  console.log(`render items id = ${id}`);

  return (
    <div>
      <input type="number" value={id} onChange={(e) => setId(e.target.value)} />
      {data && (
        <div>
          <p>Name: {data.name}</p>
          <p>Birth Year: {data.birth_year}</p>
        </div>
      )}
    </div>
  );
});
החלק הראשון בקוד הקומפוננטה שקשור לריאקטיביות הוא השורה:
const data = charactersRepo.items[id]?.data;
שורה זו ניגשת לאוביקט items של הריפו במפתח id שלו. אם יש שם משהו היא תיקח את שדה data שלו, שזה המידע שלקחנו כבר מהשרת (אולי בתגובה לבקשה אחרת). יותר מעניין מה קורה אם זה לא קיים. במצב כזה אנחנו נקבל undefined, אבל מובאקס גם מסמן שניסינו לגשת לאוביקט במזהה id. אם בעתיד הערך במזהה הזה ישתנה, באופן אוטומטי הקומפוננטה תרונדר מחדש ותקבל הזדמנות נוספת לקחת את הערך החדש משם. החלק השני הוא האפקט:
useEffect(() => {
  if (id !== "") {
    charactersRepo.fetch(id);
  }
}, [id]);
כל פעם ש id משתנה והערך החדש אינו מחרוזת ריקה, תופעל הפונקציה fetch עם ה id החדש. ברגע ש fetch מסיימת ומעדכנת את items במפתח id, בצורה אוטומטית הקומפוננטה תתעדכן מחדש עם הערך שנשמר והכל פשוט יעבוד. יש רק בעיה אחת עם הקוד אותה אנחנו רואים בקונסול בהודעת האזהרה:
[MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed. Tried to modify: Repo@5.items.2? 
האזהרה אומרת שביצענו עדכון של משתנה ריאקטיבי מחוץ ל action. הגדרת action חשובה במובאקס בגלל אופטימיזציית ביצועים. בתוך פונקציה שמסומנת כ action כל השינויים במשתנה הריאקטיבי יבוצעו בטרנזאקציה יחידה ולא "ישודרו" החוצה עד שה Action מסתיים. בלי action, כל שינוי במשתנה ריאקטיבי אוטומטית יגרום ל render מחדש של הקומפוננטה, ואם יש לנו 5 פקודות שמשנות 5 מאפיינים שונים של אוביקט ריאקטיבי יחיד, אז קומפוננטה שמסתכלת על האוביקט הזה תתרנדר 5 פעמים. אבל רגע, הפונקציה שלי fetch דווקא כן הוגדרה כ action נכון? הנה הקריאה ל makeObservable:
makeObservable(this, {
  items: observable,
  fetch: action
});
כן טוב - הפונקציה fetch היא action, אבל הקוד שבתוך ה then של ההבטחות הוא כבר לא. כלומר בקריאה הזו:
fetch(`${this.endpoint}/${id}`)
  .then((res) => res.json())
  .then((data) => {
    this.items[id] = {
      id,
      data,
      index: ++this.index
    };

    if (Object.keys(this.items).length > MAX_CACHE_SIZE) {
      const oldestItem = _.minBy(Object.values(this.items), (i) => i.index);
      delete this.items[oldestItem.id];
    }
  });
שתי פונקציות ה Callback שמועברות לשני הבלוקים של then כבר אינן נחשבות בתור action. כשפונקציית ה Callback השניה משנה את this.items זה גורם לאזהרה שראינו. הפיתרון הוא לעטוף את החלק שמשנה משתנים ריאקטיביים בתוך קריאה ל runInAction, פונקציה של mobx שמקבלת פונקציה והופכת אותה ל action. הקוד המתוקן נראה כך:
  fetch(id) {
    if (this.items[id]) {
      return;
    }

    fetch(`${this.endpoint}/${id}`)
      .then((res) => res.json())
      .then((data) => {
        runInAction(() => {
          this.items[id] = {
            id,
            data,
            index: ++this.index
          };

          if (Object.keys(this.items).length > MAX_CACHE_SIZE) {
            const oldestItem = _.minBy(

ToCode
1 419
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="objective-fog-1rmocb" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe> ופה כבר הקוד נשבר. אתם יכולים ללחוץ על כפתור Shuffle ולראות שהרשימה מתעדכנת, אבל כשתנסו ללחוץ על כפתור פלוס 1 כדי להוסיף 1 לכל פריט ברשימה אנחנו מקבלים את הודעת ההדפסה ממובאקס, אבל ריאקט לא מתעדכן בשינוי. מה קורה כאן ואיך מתקנים? נתחיל בבעיה והיא הפונקציה RandomList:
function RandomList(props) {
  const { items, renderItem = defaultRenderItem } = props;
  return (
    <ul>
      {items.map((v, index) => (
        <li key={index}>{renderItem(v)}</li>
      ))}
    </ul>
  );
}
הפונקציה לא מוגדרת בתור observer ולכן לא מקשיבה לשינויים במשתנים הריאקטיביים שלה. מי שעובד עם מידע ריאקטיבי, שזה הפונקציה החיצונית App, מוגדר בתור observer אבל זה לא מספיק כי הוא מעביר את המידע הריאקטיבי שלו פנימה. הפעולה השניה, shuffle, דווקא כן עובדת בגלל שהיא משנה את המערך values עצמו. הקומפוננטה App מסתכלת על המערך values וכשהוא יוחלף בחדש גם היא תתרנדר מחדש. פעולת הפלוס היא הבעייתית כי היא משנה את האיברים בתוך המערך, ועליהם מי שמסתכל זה רק הקומפוננטה RandomList בפונקציית ה map שהיא מפעילה. הפיתרון הקל במצב כזה הוא להפוך את RandomList ל observer. אבל לא תמיד זה אפשרי כי אולי קיבלתם אותה מספריית קוד חיצונית. אם אתם לא יכולים לשנות את RandomList עצמה הפיתרון היותר גנרי מורכב משני חלקים: 1. אנחנו צריכים להפוך את הקוד של renderItem ל Observer. זה לא בעיה כי זה קוד שאנחנו מעבירים מבחוץ לתוך הקומפוננטה. 2. אנחנו צריכים לעדכן את Randomizer כך שישמור רשימה של אוביקטים ולא של מספרים. בצורה כזאת הגישה לאוביקט ריאקטיבי תחזור להיות מתוך קוד ריאקטיבי וכל השינויים יוצגו כמו שצריך על המסך. אפשר לראות את הפיתרון בקודסנדבוקס בקישור: https://codesandbox.io/s/gallant-parm-p1nwgm?file=/src/App.js או מוטמע כאן: <iframe src="https://codesandbox.io/embed/gallant-parm-p1nwgm?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="gallant-parm-p1nwgm" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe> ## מה עושים עם קוד אסינכרוני שמשנה מידע ריאקטיבי אם כל המידע שלנו נשמר במובאקס, רק הגיוני לנסות להשתמש בו גם בתור מנהל לבקשות הרשת שלנו. לפני שנגיע לדבר על Action-ים אסינכרוניים, בואו ניקח את הגישה הנאיבית ונכתוב קוד אסינכרוני במובאקס. בדוגמה הבאה אני רוצה לכתוב מערכת שמציגה מידע מתוך swapi כלומר מידע על דמויות, סרטים, חלליות ומה לא ממלחמת הכוכבים. אני מתחיל עם קלאס מובאקסי בשם Repo שמספק ייצוג צד-לקוח לטבלה. ל Repo יש Endpoint, שזה הנתיב בשרת ממנו הוא לוקח את הנתונים, ופונקציה בשם fetch שמקבלת מזהה כלשהו ושומרת את המידע שמתאים לו. הריפו מוגבל לשמירת 10 פריטים וכל הפריטים נשמרים בצורה ריאקטיבית. זה הקוד:
import _ from "lodash";
import { makeObservable, observable, action, runInAction } from "mobx";

const MAX_CACHE_SIZE = 10;

class Repo {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.index = 0;
    this.items = {};

    makeObservable(this, {
      items: observable,
      fetch: action
    });
  }

  fetch(id) {
    if (this.items[id]) {
      return;
    }

    fetch(`${this.endpoint}/${id}`)
      .then((res) => res.json())
      .then((data) => {
        this.items[id] = {
          id,
          data,
          index: ++this.index
        };

        if (Object.keys(this.items).length > MAX_CACHE_SIZE) {
          const oldestItem = _.minBy(Object.values(this.items), (i) => i.index);

ToCode
1 419
ביום רגיל הייתי משתמש ב bind מה constructor, אבל בגלל שאני קורא ל makeObservable גישה זו לא תעבוד. הפיתרון הוא לעדכן את מובאקס שאני רוצה לאפשר ללקוחות מחוץ למחלקה להשתמש בפונקציה ולקבל תמיד את הערך הנכון של this, וזאת המשמעות של action.bound. בחזרה לריאקט, נוכל לכתוב את הקוד הבא כדי להציג שלושה מוני לחיצות מסונכרנים - לחיצה על הכפתור בכל אחת מהקומפוננטות תעדכן את הערך בשתי האחרות:
import "./styles.css";
import { observer } from "mobx-react-lite";
import counter from "./mobx/counter";

const Counter = observer(function Counter() {
  return (
    <div>
      <p>Value = {counter.value}</p>
      <button onClick={counter.click}>+1</button>
    </div>
  );
});

export default function App() {
  return (
    <div className="App">
      <Counter />
      <Counter />
      <Counter />
    </div>
  );
}
וגם את הדמו הזה העליתי לקודסנדבוקס ויכולים לראות אותו בקישור: https://codesandbox.io/s/gifted-ride-qnoj92?file=/src/App.js או כאן למטה: <iframe src="https://codesandbox.io/embed/gifted-ride-qnoj92?fontsize=14&hidenavigation=1&theme=dark" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="gifted-ride-qnoj92" allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" ></iframe> ## הבעיה עם תבנית Render Props ואיך להתאים אותה לעבודה ריאקטיבית רוב הדברים שאנחנו הולכים לכתוב בריאקט יעבדו בלי בעיות מיוחדות. שיטת העבודה עם ריאקט ומובאקס תהיה לכתוב קודם את כל הלוגיקה והסטייט הגלובאלי בתוך מחלקות מובאקס, ואחר כך לחבר לזה ממשק משתמש באמצעות ריאקט. אבל כן יש מספר מקרים לא אינטואיטיביים בעבודה עם מובאקס ועליהם אני רוצה לדבר בחלקים הבאים של מדריך זה. המוקש הראשון בעבודה עם מובאקס וריאקט הוא תבנית Render Props. בתבנית זו יש לנו קומפוננטת ריאקט שמקבלת חלק מהלוגיקה של איך להציג מידע מקומפוננטה אחרת - למשל קומפוננטה של רשימה שמקבלת בתור פרמטר פונקציה שאחראית על רינדור פריט ברשימה. תבנית Render Props היא מבלבלת כי הקוד שמגיע מבחוץ, כברירת מחדל, לא מוגדר בתור Observer ולכן לפעמים שינויים לא ייקלטו. בואו נמחיש את הבאג עם תוכנית קטנה לניהול רשימת מספרים אקראיים. תחילה אני כותב מחלקה במובאקס שיודעת לייצר ולשמור 10 מספרים אקראיים:
import _ from "lodash";
import { makeObservable, observable, action } from "mobx";

class Randomizer {
  constructor() {
    this.shuffle();

    makeObservable(this, {
      values: observable,
      shuffle: action.bound,
      incAll: action.bound
    });
  }

  shuffle() {
    this.values = _.range(10).map((x) => _.random(1, 100));
  }

  incAll() {
    for (let i = 0; i < this.values.length; i++) {
      this.values[i] += 1;
    }
    console.log(this.values);
  }
}

const randomizer = new Randomizer();
export default randomizer;
עכשיו אני ממשיך לחבר אותה לקומפוננטות ריאקט עם הקוד הבא ב App.js:
import "./styles.css";
import { observer, Observer } from "mobx-react-lite";
import randomizer from "./mobx/randomizer";

function defaultRenderItem(value) {
  return <span>{value}</span>;
}

function RandomList(props) {
  const { items, renderItem = defaultRenderItem } = props;
  return (
    <ul>
      {items.map((v, index) => (
        <li key={index}>{renderItem(v)}</li>
      ))}
    </ul>
  );
}

export default observer(function App() {
  return (
    <div className="App">
      <button onClick={randomizer.shuffle}>Shuffle</button>
      <button onClick={randomizer.incAll}>+1 </button>
      <RandomList
        items={randomizer.values}
        renderItem={(item) => <span>{item}</span>}
      />
    </div>
  );
});
אפשר לראות גם אותו לייב בקודסנדבוקס כאן: <iframe src="https://codesandbox.io/embed/objective-fog-1rmocb?fontsize=14&hidenavigation=1&theme=dark"