7.6 דפוסי תכנון וביצועים הרצאה
דפוסי תכנון וביצועים - Design Patterns and Performance¶
בשיעור הזה נלמד על דפוסי תכנון (Design Patterns) נפוצים בריאקט ועל כלים לאופטימיזציית ביצועים.
הרכבה מול הורשה - Composition vs Inheritance¶
עקרון ההרכבה¶
בריאקט, הרכבה (composition) היא הדרך המומלצת לשימוש חוזר בקוד. במקום ליצור היררכיית ירושה, אנחנו מרכיבים קומפוננטות מקומפוננטות קטנות יותר:
// הרכבה - נכון
function Dialog({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="dialog">
<h2>{title}</h2>
<div className="dialog-body">{children}</div>
</div>
);
}
function WelcomeDialog() {
return (
<Dialog title="ברוכים הבאים">
<p>תודה שנרשמת לאפליקציה!</p>
<button>התחל</button>
</Dialog>
);
}
function ConfirmDialog({ message, onConfirm }: { message: string; onConfirm: () => void }) {
return (
<Dialog title="אישור">
<p>{message}</p>
<button onClick={onConfirm}>אשר</button>
</Dialog>
);
}
Slots עם children ו-props¶
interface PageLayoutProps {
header: React.ReactNode;
sidebar: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
}
function PageLayout({ header, sidebar, children, footer }: PageLayoutProps) {
return (
<div className="page">
<header>{header}</header>
<div className="content">
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
{footer && <footer>{footer}</footer>}
</div>
);
}
function DashboardPage() {
return (
<PageLayout
header={<Navigation />}
sidebar={<DashboardSidebar />}
footer={<Copyright />}
>
<h1>לוח בקרה</h1>
<DashboardContent />
</PageLayout>
);
}
דפוס Render Props¶
מה זה Render Props?¶
דפוס שבו קומפוננטה מקבלת פונקציה כ-prop ומשתמשת בה כדי להחליט מה לרנדר:
interface MousePosition {
x: number;
y: number;
}
interface MouseTrackerProps {
render: (position: MousePosition) => React.ReactNode;
}
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} style={{ height: "300px" }}>
{render(position)}
</div>
);
}
// שימוש
function App() {
return (
<MouseTracker
render={({ x, y }) => (
<p>
מיקום העכבר: ({x}, {y})
</p>
)}
/>
);
}
וריאנט - children כפונקציה¶
interface ToggleProps {
children: (props: { isOn: boolean; toggle: () => void }) => React.ReactNode;
}
function Toggle({ children }: ToggleProps) {
const [isOn, setIsOn] = useState(false);
const toggle = () => setIsOn((prev) => !prev);
return <>{children({ isOn, toggle })}</>;
}
function App() {
return (
<Toggle>
{({ isOn, toggle }) => (
<div>
<p>מצב: {isOn ? "פעיל" : "כבוי"}</p>
<button onClick={toggle}>החלף</button>
</div>
)}
</Toggle>
);
}
- בימינו, הוקים מותאמים אישית מחליפים את רוב השימושים ב-Render Props
- עדיין שימושי כשצריך שליטה בקומפוננטת הילד מתוך ההורה
קומפוננטות מעטפת - Higher-Order Components (HOC)¶
מה זה HOC?¶
פונקציה שמקבלת קומפוננטה ומחזירה קומפוננטה חדשה עם יכולות נוספות:
function withAuth<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function AuthenticatedComponent(props: P) {
const { user } = useAuth();
if (!user) {
return <p>יש להתחבר כדי לצפות בתוכן זה</p>;
}
return <WrappedComponent {...props} />;
};
}
// שימוש
function Dashboard() {
return <h1>לוח בקרה</h1>;
}
const ProtectedDashboard = withAuth(Dashboard);
דוגמה - HOC לטעינת נתונים¶
interface WithLoadingProps {
loading: boolean;
}
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
) {
return function LoadingComponent(props: P & WithLoadingProps) {
const { loading, ...rest } = props as WithLoadingProps & Record<string, any>;
if (loading) {
return (
<div style={{ textAlign: "center", padding: "20px" }}>
<p>טוען...</p>
</div>
);
}
return <WrappedComponent {...(rest as P)} />;
};
}
function UserList({ users }: { users: string[] }) {
return (
<ul>
{users.map((user, i) => (
<li key={i}>{user}</li>
))}
</ul>
);
}
const UserListWithLoading = withLoading(UserList);
// שימוש
function App() {
return <UserListWithLoading loading={false} users={["דני", "מיכל"]} />;
}
- הHOC היה נפוץ מאוד לפני הוקים, אבל כיום הוקים מספקים פתרון פשוט יותר ברוב המקרים
- עדיין שימושי כשצריך לעטוף קומפוננטה ללא שינוי שלה
דפוס קומפוננטות מורכבות - Compound Components¶
מה זה?¶
דפוס שבו קבוצת קומפוננטות עובדות יחד ומשתפות state דרך context:
import { createContext, useContext, useState } from "react";
interface AccordionContextType {
activeIndex: number | null;
toggleItem: (index: number) => void;
}
const AccordionContext = createContext<AccordionContextType | null>(null);
function useAccordion() {
const context = useContext(AccordionContext);
if (!context) throw new Error("Must be used within Accordion");
return context;
}
function Accordion({ children }: { children: React.ReactNode }) {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const toggleItem = (index: number) => {
setActiveIndex((prev) => (prev === index ? null : index));
};
return (
<AccordionContext.Provider value={{ activeIndex, toggleItem }}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
}
function AccordionItem({
index,
children,
}: {
index: number;
children: React.ReactNode;
}) {
return <div className="accordion-item">{children}</div>;
}
function AccordionHeader({
index,
children,
}: {
index: number;
children: React.ReactNode;
}) {
const { activeIndex, toggleItem } = useAccordion();
const isOpen = activeIndex === index;
return (
<button
onClick={() => toggleItem(index)}
style={{
width: "100%",
padding: "12px",
textAlign: "right",
backgroundColor: isOpen ? "#e3f2fd" : "#f5f5f5",
border: "1px solid #ddd",
cursor: "pointer",
}}
>
{children} {isOpen ? "▲" : "▼"}
</button>
);
}
function AccordionPanel({
index,
children,
}: {
index: number;
children: React.ReactNode;
}) {
const { activeIndex } = useAccordion();
const isOpen = activeIndex === index;
if (!isOpen) return null;
return (
<div style={{ padding: "12px", border: "1px solid #ddd", borderTop: "none" }}>
{children}
</div>
);
}
// חיבור הקומפוננטות ל-namespace
Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Panel = AccordionPanel;
// שימוש
function FAQ() {
return (
<Accordion>
<Accordion.Item index={0}>
<Accordion.Header index={0}>מה זה ריאקט?</Accordion.Header>
<Accordion.Panel index={0}>
ריאקט היא ספריית JavaScript לבניית ממשקי משתמש.
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item index={1}>
<Accordion.Header index={1}>למה להשתמש בריאקט?</Accordion.Header>
<Accordion.Panel index={1}>
ריאקט מאפשרת לבנות ממשקים מורכבים מקומפוננטות פשוטות.
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}
דוגמה נוספת - Tabs¶
const TabsContext = createContext<{
activeTab: string;
setActiveTab: (tab: string) => void;
} | null>(null);
function Tabs({
defaultTab,
children,
}: {
defaultTab: string;
children: React.ReactNode;
}) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div>{children}</div>
</TabsContext.Provider>
);
}
function TabList({ children }: { children: React.ReactNode }) {
return <div style={{ display: "flex", borderBottom: "2px solid #ddd" }}>{children}</div>;
}
function Tab({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useContext(TabsContext)!;
return (
<button
onClick={() => setActiveTab(value)}
style={{
padding: "10px 20px",
border: "none",
borderBottom: activeTab === value ? "2px solid #3b82f6" : "none",
backgroundColor: "transparent",
fontWeight: activeTab === value ? "bold" : "normal",
cursor: "pointer",
}}
>
{children}
</button>
);
}
function TabPanel({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab } = useContext(TabsContext)!;
if (activeTab !== value) return null;
return <div style={{ padding: "16px" }}>{children}</div>;
}
// שימוש
function App() {
return (
<Tabs defaultTab="overview">
<TabList>
<Tab value="overview">סקירה</Tab>
<Tab value="features">תכונות</Tab>
<Tab value="pricing">מחירים</Tab>
</TabList>
<TabPanel value="overview">תוכן סקירה</TabPanel>
<TabPanel value="features">תוכן תכונות</TabPanel>
<TabPanel value="pricing">תוכן מחירים</TabPanel>
</Tabs>
);
}
אופטימיזציית ביצועים - React.memo¶
מתי קומפוננטות עוברות רנדר מחדש?¶
קומפוננטה עוברת רנדר מחדש כש:
- ה-state שלה משתנה
- ה-props שלה משתנים
- קומפוננטת ההורה שלה עוברת רנדר מחדש (גם אם ה-props לא השתנו!)
מה React.memo עושה?¶
React.memo עוטפת קומפוננטה ומונעת רנדר מחדש אם ה-props לא השתנו:
import { memo } from "react";
interface UserCardProps {
name: string;
email: string;
avatar: string;
}
const UserCard = memo(function UserCard({ name, email, avatar }: UserCardProps) {
console.log(`Rendering UserCard: ${name}`);
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{email}</p>
</div>
);
});
השוואה מותאמת אישית¶
interface ListItemProps {
item: { id: number; text: string; count: number };
onSelect: (id: number) => void;
}
const ListItem = memo(
function ListItem({ item, onSelect }: ListItemProps) {
return (
<li onClick={() => onSelect(item.id)}>
{item.text} ({item.count})
</li>
);
},
(prevProps, nextProps) => {
// מחזיר true אם ה-props שווים (לא צריך רנדר)
return (
prevProps.item.id === nextProps.item.id &&
prevProps.item.text === nextProps.item.text &&
prevProps.item.count === nextProps.item.count
);
}
);
React DevTools Profiler¶
מה זה?¶
כלי שמובנה ב-React DevTools שמאפשר למדוד ביצועי רנדור:
- מדד כמה זמן לוקח לכל קומפוננטה לרנדר
- מזהה קומפוננטות שמתרנדרות מחדש ללא צורך
- מציג flame chart של רנדרים
איך להשתמש¶
- פתחו את React DevTools בדפדפן
- עברו ללשונית "Profiler"
- לחצו על כפתור ההקלטה
- בצעו את הפעולה שרוצים לבדוק
- עצרו את ההקלטה ונתחו את התוצאות
מה לחפש¶
- קומפוננטות שמתרנדרות מחדש הרבה פעמים
- רנדרים ארוכים (מעל 16ms)
- קומפוננטות שמתרנדרות ללא סיבה (ה-props לא השתנו)
הגדרות מועילות¶
בהגדרות של React DevTools, הפעילו:
- "Highlight updates when components render" - מדגיש ויזואלית קומפוננטות שמרונדרות
- "Record why each component rendered" - מתעד למה כל קומפוננטה רונדרה מחדש
טיפים לאופטימיזציה¶
מניעת רנדרים מיותרים¶
// בעיה - אובייקט חדש בכל רנדר
function Parent() {
return <Child style={{ color: "red" }} />;
}
// פתרון - הוצאה מחוץ לקומפוננטה
const childStyle = { color: "red" };
function Parent() {
return <Child style={childStyle} />;
}
// בעיה - פונקציה חדשה בכל רנדר
function Parent() {
return <Child onClick={() => console.log("click")} />;
}
// פתרון - useCallback
function Parent() {
const handleClick = useCallback(() => console.log("click"), []);
return <Child onClick={handleClick} />;
}
רשימות ארוכות - Virtualization¶
עבור רשימות עם הרבה פריטים, אפשר להשתמש ב-virtualization שמרנדר רק את הפריטים הנראים:
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
function VirtualList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
});
return (
<div
ref={parentRef}
style={{ height: "400px", overflow: "auto" }}
>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: "relative",
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: `${virtualItem.size}px`,
width: "100%",
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
</div>
);
}
סיכום¶
- הרכבה (composition) היא הדרך המומלצת לשימוש חוזר בקוד בריאקט, לא ירושה
- Render Props מאפשרים שיתוף לוגיקה דרך פונקציה כ-prop, אבל כיום הוקים מחליפים רוב השימושים
- HOC (Higher-Order Components) עוטפים קומפוננטה ומוסיפים יכולות - שימושי כשלא רוצים לשנות את הקומפוננטה המקורית
- Compound Components מאפשרות ליצור קבוצת קומפוננטות שעובדות יחד דרך context
- React.memo מונע רנדרים מיותרים כשה-props לא השתנו
- React DevTools Profiler מאפשר לזהות בעיות ביצועים ולמדוד שיפורים
- אופטימיזציה מוקדמת היא שורש כל רע - קודם כותבים קוד נכון, ורק אם יש בעיית ביצועים מטפלים