לדלג לתוכן

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 של רנדרים

איך להשתמש

  1. פתחו את React DevTools בדפדפן
  2. עברו ללשונית "Profiler"
  3. לחצו על כפתור ההקלטה
  4. בצעו את הפעולה שרוצים לבדוק
  5. עצרו את ההקלטה ונתחו את התוצאות

מה לחפש

  • קומפוננטות שמתרנדרות מחדש הרבה פעמים
  • רנדרים ארוכים (מעל 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 שמרנדר רק את הפריטים הנראים:

npm install @tanstack/react-virtual
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 מאפשר לזהות בעיות ביצועים ולמדוד שיפורים
  • אופטימיזציה מוקדמת היא שורש כל רע - קודם כותבים קוד נכון, ורק אם יש בעיית ביצועים מטפלים