פרויקט הריאקט השני שכל מתכנת חייב לבנות - רשימת משימות עם זיכרון אמיתי¶
אם עשיתם פרויקט ריאקט אחד ואתם לא בטוחים מה הבא - הנה התשובה. רשימת משימות (To-Do list) היא הפרויקט הכי מוכר בעולם הפרונט, ויש לזה סיבה טובה. היא נראית פשוטה, אבל ברגע שאתם מוסיפים דרישה אחת קטנה - "שהמשימות יישארו אחרי שרענתי את הדף" - היא הופכת לתרגיל אמיתי בניהול state ובאינטראקציה עם הדפדפן.
בפוסט הזה נעבור על המבנה המלא, כולל החלק שרוב המדריכים מדלגים עליו: איך לשמור את המידע כך שהוא לא נעלם.
למה זה הפרויקט השני הנכון¶
אחרי טוטוריאל ראשון אתם בדרך כלל יודעים להציג רשימה סטטית. הפרויקט הזה דורש מכם לנהל רשימה שמשתנה - הוספה, מחיקה, סימון כהושלם. זה בדיוק התרגול שמעביר אתכם מ"אני מכיר את הסינטקס" ל"אני יודע לחשוב במונחי state". וכשמוסיפים שמירה מקומית, אתם פוגשים לראשונה את הרעיון שריאקט לא חי בוואקום - יש לו דרכים לתקשר עם מה שקורה מחוץ לקומפוננטה.
מבנה הנתונים¶
לפני שכותבים קוד, תחליטו איך נראית "משימה" בודדת. בדרך כלל זה אובייקט עם שדות כמו:
השדה id קריטי - בלעדיו אי אפשר לדעת איזו משימה למחוק או לסמן כהושלמה כשמרנדרים רשימה עם map. הרבה מתחילים משתמשים באינדקס במערך בתור מזהה, וזה עובד עד שמוחקים פריט מהאמצע ופתאום הכל מתבלבל. עדיף מזהה ייחודי אמיתי, למשל עם Date.now() או ספרייה קטנה כמו uuid.
ניהול הרשימה עם useState¶
כל הרשימה חיה במשתנה state יחיד - מערך של אובייקטי משימות. שלוש הפעולות המרכזיות:
const addTask = (text) => {
setTasks([...tasks, { id: Date.now(), text, completed: false }]);
};
const toggleTask = (id) => {
setTasks(tasks.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t)));
};
const deleteTask = (id) => {
setTasks(tasks.filter((t) => t.id !== id));
};
שימו לב שאף פעם לא משנים את המערך הקיים ישירות - תמיד יוצרים מערך חדש עם spread, map או filter. זה לא סתם סגנון - ריאקט מזהה שינויים לפי השוואת רפרנס, ואם תשנו את המערך "במקום" (למשל עם push), ריאקט לא תמיד יבין שמשהו השתנה ולא ירנדר מחדש.
שמירה ב-localStorage¶
זה החלק שהופך את הפרויקט מתרגיל לכלי שימושי. localStorage הוא מקום אחסון בדפדפן שנשאר גם אחרי שסוגרים את הטאב. הטריק הוא לחבר אותו לריאקט בעזרת שני hooks:
useEffect(() => {
const saved = localStorage.getItem("tasks");
if (saved) setTasks(JSON.parse(saved));
}, []);
useEffect(() => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
ה-useEffect הראשון רץ פעם אחת בטעינת האפליקציה וטוען מידע שמור. השני רץ בכל פעם שהרשימה משתנה ושומר אותה מחדש. שני החוקים האלה ביחד הם בעצם דוגמה מושלמת לסנכרון בין state של ריאקט לבין מקור מידע חיצוני - עיקרון שתפגשו שוב כשתעבדו עם שרתים אמיתיים.
מכשול נפוץ - סדר הטעינה¶
טעות קלאסית היא לשכוח שה-useEffect שטוען מ-localStorage וה-useEffect ששומר אליו יכולים "להתנגש". אם לא שמים לב, ה-effect של השמירה עלול לרוץ לפני שהטעינה הראשונית הסתיימה, ולמחוק את המידע השמור בטעות עם מערך ריק. הפתרון הנפוץ הוא לוודא שיש שני useEffect נפרדים עם מערכי תלויות ברורים, בדיוק כמו בדוגמה למעלה.
הרחבות שכדאי לנסות¶
אחרי שהגרסה הבסיסית עובדת, נסו להוסיף: סינון לפי "הכל / פעיל / הושלם", עריכת טקסט של משימה קיימת, או מיון לפי תאריך יצירה. כל תוספת כזו מתרגלת סוג אחר של ניהול state, וזה בדיוק מה שמכין אתכם לפרויקטים גדולים יותר.
אם אתם רוצים להבין לעומק את ההיגיון שמאחורי useState, useEffect ועדכון immutable של מערכים לפני שאתם ניגשים לפרויקט, זה בדיוק החומר שמלמדים בקורס פיתוח צד לקוח - בחינם ובעברית, עם תרגילים בכל שלב.
סיכום¶
רשימת משימות עם שמירה מקומית היא לא פרויקט "ילדותי" - היא מכילה בפנים כמעט את כל היסודות שתשתמשו בהם בכל אפליקציית ריאקט אמיתית. אם תבנו אותה בעצמכם, מההתחלה ועד הסוף, תצאו עם הבנה הרבה יותר יציבה מכל מדריך שרק צופים בו.
רוצים ללמוד את זה בצורה מסודרת, עם הסבר על כל hook ולמה הוא קיים? מוזמנים להתחיל בקורס פיתוח צד לקוח.
ואם משהו לא עובד באמצע הדרך - יש קהילה שתשמח לעזור.