7.5 Error Boundaries, Portals ו Suspense פתרון
פתרון - Error Boundaries, Portals ו-Suspense¶
פתרון תרגיל 1 - Error Boundary מותאם אישית¶
import { Component, ErrorInfo, ReactNode, useState } from "react";
interface Props {
children: ReactNode;
name?: string;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
showDetails: boolean;
}
class CustomErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ errorInfo });
console.error(
`[ErrorBoundary${this.props.name ? ` - ${this.props.name}` : ""}]`,
{
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
}
);
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
showDetails: false,
});
};
render() {
if (this.state.hasError) {
return (
<div
style={{
padding: "20px",
margin: "10px",
border: "1px solid #f44336",
borderRadius: "8px",
backgroundColor: "#fff5f5",
}}
>
<h3>שגיאה {this.props.name ? `ב-${this.props.name}` : ""}</h3>
<p>אירעה שגיאה בלתי צפויה. אנא נסו שוב.</p>
<div style={{ display: "flex", gap: "8px" }}>
<button onClick={this.handleReset}>נסה שוב</button>
<button
onClick={() =>
this.setState((s) => ({ showDetails: !s.showDetails }))
}
>
{this.state.showDetails ? "הסתר פרטים" : "צפה בפרטים"}
</button>
</div>
{this.state.showDetails && (
<pre
style={{
marginTop: "12px",
padding: "12px",
backgroundColor: "#f0f0f0",
overflow: "auto",
fontSize: "12px",
}}
>
{this.state.error?.message}
{"\n\n"}
{this.state.error?.stack}
{"\n\nComponent Stack:"}
{this.state.errorInfo?.componentStack}
</pre>
)}
</div>
);
}
return this.props.children;
}
}
// קומפוננטות שזורקות שגיאות
function BuggyCounter() {
const [count, setCount] = useState(0);
if (count === 3) throw new Error("המונה הגיע ל-3!");
return (
<div>
<p>מונה: {count}</p>
<button onClick={() => setCount(count + 1)}>הגדל (קורס ב-3)</button>
</div>
);
}
function BuggyRenderer() {
const data: any = null;
return <p>{data.nonExistent}</p>;
}
// שימוש
function App() {
return (
<div>
<h1>דוגמת Error Boundary</h1>
<CustomErrorBoundary name="מונה">
<BuggyCounter />
</CustomErrorBoundary>
<CustomErrorBoundary name="רנדרר">
<BuggyRenderer />
</CustomErrorBoundary>
<p>התוכן הזה ממשיך לעבוד גם כשקומפוננטות אחרות נפלו</p>
</div>
);
}
הסבר:
- כל קומפוננטה עטופה ב-Error Boundary נפרד עם שם ייחודי
- כפתור "צפה בפרטים" מתקפל/מתפתח ומציג stack trace מלא
- הלוג כולל timestamp, שם ה-boundary, ו-component stack
פתרון תרגיל 2 - מערכת מודלים עם Portal¶
import { createPortal } from "react-dom";
import { useState, useEffect, useCallback, useRef } from "react";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
}
function Modal({ isOpen, onClose, title, children }: ModalProps) {
const [isAnimating, setIsAnimating] = useState(false);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (isOpen) {
setIsVisible(true);
requestAnimationFrame(() => setIsAnimating(true));
document.body.style.overflow = "hidden";
} else {
setIsAnimating(false);
const timer = setTimeout(() => setIsVisible(false), 300);
document.body.style.overflow = "";
return () => clearTimeout(timer);
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen, onClose]);
if (!isVisible) return null;
return createPortal(
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: `rgba(0, 0, 0, ${isAnimating ? 0.5 : 0})`,
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
transition: "background-color 0.3s ease",
}}
onClick={onClose}
>
<div
style={{
backgroundColor: "white",
borderRadius: "12px",
padding: "24px",
maxWidth: "500px",
width: "90%",
maxHeight: "80vh",
overflow: "auto",
transform: isAnimating ? "scale(1)" : "scale(0.9)",
opacity: isAnimating ? 1 : 0,
transition: "transform 0.3s ease, opacity 0.3s ease",
}}
onClick={(e) => e.stopPropagation()}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: "16px",
}}
>
{title && <h2 style={{ margin: 0 }}>{title}</h2>}
<button
onClick={onClose}
style={{
border: "none",
background: "none",
fontSize: "20px",
cursor: "pointer",
}}
>
X
</button>
</div>
{children}
</div>
</div>,
document.body
);
}
function ConfirmDialog({
isOpen,
onConfirm,
onCancel,
title,
message,
}: {
isOpen: boolean;
onConfirm: () => void;
onCancel: () => void;
title: string;
message: string;
}) {
return (
<Modal isOpen={isOpen} onClose={onCancel} title={title}>
<p>{message}</p>
<div style={{ display: "flex", gap: "8px", justifyContent: "flex-end" }}>
<button onClick={onCancel}>ביטול</button>
<button
onClick={onConfirm}
style={{ backgroundColor: "#f44336", color: "white" }}
>
אישור
</button>
</div>
</Modal>
);
}
function AlertDialog({
isOpen,
onClose,
title,
message,
}: {
isOpen: boolean;
onClose: () => void;
title: string;
message: string;
}) {
return (
<Modal isOpen={isOpen} onClose={onClose} title={title}>
<p>{message}</p>
<div style={{ textAlign: "left" }}>
<button onClick={onClose}>הבנתי</button>
</div>
</Modal>
);
}
// שימוש
function App() {
const [showModal, setShowModal] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [showNested, setShowNested] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>פתח מודל</button>
<button onClick={() => setShowConfirm(true)}>פתח אישור</button>
<Modal isOpen={showModal} onClose={() => setShowModal(false)} title="מודל ראשי">
<p>תוכן המודל</p>
<button onClick={() => setShowNested(true)}>פתח מודל מקונן</button>
<Modal isOpen={showNested} onClose={() => setShowNested(false)} title="מודל מקונן">
<p>זה מודל בתוך מודל!</p>
</Modal>
</Modal>
<ConfirmDialog
isOpen={showConfirm}
onConfirm={() => { alert("אושר!"); setShowConfirm(false); }}
onCancel={() => setShowConfirm(false)}
title="אישור מחיקה"
message="האם אתה בטוח שברצונך למחוק?"
/>
</div>
);
}
הסبر:
- האנימציה מבוססת על שני מצבים: isVisible (האם ב-DOM) ו-isAnimating (האם מוצג)
- כשנסגר, קודם מתחילה אנימציית יציאה ורק אחרי 300ms מורידים מה-DOM
- body.style.overflow = "hidden" מונע גלילה ברקע
- מודלים מקוננים עובדים כי כל אחד הוא Portal נפרד
פתרון תרגיל 3 - Tooltip ו-Dropdown עם Portals¶
import { createPortal } from "react-dom";
import { useState, useRef, useEffect, useCallback } from "react";
type Position = "top" | "bottom" | "left" | "right";
function Tooltip({
text,
position = "top",
children,
}: {
text: string;
position?: Position;
children: React.ReactNode;
}) {
const [show, setShow] = useState(false);
const [coords, setCoords] = useState({ top: 0, left: 0 });
const [actualPosition, setActualPosition] = useState(position);
const triggerRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (!show || !triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
let top = 0;
let left = 0;
let pos = position;
// בדיקת מקום
if (position === "top" && rect.top < 40) pos = "bottom";
if (position === "bottom" && window.innerHeight - rect.bottom < 40) pos = "top";
switch (pos) {
case "top":
top = rect.top - 8;
left = rect.left + rect.width / 2;
break;
case "bottom":
top = rect.bottom + 8;
left = rect.left + rect.width / 2;
break;
case "left":
top = rect.top + rect.height / 2;
left = rect.left - 8;
break;
case "right":
top = rect.top + rect.height / 2;
left = rect.right + 8;
break;
}
setCoords({ top, left });
setActualPosition(pos);
}, [show, position]);
const tooltipStyle: React.CSSProperties = {
position: "fixed",
top: coords.top,
left: coords.left,
transform:
actualPosition === "top"
? "translate(-50%, -100%)"
: actualPosition === "bottom"
? "translate(-50%, 0)"
: actualPosition === "left"
? "translate(-100%, -50%)"
: "translate(0, -50%)",
backgroundColor: "#333",
color: "white",
padding: "6px 10px",
borderRadius: "4px",
fontSize: "13px",
whiteSpace: "nowrap",
zIndex: 9999,
pointerEvents: "none",
};
return (
<>
<span
ref={triggerRef}
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
style={{ display: "inline-block" }}
>
{children}
</span>
{show &&
createPortal(<div style={tooltipStyle}>{text}</div>, document.body)}
</>
);
}
// Dropdown
interface MenuItem {
label: string;
onClick?: () => void;
children?: MenuItem[];
}
function Dropdown({
trigger,
items,
}: {
trigger: React.ReactNode;
items: MenuItem[];
}) {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const triggerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isOpen || !triggerRef.current) return;
const rect = triggerRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
setPosition({
top: spaceBelow > 200 ? rect.bottom + 4 : rect.top - 4,
left: rect.left,
});
}, [isOpen]);
useEffect(() => {
if (!isOpen) return;
const handler = (e: MouseEvent) => {
if (
triggerRef.current &&
!triggerRef.current.contains(e.target as Node)
) {
setIsOpen(false);
}
};
setTimeout(() => document.addEventListener("click", handler), 0);
return () => document.removeEventListener("click", handler);
}, [isOpen]);
return (
<>
<div
ref={triggerRef}
onClick={() => setIsOpen(!isOpen)}
style={{ display: "inline-block", cursor: "pointer" }}
>
{trigger}
</div>
{isOpen &&
createPortal(
<DropdownMenu items={items} position={position} onClose={() => setIsOpen(false)} />,
document.body
)}
</>
);
}
function DropdownMenu({
items,
position,
onClose,
}: {
items: MenuItem[];
position: { top: number; left: number };
onClose: () => void;
}) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
return (
<ul
style={{
position: "fixed",
top: position.top,
left: position.left,
backgroundColor: "white",
border: "1px solid #ddd",
borderRadius: "6px",
listStyle: "none",
padding: "4px 0",
margin: 0,
minWidth: "160px",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
zIndex: 1000,
}}
>
{items.map((item, index) => (
<li
key={index}
ref={(el) => { itemRefs.current[index] = el; }}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
onClick={() => {
item.onClick?.();
if (!item.children) onClose();
}}
style={{
padding: "8px 16px",
cursor: "pointer",
backgroundColor: hoveredIndex === index ? "#f0f0f0" : "transparent",
position: "relative",
}}
>
{item.label}
{item.children && " >"}
{item.children && hoveredIndex === index && (
<DropdownMenu
items={item.children}
position={{
top: itemRefs.current[index]?.getBoundingClientRect().top ?? 0,
left: (itemRefs.current[index]?.getBoundingClientRect().right ?? 0) + 4,
}}
onClose={onClose}
/>
)}
</li>
))}
</ul>
);
}
// שימוש
function App() {
const menuItems: MenuItem[] = [
{ label: "ערוך", onClick: () => console.log("ערוך") },
{
label: "שתף",
children: [
{ label: "אימייל", onClick: () => console.log("אימייל") },
{ label: "הודעה", onClick: () => console.log("הודעה") },
],
},
{ label: "מחק", onClick: () => console.log("מחק") },
];
return (
<div style={{ overflow: "hidden", height: "100px" }}>
<Tooltip text="זה טולטיפ!" position="top">
<span>רחף עליי</span>
</Tooltip>
<Dropdown trigger={<button>תפריט</button>} items={menuItems} />
</div>
);
}
הסبر:
- ה-Tooltip מחשב מיקום דינמי ומשנה כיוון אם אין מקום
- ה-Dropdown תומך בתפריטי משנה מקוננים על ידי רנדור רקורסיבי
- שניהם עובדים נכון גם כשלהורה יש overflow: hidden
פתרון תרגיל 4 - טעינה עצלה עם Suspense¶
import { lazy, Suspense, useState } from "react";
import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom";
import { ErrorBoundary } from "react-error-boundary";
// טעינה עצלה
const HomePage = lazy(() => import("./pages/HomePage"));
const ProductsPage = lazy(() => import("./pages/ProductsPage"));
const AboutPage = lazy(() => import("./pages/AboutPage"));
const ContactPage = lazy(() => import("./pages/ContactPage"));
// מפת דפים לטעינה מוקדמת
const pageImports: Record<string, () => Promise<any>> = {
"/": () => import("./pages/HomePage"),
"/products": () => import("./pages/ProductsPage"),
"/about": () => import("./pages/AboutPage"),
"/contact": () => import("./pages/ContactPage"),
};
function PreloadLink({
to,
children,
}: {
to: string;
children: React.ReactNode;
}) {
const handleMouseEnter = () => {
const importFn = pageImports[to];
if (importFn) importFn();
};
return (
<NavLink
to={to}
onMouseEnter={handleMouseEnter}
style={({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
padding: "8px 16px",
})}
>
{children}
</NavLink>
);
}
function SkeletonLoader() {
return (
<div style={{ padding: "20px" }}>
<div
style={{
height: "32px",
width: "60%",
backgroundColor: "#e0e0e0",
borderRadius: "4px",
marginBottom: "16px",
animation: "pulse 1.5s infinite",
}}
/>
{[1, 2, 3].map((i) => (
<div
key={i}
style={{
height: "16px",
width: `${80 - i * 10}%`,
backgroundColor: "#e0e0e0",
borderRadius: "4px",
marginBottom: "8px",
animation: "pulse 1.5s infinite",
}}
/>
))}
</div>
);
}
function PageErrorFallback({
error,
resetErrorBoundary,
}: {
error: Error;
resetErrorBoundary: () => void;
}) {
return (
<div style={{ padding: "20px", textAlign: "center" }}>
<h2>שגיאה בטעינת הדף</h2>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>נסה שוב</button>
</div>
);
}
function App() {
return (
<BrowserRouter>
<nav>
<PreloadLink to="/">בית</PreloadLink>
<PreloadLink to="/products">מוצרים</PreloadLink>
<PreloadLink to="/about">אודות</PreloadLink>
<PreloadLink to="/contact">צרו קשר</PreloadLink>
</nav>
<ErrorBoundary FallbackComponent={PageErrorFallback}>
<Suspense fallback={<SkeletonLoader />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
}
הסבר:
- PreloadLink מפעיל את ה-import כשהמשתמש מרחף על הלינק, כך שהטעינה מתחילה לפני הקליק
- SkeletonLoader מציג צורות אפורות שמדמות את המבנה של הדף
- Error Boundary עוטף את Suspense כדי לתפוס שגיאות טעינה
פתרון תרגיל 5 - מערכת התראות עם Portal¶
import { createPortal } from "react-dom";
import { createContext, useContext, useState, useCallback, useEffect, useRef } from "react";
type ToastType = "success" | "error" | "warning" | "info";
type ToastPosition = "top-right" | "top-left" | "bottom-right" | "bottom-left";
interface Toast {
id: string;
type: ToastType;
message: string;
duration: number;
}
interface ToastContextType {
addToast: (type: ToastType, message: string, duration?: number) => void;
}
const ToastContext = createContext<ToastContextType | null>(null);
function useToast() {
const context = useContext(ToastContext);
if (!context) throw new Error("useToast must be used within ToastProvider");
return context;
}
const MAX_TOASTS = 5;
function ToastProvider({
children,
position = "top-right",
}: {
children: React.ReactNode;
position?: ToastPosition;
}) {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback(
(type: ToastType, message: string, duration = 5000) => {
const id = Date.now().toString() + Math.random().toString(36).slice(2);
setToasts((prev) => {
const next = [...prev, { id, type, message, duration }];
if (next.length > MAX_TOASTS) {
return next.slice(next.length - MAX_TOASTS);
}
return next;
});
},
[]
);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, []);
const positionStyle: React.CSSProperties = {
position: "fixed",
zIndex: 9999,
display: "flex",
flexDirection: "column",
gap: "8px",
...(position.includes("top") ? { top: "20px" } : { bottom: "20px" }),
...(position.includes("right") ? { right: "20px" } : { left: "20px" }),
};
return (
<ToastContext.Provider value={{ addToast }}>
{children}
{createPortal(
<div style={positionStyle}>
{toasts.map((toast) => (
<ToastItem
key={toast.id}
toast={toast}
onRemove={removeToast}
position={position}
/>
))}
</div>,
document.body
)}
</ToastContext.Provider>
);
}
function ToastItem({
toast,
onRemove,
position,
}: {
toast: Toast;
onRemove: (id: string) => void;
position: ToastPosition;
}) {
const [isExiting, setIsExiting] = useState(false);
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (toast.duration > 0) {
timerRef.current = window.setTimeout(() => {
setIsExiting(true);
setTimeout(() => onRemove(toast.id), 300);
}, toast.duration);
}
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [toast.id, toast.duration, onRemove]);
const handleClose = () => {
setIsExiting(true);
setTimeout(() => onRemove(toast.id), 300);
};
const colors: Record<ToastType, { bg: string; border: string }> = {
success: { bg: "#d4edda", border: "#28a745" },
error: { bg: "#f8d7da", border: "#dc3545" },
warning: { bg: "#fff3cd", border: "#ffc107" },
info: { bg: "#d1ecf1", border: "#17a2b8" },
};
const slideDirection = position.includes("right") ? "20px" : "-20px";
return (
<div
style={{
backgroundColor: colors[toast.type].bg,
borderRight: `4px solid ${colors[toast.type].border}`,
padding: "12px 16px",
borderRadius: "4px",
minWidth: "280px",
maxWidth: "400px",
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
transform: isExiting ? `translateX(${slideDirection})` : "translateX(0)",
opacity: isExiting ? 0 : 1,
transition: "transform 0.3s ease, opacity 0.3s ease",
animation: "slideIn 0.3s ease",
}}
>
<span>{toast.message}</span>
<button
onClick={handleClose}
style={{
border: "none",
background: "none",
cursor: "pointer",
fontSize: "16px",
marginRight: "8px",
}}
>
X
</button>
</div>
);
}
// שימוש
function DemoPage() {
const { addToast } = useToast();
return (
<div>
<h1>דוגמת התראות</h1>
<button onClick={() => addToast("success", "הפעולה בוצעה בהצלחה!")}>
הצלחה
</button>
<button onClick={() => addToast("error", "אירעה שגיאה בשמירה")}>
שגיאה
</button>
<button onClick={() => addToast("warning", "שים לב - פעולה זו לא ניתנת לביטול")}>
אזהרה
</button>
<button onClick={() => addToast("info", "עדכון חדש זמין")}>
מידע
</button>
</div>
);
}
function App() {
return (
<ToastProvider position="top-right">
<DemoPage />
</ToastProvider>
);
}
הסבר:
- ההתראות מרונדרות ב-Portal לגוף הדף
- מקסימום 5 התראות - חדשה דוחפת את הישנה ביותר
- אנימציית כניסה (slideIn) ויציאה (fade+slide)
- כל התראה מתנקה אוטומטית אחרי ה-duration שלה
פתרון תרגיל 6 - אפליקציה מלאה עם שלושת המנגנונים¶
הפתרון משלב את כל המנגנונים שלמדנו. הנה המבנה העיקרי:
import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ErrorBoundary } from "react-error-boundary";
// טעינה עצלה
const StorePage = lazy(() => import("./pages/StorePage"));
const CartPage = lazy(() => import("./pages/CartPage"));
const CheckoutPage = lazy(() => import("./pages/CheckoutPage"));
function App() {
return (
<BrowserRouter>
<ToastProvider>
<CartProvider>
<ErrorBoundary FallbackComponent={GlobalErrorFallback}>
<Layout>
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route
path="/"
element={
<ErrorBoundary FallbackComponent={PageErrorFallback}>
<StorePage />
</ErrorBoundary>
}
/>
<Route
path="/cart"
element={
<ErrorBoundary FallbackComponent={PageErrorFallback}>
<CartPage />
</ErrorBoundary>
}
/>
<Route
path="/checkout"
element={
<ErrorBoundary FallbackComponent={PageErrorFallback}>
<CheckoutPage />
</ErrorBoundary>
}
/>
</Routes>
</Suspense>
</Layout>
</ErrorBoundary>
</CartProvider>
</ToastProvider>
</BrowserRouter>
);
}
הסבר:
- שלושה שכבות: ToastProvider (Portals) > ErrorBoundary (שגיאות) > Suspense (טעינה)
- כל דף עטוף ב-ErrorBoundary נפרד כדי שנפילת דף אחד לא תפיל את האחרים
- Toasts משמשים להודעות הצלחה/שגיאה ב-CRUD operations
- מודלים (הוספה לסל, אישור הזמנה) מרונדרים כ-Portals
תשובות לשאלות¶
-
Error Boundaries ומחלקות: Error Boundaries מוגבלים למחלקות כי הם מסתמכים על lifecycle methods (getDerivedStateFromError, componentDidCatch) שאין להם מקבילה בהוקים. ספריית react-error-boundary מספקת wrapper שמאפשר שימוש ב-ErrorBoundary כקומפוננטה רגילה, ו-useErrorBoundary מאפשר לזרוק שגיאות מקוד אסינכרוני.
-
Event bubbling ב-Portals: למרות שב-DOM האלמנט נמצא במקום אחר (למשל ב-body), בריאקט אירועים ממשיכים לעלות דרך עץ הקומפוננטות המקורי. אז onClick על אלמנט בתוך Portal יעלה לקומפוננטת ההורה הריאקטית, לא להורה ב-DOM.
-
מתי להשתמש ב-lazy: מומלץ לנתיבים (routes) של דפים שלמים, לקומפוננטות כבדות שלא תמיד נדרשות (כמו עורך קוד, גרף מורכב), ולתכונות שרק חלק מהמשתמשים ניגשים אליהם. לא מומלץ לקומפוננטות קטנות (ה-overhead של network request גדול מהחיסכון) או לתוכן שהמשתמש תמיד רואה (above the fold).
-
סכנות Portals: שימוש יתר עלול לגרום לבעיות נגישות (a11y) כי קוראי מסך עשויים לא לזהות את הקשר בין ה-Portal לטריגר. כמו כן, ניהול focus, z-index ו-stacking context הופך מורכב. עדיף לא להשתמש ב-Portal כשאפשר לפתור את הבעיה עם CSS (למשל position: fixed).
-
איך Suspense עובד: כש-React.lazy טוען קומפוננטה, הוא זורק Promise. Suspense תופס את ה-Promise הזה, מציג את ה-fallback, וכש-Promise מסתיים (הקומפוננטה נטענה), ריאקט מרנדר מחדש ומציג את הקומפוננטה. זה מנגנון דומה ל-Error Boundary, אבל עבור Promises במקום Errors.