6.9 חשיבה בריאקט הרצאה
חשיבה בריאקט - Thinking in React¶
עד עכשיו למדנו את אבני הבניין של ריאקט - קומפוננטות, props, סטייט, אירועים, רשימות, טפסים, ו-useEffect. עכשיו הזמן ללמוד איך לחשוב בריאקט - התהליך של בניית אפליקציה מאפס.
הגישה של ריאקט מבוססת על חמישה שלבים:
- פירוק ה-UI לקומפוננטות
- בניית גרסה סטטית
- זיהוי הסטייט המינימלי
- קביעת היכן הסטייט חי
- הוספת זרימת נתונים הפוכה
שלב 1 - פירוק ה-UI לקומפוננטות¶
הצעד הראשון הוא להסתכל על העיצוב (mockup) ולצייר מלבנים סביב כל קומפוננטה. כל מלבן הוא קומפוננטה פוטנציאלית.
כללים לפירוק:
- עיקרון האחריות הבודדת (Single Responsibility) - כל קומפוננטה עושה דבר אחד. אם היא גדלה, פצלו אותה
- התאמה לנתונים - קומפוננטות נוטות להתאים למבנה הנתונים. כל רמה בנתונים בדרך כלל מתאימה לקומפוננטה
- שימוש חוזר - אם חלק מופיע כמה פעמים, הוא קומפוננטה נפרדת
דוגמה - אפליקציית רשימת מוצרים:
כל רמה בעץ מתאימה לרמה בנתונים: הטבלה מכילה קטגוריות, כל קטגוריה מכילה מוצרים.
שלב 2 - בניית גרסה סטטית¶
בנו גרסה שמציגה את ה-UI בלי אינטראקטיביות. שלב זה דורש הרבה כתיבה ומעט חשיבה:
interface Product {
category: string;
price: string;
stocked: boolean;
name: string;
}
function ProductRow({ product }: { product: Product }) {
const nameStyle = product.stocked ? {} : { color: "red" };
return (
<tr>
<td style={nameStyle}>{product.name}</td>
<td>{product.price}</td>
</tr>
);
}
function ProductCategoryRow({ category }: { category: string }) {
return (
<tr>
<th colSpan={2}>{category}</th>
</tr>
);
}
function ProductTable({ products }: { products: Product[] }) {
const categories = [...new Set(products.map((p) => p.category))];
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{categories.map((category) => (
<React.Fragment key={category}>
<ProductCategoryRow category={category} />
{products
.filter((p) => p.category === category)
.map((product) => (
<ProductRow key={product.name} product={product} />
))}
</React.Fragment>
))}
</tbody>
</table>
);
}
function SearchBar() {
return (
<div>
<input type="text" placeholder="Search..." />
<label>
<input type="checkbox" /> Only show products in stock
</label>
</div>
);
}
function FilterableProductTable({ products }: { products: Product[] }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
);
}
כללים בגרסה הסטטית:
- השתמשו רק ב-props, לא ב-state
- העבירו נתונים רק מלמעלה למטה
- אל תדאגו לאינטראקטיביות - זה בא בשלב הבא
שלב 3 - זיהוי הסטייט המינימלי¶
עכשיו צריך לזהות מה צריך להיות סטייט. העיקרון: מצאו את הסט המינימלי של סטייט שמתאר את כל ה-UI.
שאלות לזיהוי סטייט:
- האם הערך משתנה לאורך זמן? אם לא, זה לא סטייט
- האם הערך מועבר מאב דרך props? אם כן, זה לא סטייט
- האם אפשר לחשב את הערך מסטייט או props קיימים? אם כן, זה לא סטייט
בדוגמה שלנו:
| נתון | סטייט? | למה? |
|---|---|---|
| רשימת המוצרים | לא | מגיע כ-props |
| טקסט החיפוש | כן | משתנה, לא ניתן לחישוב |
| ערך ה-checkbox | כן | משתנה, לא ניתן לחישוב |
| הרשימה המסוננת | לא | ניתן לחישוב מהמוצרים + החיפוש + ה-checkbox |
הסטייט המינימלי: טקסט חיפוש + ערך checkbox. זה הכל.
שלב 4 - היכן הסטייט חי¶
אחרי שזיהינו את הסטייט, צריך להחליט באיזו קומפוננטה הוא יחיה.
הכלל: הסטייט חי בקומפוננטת האב המשותפת הקרובה ביותר של כל הקומפוננטות שצריכות אותו.
בדוגמה:
- SearchBar צריכה את טקסט החיפוש ואת ה-checkbox (כדי להציג אותם)
- ProductTable צריכה את טקסט החיפוש ואת ה-checkbox (כדי לסנן)
- האב המשותף: FilterableProductTable
function FilterableProductTable({ products }: { products: Product[] }) {
const [filterText, setFilterText] = useState("");
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
/>
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly}
/>
</div>
);
}
שלב 5 - זרימת נתונים הפוכה - inverse data flow¶
הנתונים זורמים מלמעלה למטה דרך props. אבל SearchBar צריכה לשנות את הסטייט שחי ב-FilterableProductTable. הפתרון: העברת פונקציות callback כ-props:
interface SearchBarProps {
filterText: string;
inStockOnly: boolean;
onFilterTextChange: (text: string) => void;
onInStockOnlyChange: (checked: boolean) => void;
}
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange,
}: SearchBarProps) {
return (
<div>
<input
type="text"
value={filterText}
onChange={(e) => onFilterTextChange(e.target.value)}
placeholder="Search..."
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={(e) => onInStockOnlyChange(e.target.checked)}
/>
Only show products in stock
</label>
</div>
);
}
והאב מעביר את הפונקציות:
function FilterableProductTable({ products }: { products: Product[] }) {
const [filterText, setFilterText] = useState("");
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly}
/>
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly}
/>
</div>
);
}
דפוסי הרכבה - composition patterns¶
ילדים כ-props - children¶
במקום ליצור קומפוננטה שמכילה הכל, אפשר לתת לקומפוננטת האב להחליט מה להציג בפנים:
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div style={{ border: "1px solid #ddd", borderRadius: "8px", padding: "16px" }}>
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
// usage - the parent decides what goes inside
function App() {
return (
<Card title="User Info">
<p>Alice, age 30</p>
<button>Edit</button>
</Card>
);
}
פרופס כ-slots¶
כשצריכים יותר "חורים" מאשר רק children:
interface LayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
}
function Layout({ header, sidebar, children }: LayoutProps) {
return (
<div>
<header>{header}</header>
<div style={{ display: "flex" }}>
<aside style={{ width: "200px" }}>{sidebar}</aside>
<main style={{ flex: 1 }}>{children}</main>
</div>
</div>
);
}
function App() {
return (
<Layout
header={<h1>My App</h1>}
sidebar={<nav>Navigation here</nav>}
>
<p>Main content here</p>
</Layout>
);
}
הפרדת לוגיקה מ-UI¶
כשקומפוננטה מכילה גם לוגיקה מורכבת וגם UI, אפשר לפצל:
// logic
function useUsers() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
return { users, loading };
}
// UI
function UserList() {
const { users, loading } = useUsers();
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
זה נקרא custom hook - פונקציה שמתחילה ב-use ומכילה לוגיקה עם hooks. נלמד על זה יותר בהמשך.
טיפים מעשיים¶
מתי לפצל קומפוננטה¶
- כשהקומפוננטה עוברת 100+ שורות
- כשיש חלק שחוזר על עצמו
- כשיש חלק עם אחריות שונה (למשל header ו-content)
- כשרוצים לשתף חלק מה-UI בין מסכים
מתי לא לפצל¶
- כשהפיצול יוצר יותר מורכבות מאשר הוא פותר
- כשהחלק משמש רק במקום אחד ויש לו 20 שורות
- כשהפיצול ידרוש להעביר 10+ props
כללי אצבע לסטייט¶
- התחילו בלי סטייט. הוסיפו רק כשצריך אינטראקטיביות
- העדיפו חישוב על סטייט - אם אפשר לחשב ערך, אל תשמרו אותו בסטייט
- סטייט מינימלי - שמרו רק את מה שאי אפשר לחשב
- הרימו סטייט רק כשצריך - אם רק קומפוננטה אחת משתמשת, הסטייט נשאר שם
דוגמה מלאה - רשימת משימות¶
בואו ניישם את כל החמישה שלבים על אפליקציית משימות:
שלב 1 - קומפוננטות:
שלב 2 + 3 + 4 + 5 - מימוש מלא:
import { useState } from "react";
// types
interface Todo {
id: number;
text: string;
done: boolean;
}
type Filter = "all" | "active" | "completed";
// TodoItem
interface TodoItemProps {
todo: Todo;
onToggle: (id: number) => void;
onDelete: (id: number) => void;
}
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
return (
<li style={{ display: "flex", justifyContent: "space-between", padding: "8px" }}>
<label style={{ textDecoration: todo.done ? "line-through" : "none" }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</label>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
}
// AddTodoForm
interface AddTodoFormProps {
onAdd: (text: string) => void;
}
function AddTodoForm({ onAdd }: AddTodoFormProps) {
const [text, setText] = useState("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (text.trim()) {
onAdd(text.trim());
setText("");
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Add todo..."
/>
<button type="submit">Add</button>
</form>
);
}
// FilterBar
interface FilterBarProps {
filter: Filter;
onFilterChange: (filter: Filter) => void;
}
function FilterBar({ filter, onFilterChange }: FilterBarProps) {
const filters: Filter[] = ["all", "active", "completed"];
return (
<div>
{filters.map((f) => (
<button
key={f}
onClick={() => onFilterChange(f)}
style={{ fontWeight: filter === f ? "bold" : "normal" }}
>
{f}
</button>
))}
</div>
);
}
// TodoStats
function TodoStats({ todos }: { todos: Todo[] }) {
const total = todos.length;
const completed = todos.filter((t) => t.done).length;
const active = total - completed;
return (
<p>
{total} total, {active} active, {completed} completed
</p>
);
}
// TodoApp - the main component that holds all state
function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<Filter>("all");
const [nextId, setNextId] = useState(1);
const addTodo = (text: string) => {
setTodos([...todos, { id: nextId, text, done: false }]);
setNextId(nextId + 1);
};
const toggleTodo = (id: number) => {
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
};
const deleteTodo = (id: number) => {
setTodos(todos.filter((t) => t.id !== id));
};
// filtered list is computed, not state!
const filteredTodos = todos.filter((todo) => {
if (filter === "active") return !todo.done;
if (filter === "completed") return todo.done;
return true;
});
return (
<div style={{ maxWidth: "400px", margin: "0 auto" }}>
<h1>Todo App</h1>
<AddTodoForm onAdd={addTodo} />
<FilterBar filter={filter} onFilterChange={setFilter} />
<ul style={{ listStyle: "none", padding: 0 }}>
{filteredTodos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
))}
</ul>
<TodoStats todos={todos} />
</div>
);
}
שימו לב:
- סטייט מינימלי: todos ו-filter בלבד. filteredTodos הוא חישוב, לא סטייט
- סטייט באב: TodoApp מחזיק את הסטייט כי כל הקומפוננטות צריכות אותו
- זרימה הפוכה: AddTodoForm קורא ל-onAdd, TodoItem קורא ל-onToggle ו-onDelete
- סטייט מקומי: text של AddTodoForm הוא סטייט מקומי - רק הטופס צריך אותו
סיכום¶
- חשיבה בריאקט מתחילה בפירוק ה-UI לקומפוננטות לפי אחריות בודדת
- גרסה סטטית קודם - בלי סטייט, רק props
- סטייט מינימלי - שמרו רק מה שלא ניתן לחשב
- סטייט חי באב המשותף הקרוב ביותר של הקומפוננטות שצריכות אותו
- זרימה הפוכה - העברת callbacks כ-props כדי שילדים ישנו את סטייט האב
- דפוסי הרכבה: children, slots, custom hooks
- כללי אצבע: התחילו פשוט, פצלו כשצריך, העדיפו חישוב על סטייט