6.6 רשימות ומפתחות פתרון
פתרון - רשימות ומפתחות¶
פתרון תרגיל 1¶
interface Student {
id: number;
name: string;
grade: number;
}
function StudentList() {
const students: Student[] = [
{ id: 1, name: "Alice", grade: 92 },
{ id: 2, name: "Bob", grade: 45 },
{ id: 3, name: "Charlie", grade: 78 },
{ id: 4, name: "Diana", grade: 50 },
{ id: 5, name: "Eve", grade: 88 },
];
return (
<table style={{ borderCollapse: "collapse", width: "100%" }}>
<thead>
<tr>
<th style={{ border: "1px solid #ddd", padding: "8px" }}>Name</th>
<th style={{ border: "1px solid #ddd", padding: "8px" }}>Grade</th>
<th style={{ border: "1px solid #ddd", padding: "8px" }}>Status</th>
</tr>
</thead>
<tbody>
{students.map((student) => (
<tr key={student.id}>
<td style={{ border: "1px solid #ddd", padding: "8px" }}>{student.name}</td>
<td style={{ border: "1px solid #ddd", padding: "8px" }}>{student.grade}</td>
<td
style={{
border: "1px solid #ddd",
padding: "8px",
color: student.grade >= 56 ? "green" : "red",
}}
>
{student.grade >= 56 ? "Pass" : "Fail"}
</td>
</tr>
))}
</tbody>
</table>
);
}
פתרון תרגיל 2¶
import { useState } from "react";
interface Student {
id: number;
name: string;
grade: number;
}
type SortBy = "name" | "grade" | "none";
function StudentList() {
const [showPassingOnly, setShowPassingOnly] = useState(false);
const [sortBy, setSortBy] = useState<SortBy>("none");
const allStudents: Student[] = [
{ id: 1, name: "Alice", grade: 92 },
{ id: 2, name: "Bob", grade: 45 },
{ id: 3, name: "Charlie", grade: 78 },
{ id: 4, name: "Diana", grade: 50 },
{ id: 5, name: "Eve", grade: 88 },
{ id: 6, name: "Frank", grade: 33 },
{ id: 7, name: "Grace", grade: 61 },
{ id: 8, name: "Hank", grade: 72 },
];
let displayed = showPassingOnly
? allStudents.filter((s) => s.grade >= 56)
: allStudents;
if (sortBy === "name") {
displayed = [...displayed].sort((a, b) => a.name.localeCompare(b.name));
} else if (sortBy === "grade") {
displayed = [...displayed].sort((a, b) => b.grade - a.grade);
}
return (
<div>
<div style={{ marginBottom: "12px" }}>
<button onClick={() => setShowPassingOnly(!showPassingOnly)}>
{showPassingOnly ? "Show All" : "Show Passing Only"}
</button>
<button onClick={() => setSortBy("name")}>Sort by Name</button>
<button onClick={() => setSortBy("grade")}>Sort by Grade</button>
</div>
<p>
Showing {displayed.length} of {allStudents.length}
</p>
<table style={{ borderCollapse: "collapse", width: "100%" }}>
<thead>
<tr>
<th style={{ border: "1px solid #ddd", padding: "8px" }}>Name</th>
<th style={{ border: "1px solid #ddd", padding: "8px" }}>Grade</th>
<th style={{ border: "1px solid #ddd", padding: "8px" }}>Status</th>
</tr>
</thead>
<tbody>
{displayed.map((student) => (
<tr key={student.id}>
<td style={{ border: "1px solid #ddd", padding: "8px" }}>{student.name}</td>
<td style={{ border: "1px solid #ddd", padding: "8px" }}>{student.grade}</td>
<td style={{ border: "1px solid #ddd", padding: "8px", color: student.grade >= 56 ? "green" : "red" }}>
{student.grade >= 56 ? "Pass" : "Fail"}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
נוצרים מערך חדש עם [...displayed].sort(...) כדי לא לשנות את המערך המקורי. הסינון והמיון נעשים לפני הרינדור - filter, sort, ואז map.
פתרון תרגיל 3¶
import { useState } from "react";
interface Book {
id: number;
title: string;
author: string;
year: number;
}
function BookList() {
const [books, setBooks] = useState<Book[]>([]);
const [nextId, setNextId] = useState(1);
const [title, setTitle] = useState("");
const [author, setAuthor] = useState("");
const [year, setYear] = useState("");
const handleAdd = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!title.trim() || !author.trim() || !year.trim()) return;
const newBook: Book = {
id: nextId,
title: title.trim(),
author: author.trim(),
year: Number(year),
};
setBooks([...books, newBook]);
setNextId(nextId + 1);
setTitle("");
setAuthor("");
setYear("");
};
const handleRemove = (id: number) => {
setBooks(books.filter((book) => book.id !== id));
};
const handleSort = () => {
setBooks([...books].sort((a, b) => a.year - b.year));
};
return (
<div>
<form onSubmit={handleAdd} style={{ marginBottom: "16px" }}>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Title" />
<input value={author} onChange={(e) => setAuthor(e.target.value)} placeholder="Author" />
<input value={year} onChange={(e) => setYear(e.target.value)} placeholder="Year" type="number" />
<button type="submit">Add Book</button>
</form>
<button onClick={handleSort}>Sort by Year</button>
<div>
{books.map((book) => (
<div key={book.id} style={{ border: "1px solid #ddd", padding: "12px", margin: "8px 0", borderRadius: "4px" }}>
<h3>{book.title}</h3>
<p>Author: {book.author}</p>
<p>Year: {book.year}</p>
<button onClick={() => handleRemove(book.id)}>Delete</button>
</div>
))}
</div>
</div>
);
}
ה-id נוצר עם מונה (nextId) שעולה בכל הוספה. ככה ה-id תמיד ייחודי גם אם מוחקים ומוסיפים פריטים.
פתרון תרגיל 4¶
interface CourseStudent {
id: number;
name: string;
}
interface Course {
id: number;
name: string;
instructor: string;
students: CourseStudent[];
}
function CourseList() {
const courses: Course[] = [
{
id: 1,
name: "React Fundamentals",
instructor: "Alice Johnson",
students: [
{ id: 1, name: "Bob" },
{ id: 2, name: "Charlie" },
{ id: 3, name: "Diana" },
],
},
{
id: 2,
name: "TypeScript Advanced",
instructor: "Eve Smith",
students: [
{ id: 4, name: "Frank" },
{ id: 5, name: "Grace" },
],
},
{
id: 3,
name: "Node.js Basics",
instructor: "Hank Brown",
students: [
{ id: 6, name: "Ivy" },
{ id: 7, name: "Jack" },
{ id: 8, name: "Kim" },
{ id: 9, name: "Leo" },
],
},
];
return (
<div>
{courses.map((course) => (
<div key={course.id} style={{ border: "1px solid #ddd", padding: "16px", margin: "12px 0", borderRadius: "8px" }}>
<h2>
{course.name} ({course.students.length} students)
</h2>
<p>Instructor: {course.instructor}</p>
<h4>Students:</h4>
<ul>
{course.students.map((student) => (
<li key={student.id}>{student.name}</li>
))}
</ul>
</div>
))}
</div>
);
}
שימו לב שכל רמה של map צריכה key משלה. ה-key של הסטודנט הוא ה-id שלו, לא אינדקס.
פתרון תרגיל 5¶
import { useState } from "react";
interface Country {
code: string;
name: string;
continent: string;
population: number;
}
function CountrySearch() {
const [search, setSearch] = useState("");
const [continent, setContinent] = useState("all");
const countries: Country[] = [
{ code: "IL", name: "Israel", continent: "Asia", population: 9_500_000 },
{ code: "US", name: "United States", continent: "North America", population: 331_000_000 },
{ code: "JP", name: "Japan", continent: "Asia", population: 125_800_000 },
{ code: "DE", name: "Germany", continent: "Europe", population: 83_200_000 },
{ code: "BR", name: "Brazil", continent: "South America", population: 213_000_000 },
{ code: "FR", name: "France", continent: "Europe", population: 67_400_000 },
{ code: "AU", name: "Australia", continent: "Oceania", population: 25_700_000 },
{ code: "NG", name: "Nigeria", continent: "Africa", population: 211_000_000 },
{ code: "IN", name: "India", continent: "Asia", population: 1_380_000_000 },
{ code: "CA", name: "Canada", continent: "North America", population: 38_000_000 },
];
const continents = ["all", ...new Set(countries.map((c) => c.continent))];
const filtered = countries
.filter((c) => c.name.toLowerCase().includes(search.toLowerCase()))
.filter((c) => continent === "all" || c.continent === continent);
return (
<div>
<div style={{ marginBottom: "16px" }}>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search country..."
style={{ padding: "8px", marginRight: "8px" }}
/>
<select value={continent} onChange={(e) => setContinent(e.target.value)}>
{continents.map((c) => (
<option key={c} value={c}>
{c === "all" ? "All Continents" : c}
</option>
))}
</select>
</div>
<p>{filtered.length} results</p>
{filtered.length === 0 ? (
<p>No results found</p>
) : (
filtered.map((country) => (
<div key={country.code} style={{ border: "1px solid #ddd", padding: "12px", margin: "8px 0", borderRadius: "4px" }}>
<h3>{country.name} ({country.code})</h3>
<p>Continent: {country.continent}</p>
<p>Population: {country.population.toLocaleString()}</p>
</div>
))
)}
</div>
);
}
הסינון מתבצע בשני שלבים - קודם לפי טקסט החיפוש, ואז לפי יבשת. שימו לב לשימוש ב-Set כדי לקבל את רשימת היבשות הייחודיות.
תשובות לשאלות¶
-
Math.random()יוצר key חדש בכל רינדור. זה גורם לריאקט לחשוב שכל הפריטים השתנו, ולכן היא מוחקת ויוצרת מחדש את כל הקומפוננטות בכל רינדור. זה פוגע בביצועים ומאבד סטייט פנימי של קומפוננטות (למשל ערכים ב-inputs). -
עם
key={user.id}, כשמוחקים את האיבר הראשון, ריאקט מזהה שה-id הזה נעלם ומוחקת רק אותו. שאר הקומפוננטות נשארות עם הסטייט שלהן. עםkey={index}, כל הפריטים "זזים" - מה שהיה באינדקס 1 הופך ל-0, מה שהיה ב-2 הופך ל-1 וכו'. ריאקט מעדכנת את כל הקומפוננטות כי ה-props השתנו עבור כל key, וה-input האחרון נמחק (כי יש key אחד פחות). -
ריאקט צריכה את ה-key ברמה של ה-map כדי לזהות את הפריטים ברשימה. אם שמים key על אלמנט פנימי, ריאקט לא יודעת לקשר בין הפריט הישן לחדש ומתייחסת לכל הרשימה כאילו היא השתנתה לגמרי.
-
כששני פריטים ברשימה חולקים key, ריאקט מציגה אזהרה בקונסולה. בפועל, ריאקט עלולה להתבלבל - היא עשויה לא לרנדר את אחד הפריטים, או להציג props לא נכונים.