לדלג לתוכן

פרויקט ריאקט שמלמד סינון וחיפוש כמו שצריך - אפליקציית מתכונים

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

מה בונים

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

מבנה הנתונים

כל מתכון הוא אובייקט עם שדות ברורים:

{
  id: 1,
  title: "פסטה ברוטב עגבניות",
  category: "צמחוני",
  time: 25,
  ingredients: ["פסטה", "עגבניות", "שום", "בזיליקום"],
}

מערך של אובייקטים כאלה הוא מקור האמת (source of truth) של האפליקציה. כל מה שהמשתמש רואה - תוצאות חיפוש, תוצאות מסוננות - הוא בעצם תת-קבוצה של המערך הזה, לא מערך חדש שמנוהל בנפרד. זה עיקרון מרכזי: אל תשמרו state כפול לאותו מידע.

הקלט הנשלט לחיפוש

תיבת החיפוש היא controlled input קלאסי - הערך שלה נשמר במשתנה state, ובכל הקלדה מתעדכן:

const [searchTerm, setSearchTerm] = useState("");

<input
  value={searchTerm}
  onChange={(e) => setSearchTerm(e.target.value)}
  placeholder="חפשו מתכון או מרכיב..."
/>

לוגיקת הסינון - הלב של הפרויקט

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

const filteredRecipes = recipes.filter((recipe) => {
  const matchesSearch =
    recipe.title.includes(searchTerm) ||
    recipe.ingredients.some((i) => i.includes(searchTerm));
  const matchesCategory = category === "הכל" || recipe.category === category;
  const matchesTime = !maxTime || recipe.time <= maxTime;

  return matchesSearch && matchesCategory && matchesTime;
});

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

חשוב - לא לסנן עם state נוסף

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

קומפוננטת RecipeCard

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

שדרוג ל-API אמיתי

אחרי שהגרסה הסטטית עובדת, נסו להחליף את מקור הנתונים ב-fetch מ-API חינמי כמו TheMealDB. זה בדיוק אותו תרגיל שעברנו באפליקציית מזג האוויר - useEffect, מצב טעינה, טיפול בשגיאות - רק שהפעם אתם גם מפעילים עליו סינון וחיפוש בצד הלקוח. השילוב הזה - נתונים מהשרת פלוס לוגיקת סינון מקומית - הוא בדיוק מה שקורה כמעט בכל אפליקציית ווב אמיתית.

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

סיכום

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

רוצים ללמוד את זה מסודר, מההתחלה? קורס פיתוח צד לקוח מחכה לכם, בחינם ובעברית.

ואם משהו בסינון לא מסתדר - שאלו בקהילה, מישהו כבר נתקל בזה.

הצטרפו לקהילה בדיסקורד